@pikku/core 0.12.139 → 0.12.142
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 +47 -0
- package/dist/errors/errors.d.ts +1 -1
- package/dist/errors/errors.js +1 -1
- package/dist/function/function-meta.types.d.ts +6 -6
- package/dist/function/functions.types.d.ts +2 -2
- package/dist/lifecycle.d.ts +6 -0
- package/dist/lifecycle.js +58 -0
- package/dist/services/audit-service.d.ts +4 -4
- package/dist/services/feature-flag-service.d.ts +3 -3
- package/dist/services/{http-personas.d.ts → http-virtual-users.d.ts} +25 -47
- package/dist/services/{http-personas.js → http-virtual-users.js} +29 -53
- package/dist/services/index.d.ts +2 -2
- package/dist/services/index.js +1 -1
- package/dist/services/meta-service.d.ts +6 -6
- package/dist/services/meta-service.js +8 -8
- package/dist/{wirings/workflow → services}/scenario-cookie-jar.d.ts +2 -0
- package/dist/{wirings/workflow → services}/scenario-cookie-jar.js +18 -14
- package/dist/services/scoped-secret-service.d.ts +1 -0
- package/dist/services/scoped-secret-service.js +3 -0
- package/dist/services/secret-service.d.ts +6 -0
- package/dist/services/system-role-guard.d.ts +1 -1
- package/dist/services/typed-secret-service.d.ts +1 -0
- package/dist/services/typed-secret-service.js +7 -0
- package/dist/services/virtual-user-sign-in.d.ts +52 -0
- package/dist/services/virtual-user-sign-in.js +80 -0
- package/dist/services/virtual-user-statement.d.ts +31 -0
- package/dist/services/virtual-user-statement.js +24 -0
- package/dist/services/{personas-service.d.ts → virtual-users-service.d.ts} +25 -23
- package/dist/services/{personas-service.js → virtual-users-service.js} +3 -3
- package/dist/testing/service-tests/secret-service-tests.js +9 -0
- package/dist/types/core.types.d.ts +11 -11
- package/dist/utils.d.ts +2 -5
- package/dist/utils.js +1 -38
- package/dist/wirings/cli/cli-runner.js +24 -9
- package/dist/wirings/http/http-runner.js +4 -0
- package/dist/wirings/http/http.types.d.ts +1 -0
- package/dist/wirings/http/pikku-fetch-http-response.d.ts +1 -0
- package/dist/wirings/http/pikku-fetch-http-response.js +12 -0
- package/dist/wirings/http/web-request.js +7 -0
- package/dist/wirings/role/define-system-role.d.ts +1 -1
- package/dist/wirings/role/define-system-role.js +1 -1
- package/dist/wirings/{actor-flow/actor-flow.types.d.ts → virtual-user/conversation/conversation.types.d.ts} +5 -5
- package/dist/wirings/virtual-user/conversation/index.d.ts +2 -0
- package/dist/wirings/virtual-user/conversation/run-conversation.d.ts +36 -0
- package/dist/wirings/{actor-flow → virtual-user/conversation}/run-conversation.js +30 -30
- package/dist/wirings/virtual-user/define-virtual-users.d.ts +32 -0
- package/dist/wirings/{persona/define-personas.js → virtual-user/define-virtual-users.js} +9 -9
- package/dist/wirings/virtual-user/index.d.ts +30 -7
- package/dist/wirings/virtual-user/index.js +23 -4
- package/dist/wirings/virtual-user/prepare-virtual-user-run.d.ts +8 -8
- package/dist/wirings/virtual-user/prepare-virtual-user-run.js +9 -9
- package/dist/wirings/virtual-user/run-virtual-user.d.ts +6 -6
- package/dist/wirings/virtual-user/run-virtual-user.js +10 -10
- package/dist/wirings/{persona/validate-personas.d.ts → virtual-user/validate-virtual-users.d.ts} +11 -11
- package/dist/wirings/{persona/validate-personas.js → virtual-user/validate-virtual-users.js} +24 -24
- package/dist/wirings/virtual-user/virtual-user-agents.d.ts +7 -7
- package/dist/wirings/virtual-user/virtual-user-agents.js +2 -2
- package/dist/wirings/{persona/persona-app-scopes.d.ts → virtual-user/virtual-user-app-scopes.d.ts} +10 -10
- package/dist/wirings/{persona/persona-app-scopes.js → virtual-user/virtual-user-app-scopes.js} +12 -12
- package/dist/wirings/virtual-user/virtual-user-catalogue.d.ts +3 -3
- package/dist/wirings/virtual-user/virtual-user-catalogue.js +2 -2
- package/dist/wirings/{persona/persona.types.d.ts → virtual-user/virtual-user-declaration.types.d.ts} +24 -24
- package/dist/wirings/virtual-user/virtual-user-derive.js +13 -7
- package/dist/wirings/virtual-user/virtual-user-dispositions.d.ts +1 -1
- package/dist/wirings/{persona/persona-email.d.ts → virtual-user/virtual-user-email.d.ts} +11 -11
- package/dist/wirings/{persona/persona-email.js → virtual-user/virtual-user-email.js} +19 -19
- package/dist/wirings/{persona/persona-environments.d.ts → virtual-user/virtual-user-environments.d.ts} +10 -10
- package/dist/wirings/{persona/persona-environments.js → virtual-user/virtual-user-environments.js} +21 -21
- package/dist/wirings/virtual-user/virtual-user-intents.d.ts +3 -3
- package/dist/wirings/virtual-user/virtual-user-intents.js +3 -3
- package/dist/wirings/{persona/persona-mailbox.d.ts → virtual-user/virtual-user-mailbox.d.ts} +4 -4
- package/dist/wirings/virtual-user/virtual-user-run-store.d.ts +4 -4
- package/dist/wirings/virtual-user/virtual-user-scaffold.d.ts +42 -61
- package/dist/wirings/virtual-user/virtual-user-scaffold.js +69 -117
- package/dist/wirings/virtual-user/virtual-user-schedule-store.d.ts +15 -15
- package/dist/wirings/virtual-user/virtual-user-schedule.d.ts +6 -6
- package/dist/wirings/virtual-user/virtual-user-schedule.js +11 -11
- package/dist/wirings/virtual-user/virtual-user-target.d.ts +6 -6
- package/dist/wirings/virtual-user/virtual-user-target.js +5 -5
- package/dist/wirings/virtual-user/virtual-user.types.d.ts +11 -11
- package/dist/wirings/virtual-user/virtual-user.types.js +1 -1
- package/dist/wirings/workflow/dsl/workflow-dsl.types.d.ts +9 -9
- package/dist/wirings/workflow/pikku-scenario-service.d.ts +28 -28
- package/dist/wirings/workflow/pikku-scenario-service.js +77 -77
- package/dist/wirings/workflow/pikku-workflow-service.d.ts +2 -2
- package/dist/wirings/workflow/pikku-workflow-service.js +4 -4
- package/dist/wirings/workflow/scenario-prose.d.ts +8 -8
- package/dist/wirings/workflow/scenario-prose.js +12 -8
- package/dist/wirings/workflow/scenario-run.types.d.ts +13 -13
- package/dist/wirings/workflow/scenario-step.types.d.ts +23 -23
- package/dist/wirings/workflow/workflow.types.d.ts +2 -2
- package/knowledge/decisions/index.md +0 -2
- package/knowledge/decisions/internals/a-credential-name-declared-twice-must-agree-on-schema-and-type.md +29 -0
- package/knowledge/decisions/internals/a-grant-can-name-a-scope-declared-without-its-addon.md +29 -0
- package/knowledge/decisions/internals/a-scheduled-task-writes-its-caller-session-before-it-runs.md +22 -0
- package/knowledge/decisions/internals/a-secret-set-in-memory-shadows-the-variable-of-the-same-name.md +28 -0
- package/knowledge/decisions/internals/a-virtual-user-acts-through-invokeraw-so-a-refusal-is-data.md +32 -0
- package/knowledge/decisions/internals/a-virtual-user-cadence-is-a-row-not-a-timer.md +12 -12
- package/knowledge/decisions/internals/a-virtual-user-converse-signs-in-before-its-first-turn.md +24 -0
- package/knowledge/decisions/internals/a-virtual-user-email-is-computed-with-the-run-id-as-a-sub-address.md +34 -0
- package/knowledge/decisions/internals/a-virtual-user-run-is-not-a-workflow-but-it-needs-a-trigger.md +2 -2
- package/knowledge/decisions/internals/an-agent-is-offered-under-its-registry-key-and-only-within-the-virtual-users-scopes.md +27 -0
- package/knowledge/decisions/internals/an-oauth2-credential-implies-an-optional-app-secret.md +26 -0
- package/knowledge/decisions/internals/analytics-identity-resolvers-run-in-declared-order.md +22 -0
- package/knowledge/decisions/internals/feature-flag-declarations-refuse-an-empty-anyof-and-conflicting-scopes.md +22 -0
- package/knowledge/decisions/internals/feature-flags-fail-open-and-never-enforce-access.md +28 -0
- package/knowledge/decisions/internals/flag-sync-rewrites-only-the-declared-text.md +24 -0
- package/knowledge/decisions/internals/in-a-scenario-a-4xx-is-data-not-an-exception.md +3 -3
- package/knowledge/decisions/internals/index.md +46 -31
- package/knowledge/decisions/internals/one-project-shape-check-two-validators.md +10 -10
- package/knowledge/decisions/internals/role-names-cannot-contain-the-scope-separator.md +30 -0
- package/knowledge/decisions/internals/scenario-agent-calls-sign-in-on-401-only.md +23 -19
- package/knowledge/decisions/internals/scenario-steps-return-drained-response-records.md +2 -2
- package/knowledge/decisions/internals/scenarios-live-in-files-named-for-them.md +12 -12
- package/knowledge/decisions/internals/the-core-scope-validator-refuses-separator-wildcard-and-empty-segments.md +22 -0
- package/knowledge/decisions/internals/the-pikku-user-id-resolves-in-a-fixed-order.md +24 -0
- package/knowledge/decisions/internals/{the-actor-prompt-says-json-because-of-json-object-mode.md → the-virtual-user-prompt-says-json-because-of-json-object-mode.md} +4 -4
- package/knowledge/decisions/internals/{the-persona-runtime-is-exported-from-the-persona-entry-point.md → the-virtual-user-runtime-is-exported-from-the-virtual-user-entry-point.md} +8 -8
- package/knowledge/decisions/internals/validate-checks-virtual-users-through-a-shared-module.md +43 -0
- package/knowledge/decisions/internals/{actor-flow-drives-the-target-through-a-transport-seam.md → virtual-user-conversation-drives-the-target-through-a-transport-seam.md} +6 -6
- package/knowledge/decisions/internals/virtual-user-conversation-verdicts-are-llm-self-evaluations.md +25 -0
- package/knowledge/decisions/internals/virtual-user-conversations-seed-a-hidden-kickoff-message.md +23 -0
- package/knowledge/decisions/internals/virtual-users-are-declared-in-one-definevirtualusers-call-per-codebase.md +30 -0
- package/knowledge/decisions/internals/workflow-core-never-imports-a-browser-driver.md +5 -5
- package/knowledge/decisions/internals/workflow-run-capabilities-are-extensions-not-subclasses.md +2 -2
- package/knowledge/decisions/internals/workflow-scenario-assertions-never-retry-and-record-one-step.md +4 -4
- package/knowledge/decisions/internals/workflow-scenario-hooks-are-a-scenario-only-affordance.md +1 -1
- package/knowledge/decisions/internals/workflow-scenario-prose-is-rendered-from-typed-calls-not-parsed-from-english.md +2 -2
- package/knowledge/decisions/internals/workflow-scenario-step-targets-are-string-literals-for-the-inspector.md +1 -1
- package/knowledge/decisions/internals/workflow-suspended-runs-keep-their-in-process-context.md +1 -1
- package/knowledge/decisions/security/a-bearer-token-checked-against-a-secret-reads-it-per-request.md +26 -0
- package/knowledge/decisions/security/a-call-without-a-session-raises-missing-session-not-forbidden.md +20 -0
- package/knowledge/decisions/security/a-caller-cookie-wins-over-the-jar-for-the-same-name.md +23 -0
- package/knowledge/decisions/security/a-channel-lifecycle-acts-as-the-socket-upgrade-session.md +16 -0
- package/knowledge/decisions/security/a-channel-requires-a-session-unless-it-says-auth-false.md +26 -0
- package/knowledge/decisions/security/a-credential-read-path-follows-its-declared-type-not-absence.md +32 -0
- package/knowledge/decisions/security/a-credential-type-is-a-literal-the-inspector-checks-at-declaration.md +27 -0
- package/knowledge/decisions/security/a-first-party-cookie-is-written-only-after-its-purposes-consent.md +33 -0
- package/knowledge/decisions/security/a-readonly-session-reaches-only-readonly-functions.md +25 -0
- package/knowledge/decisions/security/a-scaffold-flag-says-a-surface-exists-not-who-may-call-it.md +10 -5
- package/knowledge/decisions/security/a-scheduled-virtual-user-run-needs-no-admin-at-run-time.md +27 -0
- package/knowledge/decisions/security/a-scoped-credential-view-is-limited-to-declared-names.md +26 -0
- package/knowledge/decisions/security/a-scoped-secret-view-reads-only-declared-keys-and-never-writes.md +25 -0
- package/knowledge/decisions/security/a-secret-is-sent-only-to-the-hosts-it-declares.md +29 -0
- package/knowledge/decisions/security/a-step-can-name-an-identity-no-virtual-user-holds-through-per-call-headers.md +25 -0
- package/knowledge/decisions/security/a-stopped-run-by-id-skips-the-ownership-check.md +24 -0
- package/knowledge/decisions/security/a-vault-secret-redacts-on-serialization-and-throws-on-coercion.md +29 -0
- package/knowledge/decisions/security/a-virtual-user-catalogue-entry-is-filtered-by-scope-and-approval-is-denied-by-default.md +30 -0
- package/knowledge/decisions/security/a-virtual-user-environment-fails-closed.md +38 -0
- package/knowledge/decisions/security/a-virtual-user-never-runs-against-production.md +23 -0
- package/knowledge/decisions/security/a-virtual-user-reads-its-roles-from-the-apps-own-rpc-before-better-auth.md +27 -0
- package/knowledge/decisions/security/a-virtual-user-run-treats-an-unresolved-environment-as-production.md +41 -0
- package/knowledge/decisions/security/a-virtual-user-scopes-are-expanded-from-its-roles-once-at-run-preparation.md +25 -0
- package/knowledge/decisions/security/a-virtual-user-whose-roles-drift-from-the-declaration-refuses-to-run.md +32 -0
- package/knowledge/decisions/security/addon-screen-scopes-hide-and-do-not-enforce.md +20 -0
- package/knowledge/decisions/security/an-addon-header-can-only-narrow-the-session.md +26 -0
- package/knowledge/decisions/security/an-addon-instance-reads-only-the-secrets-and-credentials-it-is-named.md +33 -0
- package/knowledge/decisions/security/an-addon-may-reach-only-itself-and-the-addons-it-uses.md +16 -0
- package/knowledge/decisions/security/an-api-key-is-jwt-decoded-into-the-session.md +26 -0
- package/knowledge/decisions/security/an-external-virtual-user-caller-presents-one-signed-statement.md +38 -0
- package/knowledge/decisions/security/an-inbound-webhook-source-dispatches-only-verified-requests.md +31 -0
- package/knowledge/decisions/security/an-mcp-call-with-no-http-request-stays-anonymous.md +18 -0
- package/knowledge/decisions/security/an-omitted-user-id-addresses-the-platform-credential.md +32 -0
- package/knowledge/decisions/security/analytics-identity-is-stamped-from-the-session-not-the-event.md +24 -0
- package/knowledge/decisions/security/app-access-is-a-scope-tree-synthesised-from-virtual-users.md +26 -0
- package/knowledge/decisions/security/approval-policy-decides-who-answers-a-tool-call.md +27 -0
- package/knowledge/decisions/security/auth-false-on-a-session-function-warns-and-still-requires-one.md +25 -0
- package/knowledge/decisions/security/bearer-token-hashes-are-unpeppered-sha256.md +18 -0
- package/knowledge/decisions/security/consent-is-read-from-a-cookie-and-a-recognised-denial-counts-as-refusal.md +29 -0
- package/knowledge/decisions/security/cors-defaults-to-any-origin-and-refuses-the-wildcard-with-credentials.md +22 -0
- package/knowledge/decisions/security/credential-backends-share-one-conformance-suite-for-per-user-isolation.md +29 -0
- package/knowledge/decisions/security/each-stage-has-one-virtual-user-key-pair.md +27 -0
- package/knowledge/decisions/security/hmac-verification-fails-closed-on-empty-inputs.md +25 -0
- package/knowledge/decisions/security/index.md +88 -24
- package/knowledge/decisions/security/local-content-refuses-private-buckets-under-the-public-segment.md +21 -0
- package/knowledge/decisions/security/mail-is-allowlisted-before-a-virtual-user-run-sees-it.md +38 -0
- package/knowledge/decisions/security/mcp-resources-tools-and-prompts-take-sessionless-functions.md +18 -0
- package/knowledge/decisions/security/origin-locked-routes-match-the-origin-exactly.md +22 -0
- package/knowledge/decisions/security/oss-auth-works-without-pikku-studio-cloud.md +26 -0
- package/knowledge/decisions/security/outbound-webhooks-use-an-allowlist-when-configured.md +24 -0
- package/knowledge/decisions/security/pikku-carries-virtual-user-scopes-as-data-and-the-app-grants-them.md +35 -0
- package/knowledge/decisions/security/scoped-content-access-is-a-root-folder-with-longest-prefix-grants.md +31 -0
- package/knowledge/decisions/security/services-without-secrets-get-a-throwing-secrets-accessor.md +22 -0
- package/knowledge/decisions/security/shared-guards-refuse-to-mutate-or-shadow-a-system-role.md +34 -0
- package/knowledge/decisions/security/signed-uploads-are-verified-by-the-same-check-as-signed-reads.md +26 -0
- package/knowledge/decisions/security/system-role-scopes-must-be-declared-with-define-scope.md +24 -0
- package/knowledge/decisions/security/system-roles-are-additive-and-immutable.md +16 -0
- package/knowledge/decisions/security/the-channel-rpc-does-not-send-the-callers-session.md +21 -0
- package/knowledge/decisions/security/the-remote-jobs-inbox-fails-closed-without-a-secret.md +24 -0
- package/knowledge/decisions/security/the-remote-rpc-gate-fails-closed-and-binds-the-token-to-its-function.md +34 -0
- package/knowledge/decisions/security/the-workflow-status-stream-rechecks-ownership-on-every-poll.md +16 -0
- package/knowledge/decisions/security/two-credentials-on-one-key-must-share-a-read-mode.md +29 -0
- package/knowledge/decisions/security/verified-bearer-claims-are-pass-through-data-that-core-never-fills.md +23 -0
- package/knowledge/decisions/security/virtual-user-catalogue-excludes-the-machinery-that-runs-virtual-users.md +25 -0
- package/knowledge/decisions/security/{actor-flow-missing-approval-decisions-default-to-denied.md → virtual-user-conversation-missing-approval-decisions-default-to-denied.md} +7 -7
- package/knowledge/decisions/security/virtual-user-run-records-are-gated-behind-a-read-scope.md +18 -0
- package/knowledge/decisions/security/virtual-user-sign-in-does-not-exist-in-production.md +25 -0
- package/knowledge/decisions/security/virtual-user-sign-in-has-one-door.md +42 -0
- package/knowledge/decisions/security/{actor-sign-in-is-proven-by-set-cookie-not-a-non-empty-jar.md → virtual-user-sign-in-is-proven-by-set-cookie-not-a-non-empty-jar.md} +10 -8
- package/knowledge/decisions/security/virtual-user-sign-in-only-works-for-virtual-user-flagged-users.md +29 -0
- package/knowledge/decisions/security/workflow-helper-functions-are-sessionless-by-declaration.md +18 -0
- package/knowledge/decisions/security/{workflow-scenario-sessions-are-isolated-per-actor-and-per-scenario.md → workflow-scenario-sessions-are-isolated-per-virtual-user-and-per-scenario.md} +6 -6
- package/knowledge/decisions/security/workflow-scenario-steps-are-never-network-invocable.md +5 -5
- package/knowledge/decisions/security/{workflow-actor-steps-always-use-the-real-transport.md → workflow-virtual-user-steps-always-use-the-real-transport.md} +11 -11
- package/knowledge/decisions/security/writing-a-virtual-user-schedule-is-gated-by-its-own-scope.md +18 -0
- package/knowledge/index.md +0 -2
- package/knowledge/questions/a-bearer-jwt-with-no-identity-becomes-a-session.md +24 -0
- package/knowledge/questions/dev-quick-login-as-impersonation.md +25 -0
- package/knowledge/questions/does-a-delayed-rpc-run-as-the-session-it-was-scheduled-with.md +26 -0
- package/knowledge/questions/does-a-user-id-with-a-colon-read-another-users-credentials.md +27 -0
- package/knowledge/questions/does-a-wirecli-command-need-a-session-by-default.md +26 -0
- package/knowledge/questions/does-an-adversarial-in-scope-success-count-as-a-finding.md +26 -0
- package/knowledge/questions/does-approve-step-need-the-run-owners-session-or-the-gates-approvers.md +34 -0
- package/knowledge/questions/does-cors-origin-true-reflect-any-origin-with-credentials.md +20 -0
- package/knowledge/questions/does-skip-user-session-do-anything-on-a-channel.md +24 -0
- package/knowledge/questions/does-the-store-session-match-the-other-two.md +19 -0
- package/knowledge/questions/index.md +25 -2
- package/knowledge/questions/is-the-agent-score-worker-reachable-without-a-session.md +21 -0
- package/knowledge/questions/live-scheduled-tests-may-write.md +19 -0
- package/knowledge/questions/provision-virtual-users-has-no-caller-in-this-repo.md +23 -0
- package/knowledge/questions/remote-secret-refuses-requests-without-a-remote-token-on-every-path.md +32 -0
- package/knowledge/questions/should-a-middleware-set-session-persist-to-the-store.md +25 -0
- package/knowledge/questions/should-a-scoped-secret-view-throw-from-has-secret-for-a-disallowed-key.md +23 -0
- package/knowledge/questions/should-a-wrong-bearer-token-be-anonymous-or-refused.md +35 -0
- package/knowledge/questions/should-an-empty-webhook-signing-secret-send-unsigned.md +23 -0
- package/knowledge/questions/should-http-virtual-user-sessions-reset-between-scenarios-in-one-run.md +27 -0
- package/knowledge/questions/should-the-cookie-jar-scope-cookies-to-the-api-origin.md +26 -0
- package/knowledge/questions/should-voice-input-fetch-audio-urls-without-a-host-allowlist.md +20 -0
- package/knowledge/questions/staging-impersonation-rules.md +27 -0
- package/knowledge/questions/virtual-user-keys-on-cloud-stages-are-pikku-studios-to-build.md +34 -0
- package/knowledge/questions/virtual-user-statement-replay-is-per-process.md +20 -0
- package/knowledge/questions/what-an-impersonated-session-carries.md +22 -0
- package/package.json +3 -3
- package/src/public-surface.json +43 -47
- package/dist/services/persona-actor-secret.d.ts +0 -38
- package/dist/services/persona-actor-secret.js +0 -39
- package/dist/services/persona-sign-in.d.ts +0 -117
- package/dist/services/persona-sign-in.js +0 -154
- package/dist/wirings/actor-flow/index.d.ts +0 -2
- package/dist/wirings/actor-flow/run-conversation.d.ts +0 -36
- package/dist/wirings/persona/define-personas.d.ts +0 -32
- package/dist/wirings/persona/index.d.ts +0 -23
- package/dist/wirings/persona/index.js +0 -19
- package/knowledge/decisions/internals/actor-flow-conversations-seed-a-hidden-kickoff-message.md +0 -23
- package/knowledge/decisions/internals/actor-flow-verdicts-are-llm-self-evaluations.md +0 -25
- package/knowledge/decisions/internals/validate-checks-personas-through-a-shared-module.md +0 -43
- package/knowledge/decisions/security/actor-sign-in-only-works-for-actor-flagged-users.md +0 -31
- package/knowledge/decisions/security/an-actor-credential-is-derived-per-persona.md +0 -42
- package/knowledge/decisions/security/pikku-carries-actor-scopes-as-data-and-the-app-grants-them.md +0 -26
- /package/dist/wirings/{actor-flow/actor-flow.types.js → virtual-user/conversation/conversation.types.js} +0 -0
- /package/dist/wirings/{actor-flow → virtual-user/conversation}/index.js +0 -0
- /package/dist/wirings/{persona/persona.types.js → virtual-user/virtual-user-declaration.types.js} +0 -0
- /package/dist/wirings/{persona/persona-mailbox.js → virtual-user/virtual-user-mailbox.js} +0 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: decision
|
|
3
|
+
title: A secret may only be sent to the hosts its declaration names
|
|
4
|
+
description: assertSecretAllowedForHost refuses a host outside a secret's allowedHosts, and refuses any host at all for a secret with no allowedHosts when the project requires them
|
|
5
|
+
tags: core, secrets, egress
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# A secret may only be sent to the hosts its declaration names
|
|
9
|
+
|
|
10
|
+
`packages/core/src/services/secret-host-binding.ts` holds the check
|
|
11
|
+
`assertSecretAllowedForHost`. It looks up the secret's definition by id and reads
|
|
12
|
+
its `allowedHosts` (`secret-host-binding.ts:36-39`).
|
|
13
|
+
|
|
14
|
+
- No `allowedHosts`, and `requireAllowedHosts` is set: throw
|
|
15
|
+
(`secret-host-binding.ts:41-44`).
|
|
16
|
+
- No `allowedHosts`, and the flag is not set: allowed (`secret-host-binding.ts:45`).
|
|
17
|
+
- `allowedHosts` present: the host must match at least one entry, or throw
|
|
18
|
+
(`secret-host-binding.ts:48-50`).
|
|
19
|
+
|
|
20
|
+
Matching (`secret-host-binding.ts:18-26`) is case-insensitive. An entry is either
|
|
21
|
+
an exact host or a single leading `*.` wildcard. The wildcard requires a
|
|
22
|
+
non-empty label before the dot (`secret-host-binding.ts:23`), so `*.example.com`
|
|
23
|
+
does not match `example.com` itself.
|
|
24
|
+
|
|
25
|
+
The error message tells the author to add the host to `allowedHosts` on the
|
|
26
|
+
`defineSecret` if the send is intended (`secret-host-binding.ts:11`).
|
|
27
|
+
|
|
28
|
+
**What this rules out:** sending a secret to a host its declaration does not
|
|
29
|
+
name, and a bare-domain match against a wildcard entry.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: decision
|
|
3
|
+
title: A scenario step can name an identity no virtual user holds, through per-call headers
|
|
4
|
+
description: ScenarioInvokeOptions.headers lets a step send an impersonation or shim header on one call, and a virtual-user run never gets that option
|
|
5
|
+
tags: core, virtual-users, scenario
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# A scenario step can name an identity no virtual user holds, through per-call headers
|
|
9
|
+
|
|
10
|
+
`ScenarioInvokeOptions` in `packages/core/src/services/virtual-users-service.ts` says
|
|
11
|
+
its headers are how "a step expresses an identity no declared virtual user can — an
|
|
12
|
+
impersonation header, or one of the header-shim principals a credential scenario
|
|
13
|
+
invents." `HttpVirtualUser.invokeRaw` passes them to `postRpc`
|
|
14
|
+
(`packages/core/src/services/http-virtual-users.ts`), so they go out on that one call
|
|
15
|
+
alongside the virtual user's session.
|
|
16
|
+
|
|
17
|
+
The option belongs to the scenario author, who writes the steps and is trusted to
|
|
18
|
+
say which identity a check needs. A virtual-user run does not get it. Its target in
|
|
19
|
+
`packages/core/src/wirings/virtual-user/virtual-user-target.ts` calls
|
|
20
|
+
`virtualUser.invokeRaw(rpcName, args)` with no options, so an adversarial run cannot
|
|
21
|
+
add a header to reach an identity its virtual user does not hold.
|
|
22
|
+
|
|
23
|
+
**What this rules out:** giving a virtual-user run a per-call header, and removing the
|
|
24
|
+
option from scenario steps on the grounds that a declared virtual user should be the
|
|
25
|
+
only identity a step can use.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: decision
|
|
3
|
+
title: Stopping an agent run by its id skips the ownership check; interruptAgent is the checked path
|
|
4
|
+
description: signalRunInterrupt is exported and performs no authorisation, while interruptAgent asserts the run's resource owner before it signals
|
|
5
|
+
tags: core, agent, security
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Stopping an agent run by its id skips the ownership check; interruptAgent is the checked path
|
|
9
|
+
|
|
10
|
+
`signalRunInterrupt` in `packages/core/src/wirings/agent/agent-interrupt.ts:227-236`
|
|
11
|
+
looks up the run and aborts it, with no check on who is asking. Its doc comment at
|
|
12
|
+
`:215-220` says so directly: anyone holding a `runId` can stop the run, and callers
|
|
13
|
+
should use `interruptAgent` unless the run's ownership is already established.
|
|
14
|
+
|
|
15
|
+
It is exported from the agent entry point (`packages/core/src/wirings/agent/index.ts:33`).
|
|
16
|
+
|
|
17
|
+
`interruptAgent` in `packages/core/src/wirings/agent/agent-stream.ts:1061-1086`
|
|
18
|
+
loads the run (`:1070`), asserts the resource owner with `assertResourceOwner`
|
|
19
|
+
(`:1074-1082`), and only then calls `signalRunInterrupt` (`:1084`). The RPC wrapper
|
|
20
|
+
calls `interruptAgent` (`packages/core/src/wirings/agent/agent-rpc.ts:98`).
|
|
21
|
+
|
|
22
|
+
**What this rules out:** exposing `signalRunInterrupt` to a caller who has not
|
|
23
|
+
resolved the run's owner, and reaching a run through its id alone on a request
|
|
24
|
+
path.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: decision
|
|
3
|
+
title: A vault secret redacts when serialized and throws when coerced to a string
|
|
4
|
+
description: SecretValue serializes to a redacted marker so logs stay honest without crashing, but string coercion throws, and reveal() is the only way to read the value
|
|
5
|
+
tags: core, secrets, classification
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# A vault secret redacts when serialized and throws when coerced to a string
|
|
9
|
+
|
|
10
|
+
`packages/core/src/classification/secret-value.ts` wraps a vault value in
|
|
11
|
+
`SecretValue`. The raw value sits in a private field (`secret-value.ts:37`) and
|
|
12
|
+
is returned only by `reveal()` (`secret-value.ts:43-45`).
|
|
13
|
+
|
|
14
|
+
Two paths out of the wrapper behave differently on purpose:
|
|
15
|
+
|
|
16
|
+
- Structured serialization is redacted. `toJSON` and the Node inspect hook both
|
|
17
|
+
return `REDACTED`, which is `[secret]` (`secret-value.ts:5`, `47-53`). The doc
|
|
18
|
+
comment says an audit or log write "must stay honest about the field without
|
|
19
|
+
crashing the request" (`secret-value.ts:29-30`).
|
|
20
|
+
- String coercion throws. `toString` and `Symbol.toPrimitive` both throw
|
|
21
|
+
`SecretCoercionError` (`secret-value.ts:55-61`). The comment says "a template
|
|
22
|
+
literal or concatenation is always a leak" (`secret-value.ts:31`).
|
|
23
|
+
|
|
24
|
+
The `Safe<T>` type (`secret-value.ts:91-106`) rejects a `SecretValue` anywhere
|
|
25
|
+
in a type, for sinks typed `any`, `unknown` or a free generic.
|
|
26
|
+
|
|
27
|
+
**What this rules out:** reading a secret without calling `reveal()`, putting a
|
|
28
|
+
secret into a template literal or string concatenation, and a logger that
|
|
29
|
+
crashes on a secret field instead of redacting it.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: decision
|
|
3
|
+
title: Virtual users are offered calls by declared scope, and approval-gated calls are denied by default
|
|
4
|
+
description: The catalogue type filters by the declared scopes of each RPC rather than by permission functions, and the catalogue filter drops approval-required entries unless the caller opts in
|
|
5
|
+
tags: core, virtual-users, scopes, approval
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Virtual users are offered calls by declared scope, and approval-gated calls are denied by default
|
|
9
|
+
|
|
10
|
+
`packages/core/src/wirings/virtual-user/virtual-user.types.ts` defines the
|
|
11
|
+
catalogue entry a virtual user is offered.
|
|
12
|
+
|
|
13
|
+
The `scopes` field lists the scopes the session must hold to call the RPC, all of
|
|
14
|
+
them (`virtual-user.types.ts:92-99`). The doc comment explains why scopes and not
|
|
15
|
+
permission functions: a scope is "declared and checkable", while "a permission is
|
|
16
|
+
arbitrary code whose verdict nobody can predict without running it"
|
|
17
|
+
(`virtual-user.types.ts:95-97`).
|
|
18
|
+
|
|
19
|
+
`approvalRequired` is documented as "denied by default" (`virtual-user.types.ts:89-90`).
|
|
20
|
+
The filter in `virtual-user-catalogue.ts` enforces this. It drops an entry when
|
|
21
|
+
`approvalRequired` is set and the caller has not passed `allowApprovalRequired`
|
|
22
|
+
(`virtual-user-catalogue.ts:113`), and that flag defaults to `false`
|
|
23
|
+
(`virtual-user-catalogue.ts:95`).
|
|
24
|
+
|
|
25
|
+
The same filter drops any entry the session's scopes do not cover
|
|
26
|
+
(`virtual-user-catalogue.ts:115-117`).
|
|
27
|
+
|
|
28
|
+
**What this rules out:** a virtual user calling an approval-gated RPC unless the
|
|
29
|
+
run explicitly allows it, and a permission function being used to decide what a
|
|
30
|
+
virtual user can see.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: decision
|
|
3
|
+
title: A virtual user's sign-in is refused unless the environment is known and allowed
|
|
4
|
+
description: Virtual user provisioning, sync and sign-in refuse when PIKKU_ENV is unresolved, unknown, production without the accountable disposition, or outside the virtual user's environments, and production environments are omitted by default
|
|
5
|
+
tags: core, virtual-users, environments, security
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# A virtual user's sign-in is refused unless the environment is known and allowed
|
|
9
|
+
|
|
10
|
+
`virtualUserEnvironmentRefusal`
|
|
11
|
+
(`packages/core/src/wirings/virtual-user/virtual-user-environments.ts`) fails
|
|
12
|
+
closed when:
|
|
13
|
+
|
|
14
|
+
- `PIKKU_ENV` is unresolved;
|
|
15
|
+
- `PIKKU_ENV` names an environment the config does not know;
|
|
16
|
+
- the environment is production and the virtual user's disposition is not
|
|
17
|
+
`PRODUCTION_DISPOSITION` (`accountable`);
|
|
18
|
+
- the environment is not one the virtual user may target.
|
|
19
|
+
|
|
20
|
+
`resolveVirtualUserEnvironments` leaves production environments out when a
|
|
21
|
+
virtual user declares no `environments`. A virtual user reaches production only
|
|
22
|
+
when someone has named it there. The build-time half is
|
|
23
|
+
`virtualUserEnvironmentErrors`, which the doc comment says the inspector runs;
|
|
24
|
+
the run-time half is re-checked because "the run check does not trust which
|
|
25
|
+
artifact got deployed".
|
|
26
|
+
|
|
27
|
+
The refusal is applied by `pikku virtual-user sync`
|
|
28
|
+
(`packages/cli/src/functions/commands/virtual-user-sync.ts`), by the
|
|
29
|
+
`pikku virtual-user` command (`packages/cli/src/functions/commands/virtual-user.ts`)
|
|
30
|
+
and by `provisionVirtualUsers`
|
|
31
|
+
(`packages/services/better-auth/src/provision-virtual-users.ts`).
|
|
32
|
+
|
|
33
|
+
**What this rules out:** provisioning or signing in virtual users when the target
|
|
34
|
+
environment is ambiguous, and a production stage that carries virtual users it was
|
|
35
|
+
never meant to have.
|
|
36
|
+
|
|
37
|
+
Runs are stricter: [A virtual user never runs against production](a-virtual-user-never-runs-against-production.md)
|
|
38
|
+
refuses even the `accountable` disposition.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: decision
|
|
3
|
+
title: A virtual user never runs against production, whatever its disposition
|
|
4
|
+
description: startVirtualUserRun and an enabling writeVirtualUserSchedule both refuse with a 403 in production, including for the accountable disposition; an unresolved environment counts as production
|
|
5
|
+
tags: core, virtual-users, environments, security
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# A virtual user never runs against production, whatever its disposition
|
|
9
|
+
|
|
10
|
+
`refuseInProduction` (`packages/core/src/wirings/virtual-user/virtual-user-scaffold.ts:192`) throws a `ForbiddenError`, "Virtual users do not run against production", whenever the process is a production run. It has no disposition exception: `accountable`, which the virtual user environment check still accepts at sign-in in production, is refused here too.
|
|
11
|
+
|
|
12
|
+
It is called from two places:
|
|
13
|
+
|
|
14
|
+
- `startVirtualUserRun` (`virtual-user-scaffold.ts:232`), before the run record is created, so a refused run leaves no row.
|
|
15
|
+
- `writeVirtualUserSchedule` (`virtual-user-scaffold.ts:410`), only when the row is or becomes enabled. Disabling a schedule, or editing one that is off, is still allowed in production so a leftover schedule can be stopped. A scheduled tick dispatches `runVirtualUser` through `rpc.invoke` (`packages/cli/src/functions/wirings/virtual-user/serialize-virtual-user-functions.ts:563-566`), and that function calls `startVirtualUserRun` (same file, line 337), so a due tick is refused as well.
|
|
16
|
+
|
|
17
|
+
"Production" is decided by `isProductionRun` (`virtual-user-scaffold.ts:179`). The configured `PIKKU_ENV` environment wins when the project configures environments, and an unresolved environment counts as production. `NODE_ENV` decides only for a project that configures none.
|
|
18
|
+
|
|
19
|
+
The sign-in environment rule is separate and unchanged. See [A virtual user's sign-in is refused unless the environment is known and allowed](a-virtual-user-environment-fails-closed.md): signing an `accountable` virtual user in against production is still allowed there, and starting a run is not.
|
|
20
|
+
|
|
21
|
+
The tests are in `virtual-user-scaffold.test.ts`: "refuses every disposition in production", "the accountable disposition is refused in production too", "an accountable cadence is refused in production too" and "disabling or editing an off cadence is allowed in production".
|
|
22
|
+
|
|
23
|
+
**What this rules out:** a virtual user, probing or not, acting on real customers' data, and an enabled schedule that can only ever fail to start in production.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: decision
|
|
3
|
+
title: A virtual user reads its roles from the app's own RPC before better-auth's user.role
|
|
4
|
+
description: sessionRoles asks getMyScopes first, then get-session, and returns null rather than an empty list when the stage reports no roles
|
|
5
|
+
tags: core, virtual-users, auth
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# A virtual user reads its roles from the app's own RPC before better-auth's user.role
|
|
9
|
+
|
|
10
|
+
`HttpVirtualUser.sessionRoles` (`packages/core/src/services/http-virtual-users.ts`) answers
|
|
11
|
+
"what roles does this session hold" in a fixed order. First the app's exposed RPC,
|
|
12
|
+
which defaults to `getMyScopes` and can be renamed or switched off with `rolesRpc`.
|
|
13
|
+
Only then better-auth's `get-session`, where its admin plugin puts `role` on the
|
|
14
|
+
user.
|
|
15
|
+
|
|
16
|
+
The `rolesRpc` doc comment gives the reason. In an app that authorizes on scopes,
|
|
17
|
+
`user.role` is "a projection rather than the model": it exists so better-auth's
|
|
18
|
+
admin endpoints have something to read, and it may be absent. A virtual user verified
|
|
19
|
+
against it is verified against a copy.
|
|
20
|
+
|
|
21
|
+
Two answers are kept apart. A stage that answers neither returns `null`, which
|
|
22
|
+
means "this stage does not report roles". A session that carries a user with no
|
|
23
|
+
role field returns `[]`, which means "this person has none". The doc comment says
|
|
24
|
+
the two call for opposite responses from the caller.
|
|
25
|
+
|
|
26
|
+
**What this rules out:** making better-auth's `user.role` the first source of a
|
|
27
|
+
virtual user's roles, and reporting `[]` for a stage that could not answer at all.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: decision
|
|
3
|
+
title: A virtual-user run treats an unresolved PIKKU_ENV as production
|
|
4
|
+
description: The virtual-user scaffold decides "production" from the configured PIKKU_ENV environment, counts an unresolved one as production, and falls back to NODE_ENV only when no environments are configured
|
|
5
|
+
tags: core, virtual-users, environments, security
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# A virtual-user run treats an unresolved PIKKU_ENV as production
|
|
9
|
+
|
|
10
|
+
`startVirtualUserRun` calls `refuseInProduction` before it creates a run record
|
|
11
|
+
(`packages/core/src/wirings/virtual-user/virtual-user-scaffold.ts:232`). `PIKKU_ENV`
|
|
12
|
+
is read as the default for `environment` (`virtual-user-scaffold.ts:217`). The
|
|
13
|
+
schedule writer runs the same check when the schedule is or becomes enabled
|
|
14
|
+
(`virtual-user-scaffold.ts:399`, `:409-411`).
|
|
15
|
+
|
|
16
|
+
`refuseInProduction` throws a `ForbiddenError` whenever `isProductionRun` is true,
|
|
17
|
+
whatever the disposition (`virtual-user-scaffold.ts:192-204`). That rule is
|
|
18
|
+
[a virtual user never runs against production](a-virtual-user-never-runs-against-production.md);
|
|
19
|
+
this note is about how "production" is decided.
|
|
20
|
+
|
|
21
|
+
`isProductionRun` decides it as follows (`virtual-user-scaffold.ts:179-188`):
|
|
22
|
+
|
|
23
|
+
- With no environments configured, it is `NODE_ENV === 'production'` (`:184-186`).
|
|
24
|
+
- With environments configured and `PIKKU_ENV` unset, it is true (`:187`,
|
|
25
|
+
`environment ? ... : true`).
|
|
26
|
+
- With `PIKKU_ENV` set, it is that environment's `production` flag (`:187`). An
|
|
27
|
+
unknown name evaluates to false in this check. `virtualUserEnvironmentRefusal`
|
|
28
|
+
refuses unknown names at sign-in instead
|
|
29
|
+
(`packages/core/src/wirings/virtual-user/virtual-user-environments.ts:113-119`).
|
|
30
|
+
|
|
31
|
+
The comment gives the reasons. The configured environment wins over `NODE_ENV`
|
|
32
|
+
because a production mirror runs `NODE_ENV=production` too
|
|
33
|
+
(`virtual-user-scaffold.ts:166-172`). An unresolved environment counts as
|
|
34
|
+
production because nobody can vouch for its data (`:174-177`).
|
|
35
|
+
|
|
36
|
+
See also [a virtual user's sign-in is refused unless the environment is known and allowed](a-virtual-user-environment-fails-closed.md),
|
|
37
|
+
which covers the sign-in refusal in `virtual-user-environments.ts`.
|
|
38
|
+
|
|
39
|
+
**What this rules out:** starting a run when the environment cannot be named, and
|
|
40
|
+
keying the scaffold's refusal on `NODE_ENV` in a project that has configured
|
|
41
|
+
environments.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: decision
|
|
3
|
+
title: A virtual user's scopes are expanded from its roles once, at run preparation
|
|
4
|
+
description: A virtual user's scope set is computed from its declared roles against the defineSystemRole definitions when the run is prepared, and that set is the oracle for scope drift
|
|
5
|
+
tags: core, virtual-user, auth
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# A virtual user's scopes are expanded from its roles once, at run preparation
|
|
9
|
+
|
|
10
|
+
`virtualUserScopes` in `packages/core/src/wirings/virtual-user/prepare-virtual-user-run.ts`
|
|
11
|
+
expands a virtual user's `roles` into scopes through the role definitions the seed
|
|
12
|
+
grants from. Its doc comment says roles are the only thing a virtual user declares, and
|
|
13
|
+
that the expansion happens "once, here, against the same `defineSystemRole`
|
|
14
|
+
definitions the seed grants from".
|
|
15
|
+
|
|
16
|
+
The result feeds two things in `run-virtual-user.ts`. It narrows the catalogue
|
|
17
|
+
for a non-adversarial user, and it is the oracle: a call that succeeds on an
|
|
18
|
+
endpoint whose `scopes` the virtual user does not hold is recorded as
|
|
19
|
+
`unexpected-success`, which the code calls authorization drift. Using the static
|
|
20
|
+
expansion as the truth is what makes that finding mean something, because the
|
|
21
|
+
run never asks the target what the virtual user may do.
|
|
22
|
+
|
|
23
|
+
**What this rules out:** building a virtual user's catalogue or judging its
|
|
24
|
+
successes from a scope list fetched from the live session, which would make the
|
|
25
|
+
oracle agree with whatever the target currently grants.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: decision
|
|
3
|
+
title: A virtual user whose granted roles drift from its declaration refuses to run
|
|
4
|
+
description: verifyVirtualUserRoles compares declared and granted roles in both directions before a run, and roleMismatchMessage produces the refusal text; a provider login is never runnable
|
|
5
|
+
tags: core, virtual-users, roles, drift
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# A virtual user whose granted roles drift from its declaration refuses to run
|
|
9
|
+
|
|
10
|
+
`packages/core/src/wirings/virtual-user/validate-virtual-users.ts` has the role check that
|
|
11
|
+
runs before a virtual user's first step. `verifyVirtualUserRoles` computes what is missing
|
|
12
|
+
and what is extra, in both directions (`validate-virtual-users.ts:94-111`). `roleMismatchMessage`
|
|
13
|
+
returns `null` when they match, and otherwise a message that ends with "Refusing
|
|
14
|
+
to run" (`validate-virtual-users.ts:114-133`, the text at `validate-virtual-users.ts:131`).
|
|
15
|
+
|
|
16
|
+
The doc comment says the declaration is authoritative, but a run may target a
|
|
17
|
+
stage that the generated seed never touched (`validate-virtual-users.ts:84-87`). It
|
|
18
|
+
gives a reason for each direction: an under-granted virtual user reports seed drift
|
|
19
|
+
as an authorization bug, and an over-granted one "silently stops testing the
|
|
20
|
+
boundary it was written to test" (`validate-virtual-users.ts:89-92`).
|
|
21
|
+
|
|
22
|
+
Nothing in `packages/core/src` calls `roleMismatchMessage`. The caller that
|
|
23
|
+
stops the run is the `pikku virtual-user` command
|
|
24
|
+
(`packages/cli/src/functions/commands/virtual-user.ts`), so this file only
|
|
25
|
+
supplies the check and the message.
|
|
26
|
+
|
|
27
|
+
`isRunnableVirtualUser` (`validate-virtual-users.ts:19-27`) returns false for a virtual user
|
|
28
|
+
with `runnable: false`, and for any virtual user with an account provider, because
|
|
29
|
+
driving a provider consent screen "needs a human" (`validate-virtual-users.ts:10-12`).
|
|
30
|
+
|
|
31
|
+
**What this rules out:** running a virtual user whose roles no longer match what
|
|
32
|
+
it declared, and a provider-login virtual user being driven by a run.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: decision
|
|
3
|
+
title: Addon screen scopes hide a screen and do not enforce access
|
|
4
|
+
description: The scopes on a defineScreens entry state what a viewer must hold to see the screen; the addon's own functions are the access check
|
|
5
|
+
tags: core, addons, screens, security
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Addon screen scopes hide a screen and do not enforce access
|
|
9
|
+
|
|
10
|
+
`packages/core/src/wirings/addon/define-screens.ts:7-8` documents `scopes` on an
|
|
11
|
+
addon screen as required of the viewer to see it, and says the addon's functions
|
|
12
|
+
must enforce the same scopes, because hiding a screen is not access control.
|
|
13
|
+
|
|
14
|
+
The field is read statically by `pikku all`, which is why `scopes` must be a
|
|
15
|
+
literal (`define-screens.ts:32`). Within `packages/core/src`, no code reads a
|
|
16
|
+
screen's `scopes` at runtime; `wire-addon.ts:47-48` mounts the screens under
|
|
17
|
+
`/addons/<name>` and is the only other place they are referenced.
|
|
18
|
+
|
|
19
|
+
**What this rules out:** relying on a screen's scopes to keep data from a viewer,
|
|
20
|
+
and an addon shipping a function that trusts the screen to have been hidden.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: decision
|
|
3
|
+
title: An addon header can only narrow the session, and an unknown addon is refused
|
|
4
|
+
description: addon-scope-cap intersects the session's scopes with the named addon's role, so naming an addon never widens access, and a name the host does not know is a 403
|
|
5
|
+
tags: core, middleware, scopes, addons
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# An addon header can only narrow the session, and an unknown addon is refused
|
|
9
|
+
|
|
10
|
+
`packages/core/src/middleware/addon-scope-cap.ts` reads the `x-pikku-addon`
|
|
11
|
+
header (`addon-scope-cap.ts:8`, `31`). When a session is present, it resolves
|
|
12
|
+
the addon's role and replaces the session's scopes with
|
|
13
|
+
`intersectScopes(session.scopes, role)` (`addon-scope-cap.ts:37-45`).
|
|
14
|
+
|
|
15
|
+
The doc comment says the header "is a selector, not a credential: naming an
|
|
16
|
+
addon can only reduce a session" (`addon-scope-cap.ts:16-17`). An addon the host
|
|
17
|
+
does not know throws `ForbiddenError` (`addon-scope-cap.ts:38-40`), because the
|
|
18
|
+
comment says ignoring it "would let a misspelt name run uncapped"
|
|
19
|
+
(`addon-scope-cap.ts:18-19`).
|
|
20
|
+
|
|
21
|
+
With no header, or no session, the middleware calls `next()` and changes
|
|
22
|
+
nothing (`addon-scope-cap.ts:33-34`). The comment says to register it after the
|
|
23
|
+
session middleware (`addon-scope-cap.ts:21-22`).
|
|
24
|
+
|
|
25
|
+
**What this rules out:** using the header to gain scopes, and treating an
|
|
26
|
+
unrecognised addon name as "no cap".
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: decision
|
|
3
|
+
title: An addon instance reads only the secrets and credentials it is named
|
|
4
|
+
description: An addon's secret and credential views are its declared names plus the consuming app's grants and override keys; the global flags opt out of scoping
|
|
5
|
+
tags: core, addons, secrets, credentials
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# An addon instance reads only the secrets and credentials it is named
|
|
9
|
+
|
|
10
|
+
The consuming app decides which secrets and credentials an addon instance may
|
|
11
|
+
read. The grant fields are declared on the wiring as "set by the consuming app"
|
|
12
|
+
(`packages/core/src/wirings/rpc/rpc-types.ts:73-76`) and carried through
|
|
13
|
+
`wireAddon` (`packages/core/src/wirings/addon/wire-addon.ts:123-126`) into the
|
|
14
|
+
instance (`packages/core/src/wirings/addon/addon-runner.ts:20-22`).
|
|
15
|
+
|
|
16
|
+
The allowed set is built by `allowedNames` in `addon-runner.ts:40-49`: the names
|
|
17
|
+
the package declared, plus the grants, plus the keys of the overrides. An
|
|
18
|
+
override key therefore grants the name; the override value is the key the
|
|
19
|
+
lookup is pointed at. `wire-addon.ts:71` says the grant check runs before
|
|
20
|
+
`secretOverrides` rename a name.
|
|
21
|
+
|
|
22
|
+
Scoping is applied when the instance's services are built
|
|
23
|
+
(`addon-runner.ts:200-227`). Unless `globalSecrets` is set, the secret service
|
|
24
|
+
is wrapped in `ScopedSecretService`; unless `globalCredentials` is set, the
|
|
25
|
+
credential service is wrapped in `ScopedCredentialService`. The global options
|
|
26
|
+
hand over the unscoped service, and their value is a reason string
|
|
27
|
+
(`wire-addon.ts:75-82`). The comments say the reason is recorded in the deploy
|
|
28
|
+
manifest; that recording was not traced here.
|
|
29
|
+
|
|
30
|
+
**What this rules out:** an addon reading a name neither it declared nor the
|
|
31
|
+
host named, including through an override value; and an unscoped service
|
|
32
|
+
reaching an addon without the consuming app setting a `globalSecrets` or
|
|
33
|
+
`globalCredentials` reason.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: decision
|
|
3
|
+
title: An addon may reach only itself and the addons it declares in uses
|
|
4
|
+
description: An RPC call that arrives tagged with an addon header is refused unless the target is the caller or one of the caller's declared uses, and an unknown addon name is refused outright
|
|
5
|
+
tags: core, addons, rpc, scopes
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# An addon may reach only itself and the addons it declares in uses
|
|
9
|
+
|
|
10
|
+
`addonMayReach` in `packages/core/src/wirings/addon/addon-reach.ts` takes the caller addon and the target function name (`addon-reach.ts:6`). It takes the target from the part of the name before `:` (`addon-reach.ts:7`). It returns `false` for an empty target (`:8`), `true` when the target is the caller (`:9`), and otherwise `true` only when the caller's declared `uses` includes the target (`:10-11`).
|
|
11
|
+
|
|
12
|
+
`addonCallerOf` reads the addon header from the HTTP request (`addon-reach.ts:14-16`). A name that is not a registered addon is refused with `ForbiddenError` (`addon-reach.ts:17-18`).
|
|
13
|
+
|
|
14
|
+
`ContextAwareRPCService.rpcExposed` applies the check before resolving the function. A caller that fails `addonMayReach` gets `AddonNotUsedError` (`packages/core/src/wirings/rpc/rpc-runner.ts:189-192`).
|
|
15
|
+
|
|
16
|
+
**What this rules out:** an addon calling into another addon it does not declare in `uses`, and an addon header naming an unregistered addon being treated as anonymous rather than refused.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: decision
|
|
3
|
+
title: An API key is JWT-decoded into the session, from a header or a query parameter
|
|
4
|
+
description: authAPIKey reads x-api-key or the apiKey query parameter, decodes it as a JWT, and sets the session only when no session exists yet
|
|
5
|
+
tags: core, auth, middleware, api-keys
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# An API key is JWT-decoded into the session, from a header or a query parameter
|
|
9
|
+
|
|
10
|
+
`packages/core/src/middleware/auth-apikey.ts` takes an API key from the
|
|
11
|
+
`x-api-key` header, the `apiKey` query parameter, or both, depending on the
|
|
12
|
+
`source` option (`auth-apikey.ts:11-13`, `21-27`). The header is tried first;
|
|
13
|
+
the query is read only when no header value was found (`auth-apikey.ts:25`).
|
|
14
|
+
|
|
15
|
+
The key is decoded with `jwtService.decode` and, if that returns a value, passed
|
|
16
|
+
to `setSession` (`auth-apikey.ts:29-33`). The middleware does nothing when a
|
|
17
|
+
session already exists (`auth-apikey.ts:17`), which the doc comment describes as
|
|
18
|
+
leaving "an existing session alone, so it composes with other auth middleware"
|
|
19
|
+
(`auth-apikey.ts:6-7`).
|
|
20
|
+
|
|
21
|
+
Nothing is decoded when no `jwtService` is wired (`auth-apikey.ts:29`).
|
|
22
|
+
|
|
23
|
+
**What this rules out:** an API key overriding a session that is already set,
|
|
24
|
+
and a key being accepted without a JWT service to verify it. The code also
|
|
25
|
+
accepts a key from the query string (`auth-apikey.ts:26`), so keys can appear in
|
|
26
|
+
URLs when `source` includes `query`.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: decision
|
|
3
|
+
title: An external virtual user caller presents one signed statement
|
|
4
|
+
description: Callers outside the app sign an EdDSA statement naming the virtual user; the app verifies it against a public key and accepts each statement once
|
|
5
|
+
tags: services
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# An external virtual user caller presents one signed statement
|
|
9
|
+
|
|
10
|
+
The statement is an EdDSA (Ed25519) JWT: `sub` is the virtual user id, `aud` is the
|
|
11
|
+
app's origin, `iat`, `exp` a few minutes out (default 180 seconds, ceiling 300), and
|
|
12
|
+
`jti`, a one-time id. The CLI signs it with `node:crypto`; the app verifies it with
|
|
13
|
+
`JoseVirtualUserStatementVerifier` (`packages/services/jose`), `jwtVerify` pinned to
|
|
14
|
+
`algorithms: ['EdDSA']`, against a public key. `POST /sign-in/virtual-user` takes
|
|
15
|
+
`{ statement }`, maps `sub` to a declared virtual user, and signs it in.
|
|
16
|
+
|
|
17
|
+
It is rejected for a wrong algorithm (`none` and HS256 included), a wrong key, a wrong
|
|
18
|
+
audience, expiry, a lifetime beyond the ceiling, a missing `sub` or `jti`, and a `jti`
|
|
19
|
+
already accepted. The replay store is a per-process map of `jti` to expiry, purged on
|
|
20
|
+
each call, so it never outgrows the last few minutes of statements.
|
|
21
|
+
|
|
22
|
+
One statement replaces three older credentials — a secret derived per virtual user from
|
|
23
|
+
a root, an operator token with impersonation, and an app-wired sign-in hook — because
|
|
24
|
+
each of them put something the app could mint a session from in the hands of a caller,
|
|
25
|
+
or in the app's own configuration. A public key lets the app check a statement and
|
|
26
|
+
nothing else; capturing the app's environment yields no ability to sign one.
|
|
27
|
+
|
|
28
|
+
The **id-only dev switcher** (`/sign-in/switch-virtual-user`) is unchanged under
|
|
29
|
+
`pikku dev`. Anywhere else it also needs a `statement` naming the same id, so no
|
|
30
|
+
deployed stage has a credential-free way in.
|
|
31
|
+
|
|
32
|
+
The statement endpoint needs the declared virtual users to map an id to an address, so
|
|
33
|
+
it is registered only when the app passes `virtualUserSignIn.virtualUsers`, which the
|
|
34
|
+
generated `virtualUserList` supplies.
|
|
35
|
+
|
|
36
|
+
**What this rules out:** the app holding a private key, a statement without an
|
|
37
|
+
expiry or one-time id, an algorithm negotiated by the token, and a switcher that works
|
|
38
|
+
on a stage without proof.
|
package/knowledge/decisions/security/an-inbound-webhook-source-dispatches-only-verified-requests.md
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: decision
|
|
3
|
+
title: An inbound webhook source dispatches only requests verified against its credential
|
|
4
|
+
description: A webhook source with verify refuses a body-carrying request that fails its check, fails closed on an empty secret, and does not dispatch events from a body-less request
|
|
5
|
+
tags: core, webhooks, trigger, security
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# An inbound webhook source dispatches only requests verified against its credential
|
|
9
|
+
|
|
10
|
+
`verifyRequest` in `packages/core/src/wirings/trigger/webhook-source-runner.ts:173-200`
|
|
11
|
+
decides whether a request counts as signed. A source with no `verify` returns
|
|
12
|
+
true (`:178`). Otherwise the secret is read from the credential named by
|
|
13
|
+
`source.credential`, or from `webhookSecretCredentialName` (`:180-182`).
|
|
14
|
+
|
|
15
|
+
When the secret is missing or empty, a body-less request is treated as
|
|
16
|
+
unverified (`:184`), and a request with a body throws `UnauthorizedError`
|
|
17
|
+
(`:185-187`). `signedWith` also fails closed on an empty secret (`:127-128`).
|
|
18
|
+
The token form compares with `timingSafeStringEqual` (`:153-155`), which is
|
|
19
|
+
defined in `packages/core/src/utils/hmac.ts:57-63`.
|
|
20
|
+
|
|
21
|
+
A mismatch on a request with a body throws (`:199`). A body-less request is not
|
|
22
|
+
an error, but the runner refuses any events it produces: if `verified` is false
|
|
23
|
+
and `receive` returned events, it throws `UnauthorizedError` (`:273-276`). The
|
|
24
|
+
same contract is documented for authors at
|
|
25
|
+
`packages/core/src/wirings/trigger/webhook-source.types.ts:130-135`, and the
|
|
26
|
+
helper that runs the source's steps calls `runPikkuFunc` with `auth: false`
|
|
27
|
+
(`webhook-source-runner.ts:96-101`), so the signature, not a session, is the
|
|
28
|
+
gate.
|
|
29
|
+
|
|
30
|
+
**What this rules out:** dispatching events from a body-carrying request that did
|
|
31
|
+
not verify, and treating an empty signing secret as a valid key.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: decision
|
|
3
|
+
title: An MCP call with no HTTP request stays anonymous by transport design
|
|
4
|
+
description: MCP calls that arrive without an HTTP request, such as stdio, reach the function with no session because auth middleware reads the session only from the request
|
|
5
|
+
tags: core, mcp, auth, transport
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# An MCP call with no HTTP request stays anonymous by transport design
|
|
9
|
+
|
|
10
|
+
`RunMCPEndpointParams.http` in `packages/core/src/wirings/mcp/mcp-runner.ts` is the HTTP request an MCP call arrived on (`mcp-runner.ts:39-41`, field at `:52`).
|
|
11
|
+
|
|
12
|
+
The comment gives the mechanism. Auth middleware reads the session off `wire.http.request`, and every implementation returns early when there is no request (`mcp-runner.ts:42-46`). Without the request, an MCP call reaches the function unauthenticated whatever middleware is registered (`mcp-runner.ts:43-46`).
|
|
13
|
+
|
|
14
|
+
The field is left undefined for transports with no request, such as stdio. The comment says those calls remain anonymous, and that this is a property of the transport rather than a default chosen in the runner (`mcp-runner.ts:48-50`).
|
|
15
|
+
|
|
16
|
+
The surrounding decision about sessionless MCP entry points is in [MCP resources, tools and prompts take sessionless functions](mcp-resources-tools-and-prompts-take-sessionless-functions.md). The refusal for an HTTP call that needs a session is in [an MCP refusal is decided before dispatch](an-mcp-refusal-is-decided-before-dispatch.md).
|
|
17
|
+
|
|
18
|
+
**What this rules out:** treating a request-less MCP call as carrying a session, and filling in a fake HTTP request to make middleware run for a stdio caller.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: decision
|
|
3
|
+
title: An omitted userId on CredentialService addresses the platform credential
|
|
4
|
+
description: The CredentialService contract reads and writes the platform-level credential whenever a per-credential call omits userId, and the better-auth implementation defaults its platform owner to pikku-platform
|
|
5
|
+
tags: core, credentials, security, interface
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# An omitted userId on CredentialService addresses the platform credential
|
|
9
|
+
|
|
10
|
+
`CredentialService` in `packages/core/src/services/credential-service.ts` is the store for dynamic
|
|
11
|
+
credentials such as OAuth tokens and per-user keys. Its doc comment says that throughout the interface,
|
|
12
|
+
an omitted `userId` means the platform-level credential (`packages/core/src/services/credential-service.ts:4`).
|
|
13
|
+
|
|
14
|
+
The per-credential methods take an optional `userId`: `get` (`credential-service.ts:7`), `set` (`:9`),
|
|
15
|
+
`delete` (`:11`) and `has` (`:13`). Omitting it selects the platform credential, so the caller chooses
|
|
16
|
+
the identity by whether it passes one.
|
|
17
|
+
|
|
18
|
+
The methods that enumerate users are different. `getAll` requires `userId` (`credential-service.ts:15`),
|
|
19
|
+
and `getUsersWithCredential` and `getAllUsers` take a credential name or nothing
|
|
20
|
+
(`credential-service.ts:17` and `:19`). They do not have a platform default.
|
|
21
|
+
|
|
22
|
+
The better-auth implementation applies the platform default only to some credentials. It takes an optional
|
|
23
|
+
`platformUserId` that falls back to `PLATFORM_USER_ID` (`packages/services/better-auth/src/better-auth-credential.service.ts:58`).
|
|
24
|
+
Its owner lookup returns null for a name that is not an OAuth2 credential (`better-auth-credential.service.ts:69-70`),
|
|
25
|
+
returns that platform owner only for a singleton OAuth2 name (`better-auth-credential.service.ts:72-73`),
|
|
26
|
+
and otherwise returns the `userId` passed in, or null when none was passed (`better-auth-credential.service.ts:75`).
|
|
27
|
+
So for a non-singleton credential, an omitted `userId` reads as no owner, not as the platform credential. The
|
|
28
|
+
interface comment describes the platform case more widely than this implementation does. Who may link the
|
|
29
|
+
singleton is covered by [a singleton credential belongs to a reserved platform user](../../../../services/better-auth/knowledge/decisions/a-singleton-credential-belongs-to-a-reserved-platform-user.md).
|
|
30
|
+
|
|
31
|
+
**What this rules out:** a sentinel string or an empty value to mean the platform credential, and relying on an
|
|
32
|
+
omitted `userId` to reach the platform credential for a credential that is not a singleton.
|
package/knowledge/decisions/security/analytics-identity-is-stamped-from-the-session-not-the-event.md
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: decision
|
|
3
|
+
title: Analytics identity is stamped from the session and resolvers, never from the event body
|
|
4
|
+
description: Each analytics record takes userId and orgId from the wire session, pikkuUserId from the wire, and the rest from the identity resolvers, not from the event payload
|
|
5
|
+
tags: core, analytics, security
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Analytics identity is stamped from the session and resolvers, never from the event body
|
|
9
|
+
|
|
10
|
+
`resolveEvent` in `packages/core/src/analytics/analytics.ts:55-72` builds
|
|
11
|
+
`userIdentity` from the wire session (`:59`, `:67-69`), `this.wire.pikkuUserId`
|
|
12
|
+
(`:70`), and the output of the identity resolver (`:61`, `:71`). The event's own
|
|
13
|
+
fields are `name`, `props` and the optional `at` (`:60`, `:66`). The identity
|
|
14
|
+
fields are not read from the event.
|
|
15
|
+
|
|
16
|
+
`packages/core/src/analytics/analytics.types.ts:26-29` gives the reason: identity
|
|
17
|
+
is stamped server-side and never read from the request body, which is what makes
|
|
18
|
+
an unauthenticated ingest safe to expose.
|
|
19
|
+
|
|
20
|
+
The consent notes in this folder cover the cookie side of identity. They do not
|
|
21
|
+
cover how the record's identity is attached.
|
|
22
|
+
|
|
23
|
+
**What this rules out:** a client supplying `userId`, `orgId` or `pikkuUserId` in
|
|
24
|
+
an analytics event and having it recorded as the caller's identity.
|