@voltro/cli 0.33.0 → 0.35.0
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 +1968 -0
- package/bin/nodeEnvironment.d.mts +30 -0
- package/bin/nodeEnvironment.mjs +158 -0
- package/bin/voltro.mjs +69 -5
- package/dist/addCommand-BNeoeSxe.js +124 -0
- package/dist/addCommand-aXSQveak.js +2 -0
- package/dist/agentsMd-BTchIZku.js +2 -0
- package/dist/agentsMd-mhQMF1bx.js +254 -0
- package/dist/apiBuild-B8aoJvuw.js +2 -0
- package/dist/{apiBuild-h9VHtnlw.js → apiBuild-DYD_ONLD.js} +46 -46
- package/dist/{appGraph-gQ_6GkQQ.js → appGraph-KGDPTuTy.js} +1 -1
- package/dist/appGraph-zuMGKVYX.js +2 -0
- package/dist/appPort-B_HpJ_ck.js +48 -0
- package/dist/baselineCommand-C2ClWZN3.js +2 -0
- package/dist/baselineCommand-DIttzO8A.js +227 -0
- package/dist/bin.js +71 -28
- package/dist/build-CD8K4XOr.js +711 -0
- package/dist/cacheCommand-DA4OH9xt.js +42 -0
- package/dist/capabilitiesCommand-nq_pz5xd.js +123 -0
- package/dist/checkCommand-Ct9xkTrS.js +232 -0
- package/dist/checkCommand-DKpDLlqu.js +2 -0
- package/dist/{cliArgs-qdZSElM3.js → cliArgs-D4p8n7EE.js} +12 -1
- package/dist/cliError-BmdYnghb.js +10 -0
- package/dist/cliOutput-D1tSBoRM.js +15 -0
- package/dist/{cliRuntime-Oh517vCV.js → cliRuntime-Dh7UDinH.js} +20 -20
- package/dist/cloudClient-DWL-Hw_T.js +67 -0
- package/dist/cloudCmd-Cvv5HGaZ.js +364 -0
- package/dist/clusterCmd-D5wsCmA_.js +54 -0
- package/dist/codegen-CYM3Zqrf.js +605 -0
- package/dist/codegen-ChBi_hVa.js +2 -0
- package/dist/codegenCommand-C4YoQIc2.js +30 -0
- package/dist/codemodRunner-BnFq3Fgu.js +5384 -0
- package/dist/commandRunner-BLAEFLjp.js +47 -0
- package/dist/commands-BE8E7zF3.js +816 -0
- package/dist/connectionConfig-UFlIEiys.js +66 -0
- package/dist/dashboardCommand-D7SgZGaN.js +25 -0
- package/dist/dataCommand-BhYwDgg-.js +537 -0
- package/dist/dataProfile-dW-PsfLB.js +15 -0
- package/dist/dbCommand-DS4b97Is.js +2 -0
- package/dist/{dbCommand-DTLKAfbA.js → dbCommand-O8HA63s2.js} +552 -402
- package/dist/{dev-C_P8FLSx.js → dev-C7sFZq3m.js} +3670 -3169
- package/dist/dev-D2BikO7a.js +3 -0
- package/dist/devActivity-Dx_3nnGv.js +100 -0
- package/dist/devActivity.js +1 -1
- package/dist/dialectDriver-CgXnDfec.js +39 -0
- package/dist/discover-C9XKJDco.js +25 -0
- package/dist/doctorCommand-CM4Ch9C7.js +2 -0
- package/dist/{checkCommand-xGhRFFg2.js → doctorCommand-DnimF5IM.js} +523 -1278
- package/dist/dormancyCommand-QewYug_s.js +69 -0
- package/dist/e2eCmd-BRabZww-.js +147 -0
- package/dist/embeddingsCommand-BfiLS_QI.js +73 -0
- package/dist/envCommand-CCGPRQY1.js +60 -0
- package/dist/evalCommand-6RUfPen4.js +118 -0
- package/dist/evolveCommand-CHsLCtDf.js +281 -0
- package/dist/fileTaxonomy-CJfgOllU.js +457 -0
- package/dist/frameworkTableAssembly-YyVe32Cb.js +2 -0
- package/dist/frameworkTableAssembly-oMPBKqlE.js +511 -0
- package/dist/generateCommand-DbgcUpGw.js +147 -0
- package/dist/index.d.ts +45 -0
- package/dist/index.js +4 -3
- package/dist/infoCommand-DwOgK1t6.js +60 -0
- package/dist/{inspect-BUUjt773.js → inspect-CjTYzAs_.js} +113 -41
- package/dist/inspect-P4pxoMaV.js +2 -0
- package/dist/inspectCmd-EHFZ9yYu.js +224 -0
- package/dist/inspectFetch-EMuhTG_9.js +151 -0
- package/dist/inspectMetrics-CfdKLh6t.js +72 -0
- package/dist/loadEnv-D9nEOClM.js +44 -0
- package/dist/logFileSink-C_D2wRN1.js +105 -0
- package/dist/logsCmd-D36xK7Zu.js +260 -0
- package/dist/manifestBuild-COkJoyAr.js +2 -0
- package/dist/{manifestBuild-BLrVuSlM.js → manifestBuild-hpPLaGxV.js} +1 -1
- package/dist/metaCommands-7MJfZ5cf.js +196 -0
- package/dist/migrate-BV7I-ZHZ.js +83 -0
- package/dist/mssqlClusterPatch-_4cE_nun.js +44 -0
- package/dist/newCommand-COWOJ1_E.js +156 -0
- package/dist/nodeEnvironment-cGFAj1J8.js +28 -0
- package/dist/packageCommand-Cug_3Ogl.js +271 -0
- package/dist/pageConvention-cEiRxdab.js +5 -0
- package/dist/privacyCommand-C-Df56U_.js +146 -0
- package/dist/probeCommand-CZfaaUOZ.js +122 -0
- package/dist/projectScaffold-DmzEKHib.js +2 -0
- package/dist/projectScaffold-LMMtaavR.js +814 -0
- package/dist/renderModeScan-D7J1B7Kw.js +105 -0
- package/dist/renderProfile-1OWWAAtx.js +81 -0
- package/dist/runtimeRegistry-DMeKfTHP.js +81 -0
- package/dist/runtimeTrace-CH3eUiMw.js +91 -0
- package/dist/scheduleCmd-DQRu6BZC.js +149 -0
- package/dist/scheduleManifestCmd-D2x0CTTY.js +249 -0
- package/dist/schemaIr-UJybUUZW.js +103 -0
- package/dist/{sdkgen-C81QIkiL.js → sdkgen-BLkvGRfX.js} +111 -209
- package/dist/seedRunner-ZmLSqNe2.js +333 -0
- package/dist/serveCommand-CbDHU6l-.js +2 -0
- package/dist/serveCommand-iwlUBNS1.js +1766 -0
- package/dist/serveEntry.js +5 -5
- package/dist/serverlessCommand-CfJZy6dS.js +482 -0
- package/dist/start-BgN62boB.js +3 -0
- package/dist/start-T4VesWiM.js +1087 -0
- package/dist/startEntry.js +2 -2
- package/dist/staticCommand-Dr2M6tpU.js +304 -0
- package/dist/storageCommand-Co6NfLqN.js +42 -0
- package/dist/templates-De8IR5-c.js +102 -0
- package/dist/test-CI6iDsYc.js +115 -0
- package/dist/tracesCmd-DStmCJPi.js +232 -0
- package/dist/tsconfigPaths-BWXBWgcl.js +107 -0
- package/dist/tsxLoader-EuXmSJ1K.js +51 -0
- package/dist/typecheckCommand-BlsWiCNq.js +61 -0
- package/dist/updateCommand-BlMZhWgO.js +2 -0
- package/dist/updateCommand-x0pI_x-B.js +585 -0
- package/dist/webDev-BcykISYQ2.js +2 -0
- package/dist/{inspectMetrics-1xzTKAFx.js → webDev-Dybxew86.js} +988 -1560
- package/dist/webhookDiscovery-CrGAfhIG.js +2 -0
- package/dist/webhookDiscovery-D7VaeMlz.js +51 -0
- package/dist/webhooksCommand-DlAgS2Iw.js +267 -0
- package/dist/workflowsCmd-BGF-mRZ5.js +608 -0
- package/package.json +209 -17
- package/templates/AGENTS.core.md +58 -3
- package/templates/AGENTS.md +64 -7
- package/templates/agent-docs/_index.md +6 -4
- package/templates/agent-docs/_manifest.json +23 -6
- package/templates/agent-docs/ai.md +370 -0
- package/templates/agent-docs/authentication.md +313 -31
- package/templates/agent-docs/caching.md +6 -0
- package/templates/agent-docs/cli.md +853 -50
- package/templates/agent-docs/data.md +608 -12
- package/templates/agent-docs/database/migrations.md +223 -25
- package/templates/agent-docs/database/misc.md +156 -40
- package/templates/agent-docs/database/querying.md +19 -1
- package/templates/agent-docs/database/scaling.md +60 -0
- package/templates/agent-docs/database/schema.md +1 -0
- package/templates/agent-docs/database/seedsdialects.md +208 -19
- package/templates/agent-docs/database/transactions.md +68 -0
- package/templates/agent-docs/deployment.md +284 -25
- package/templates/agent-docs/introduction.md +88 -17
- package/templates/agent-docs/local-first-mobile.md +79 -4
- package/templates/agent-docs/multi-tenancy.md +188 -42
- package/templates/agent-docs/observability.md +58 -3
- package/templates/agent-docs/plugins/ai-flows.md +247 -2
- package/templates/agent-docs/plugins/analytics-postgres.md +1 -1
- package/templates/agent-docs/plugins/audit.md +37 -1
- package/templates/agent-docs/plugins/auth-social.md +143 -0
- package/templates/agent-docs/plugins/auth-workos.md +4 -2
- package/templates/agent-docs/plugins/auth.md +131 -6
- package/templates/agent-docs/plugins/billing.md +132 -15
- package/templates/agent-docs/plugins/cdc-out.md +46 -7
- package/templates/agent-docs/plugins/clickhouse.md +32 -1
- package/templates/agent-docs/plugins/duckdb.md +1 -1
- package/templates/agent-docs/plugins/flags.md +132 -0
- package/templates/agent-docs/plugins/governance.md +105 -7
- package/templates/agent-docs/plugins/multitenancy.md +9 -4
- package/templates/agent-docs/plugins/presence.md +13 -2
- package/templates/agent-docs/plugins/ratelimit.md +9 -0
- package/templates/agent-docs/plugins/search.md +162 -8
- package/templates/agent-docs/plugins/sso-saml.md +47 -8
- package/templates/agent-docs/plugins/storage.md +11 -0
- package/templates/agent-docs/plugins/webhooks.md +105 -0
- package/templates/agent-docs/plugins.md +152 -18
- package/templates/agent-docs/reference.md +60 -3
- package/templates/agent-docs/releases.md +1117 -0
- package/templates/agent-docs/routing.md +43 -25
- package/templates/agent-docs/scheduling.md +27 -0
- package/templates/agent-docs/schema-driven-ui.md +92 -12
- package/templates/agent-docs/security.md +426 -0
- package/templates/agent-docs/templates/apibackends.md +87 -18
- package/templates/agent-docs/templates/appshells.md +32 -14
- package/templates/agent-docs/templates/overview.md +13 -8
- package/templates/agent-docs/testing.md +211 -14
- package/templates/agent-docs/whats-new.md +98 -136
- package/templates/agent-docs/workflows.md +231 -14
- package/templates/apps/api-ai/actions/summarize.action.ts +11 -0
- package/templates/apps/api-ai/package.json +8 -7
- package/templates/apps/api-auth/actions/me.action.ts +13 -0
- package/templates/apps/api-auth/package.json +9 -8
- package/templates/apps/api-backend/mutations/notes.create.mutation.ts +9 -0
- package/templates/apps/api-backend/package.json +12 -8
- package/templates/apps/api-backend/queries/notes.query.ts +30 -8
- package/templates/apps/api-backend-deactivation/actions/users.get.action.ts +14 -0
- package/templates/apps/api-backend-deactivation/mutations/users.create.mutation.ts +8 -0
- package/templates/apps/api-backend-deactivation/mutations/users.deactivate.mutation.ts +10 -0
- package/templates/apps/api-backend-deactivation/package.json +8 -7
- package/templates/apps/api-backend-mail/actions/sendWelcome.action.ts +17 -0
- package/templates/apps/api-backend-mail/mutations/notes.create.mutation.ts +9 -0
- package/templates/apps/api-backend-mail/package.json +9 -8
- package/templates/apps/api-backend-mail/queries/notes.query.ts +30 -8
- package/templates/apps/api-backend-mariadb/.env.example +14 -0
- package/templates/apps/api-backend-mariadb/mutations/notes.create.mutation.ts +9 -0
- package/templates/apps/api-backend-mariadb/package.json +10 -9
- package/templates/apps/api-backend-mariadb/queries/notes.query.ts +30 -8
- package/templates/apps/api-backend-sqlite/.env.example +14 -0
- package/templates/apps/api-backend-sqlite/mutations/notes.create.mutation.ts +9 -0
- package/templates/apps/api-backend-sqlite/package.json +9 -8
- package/templates/apps/api-backend-sqlite/queries/notes.query.ts +30 -8
- package/templates/apps/api-backend-storage/actions/uploadAvatar.action.ts +13 -0
- package/templates/apps/api-backend-storage/actions/uploadDocument.action.ts +12 -0
- package/templates/apps/api-backend-storage/mutations/notes.create.mutation.ts +9 -0
- package/templates/apps/api-backend-storage/package.json +9 -8
- package/templates/apps/api-backend-storage/queries/notes.query.ts +30 -8
- package/templates/apps/api-cms/actions/content.get.action.ts +7 -0
- package/templates/apps/api-cms/actions/content.types.action.ts +6 -0
- package/templates/apps/api-cms/actions/me.action.ts +13 -0
- package/templates/apps/api-cms/app.config.ts +19 -0
- package/templates/apps/api-cms/authz.ts +63 -0
- package/templates/apps/api-cms/mutations/content.publish.mutation.ts +10 -0
- package/templates/apps/api-cms/mutations/content.saveDraft.mutation.ts +10 -0
- package/templates/apps/api-cms/mutations/content.unpublish.mutation.ts +5 -0
- package/templates/apps/api-cms/package.json +11 -10
- package/templates/apps/api-cms/queries/content.list.query.ts +23 -7
- package/templates/apps/api-cms/tests/accessDecisions.test.ts +121 -0
- package/templates/apps/api-cms/tests/content.descriptors.test.ts +8 -4
- package/templates/apps/api-collab/mutations/documents.create.mutation.ts +8 -0
- package/templates/apps/api-collab/mutations/documents.setBody.mutation.ts +13 -0
- package/templates/apps/api-collab/package.json +9 -8
- package/templates/apps/api-collab/queries/documents.query.ts +19 -7
- package/templates/apps/api-data-advanced/package.json +9 -8
- package/templates/apps/api-data-advanced/queries/authors.withBooks.query.ts +12 -0
- package/templates/apps/api-data-advanced/queries/books.search.query.ts +9 -0
- package/templates/apps/api-durable/mutations/orders.approve.mutation.ts +17 -0
- package/templates/apps/api-durable/mutations/orders.place.mutation.ts +9 -0
- package/templates/apps/api-durable/package.json +9 -8
- package/templates/apps/api-feature-flags/actions/notes.export.action.ts +16 -0
- package/templates/apps/api-feature-flags/mutations/notes.create.mutation.ts +12 -0
- package/templates/apps/api-feature-flags/package.json +10 -9
- package/templates/apps/api-governance/README.md +30 -8
- package/templates/apps/api-governance/actions/profiles.get.action.server.ts +17 -1
- package/templates/apps/api-governance/actions/profiles.get.action.ts +29 -4
- package/templates/apps/api-governance/database/schema.ts +16 -4
- package/templates/apps/api-governance/mutations/profiles.create.mutation.ts +12 -0
- package/templates/apps/api-governance/package.json +9 -8
- package/templates/apps/api-kv/actions/sync.pull.action.ts +15 -0
- package/templates/apps/api-kv/actions/sync.reset.action.ts +13 -0
- package/templates/apps/api-kv/actions/sync.status.action.ts +7 -0
- package/templates/apps/api-kv/package.json +9 -8
- package/templates/apps/api-kv/queries/events.list.query.ts +19 -8
- package/templates/apps/api-moderation/mutations/comments.create.mutation.ts +11 -0
- package/templates/apps/api-moderation/mutations/posts.create.mutation.ts +13 -0
- package/templates/apps/api-moderation/package.json +9 -8
- package/templates/apps/api-observability/mutations/notes.create.mutation.ts +8 -0
- package/templates/apps/api-observability/package.json +9 -8
- package/templates/apps/api-observability/queries/notes.list.query.ts +13 -0
- package/templates/apps/api-ratelimit/mutations/notes.create.mutation.ts +14 -0
- package/templates/apps/api-ratelimit/package.json +9 -8
- package/templates/apps/api-rbac/package.json +9 -8
- package/templates/apps/api-rest/package.json +8 -7
- package/templates/apps/api-saas/mutations/projects.create.mutation.ts +13 -0
- package/templates/apps/api-saas/package.json +12 -11
- package/templates/apps/api-saas/queries/projects.list.query.ts +11 -0
- package/templates/apps/api-saas-starter/actions/me.action.ts +13 -0
- package/templates/apps/api-saas-starter/app.config.ts +19 -0
- package/templates/apps/api-saas-starter/authz.ts +75 -0
- package/templates/apps/api-saas-starter/mutations/invites.create.mutation.ts +10 -0
- package/templates/apps/api-saas-starter/mutations/projects.create.mutation.ts +9 -0
- package/templates/apps/api-saas-starter/package.json +15 -11
- package/templates/apps/api-saas-starter/queries/invites.list.query.ts +19 -8
- package/templates/apps/api-saas-starter/queries/projects.list.query.ts +18 -7
- package/templates/apps/api-saas-starter/tests/accessDecisions.test.ts +135 -0
- package/templates/apps/api-search/mutations/articles.create.mutation.ts +14 -0
- package/templates/apps/api-search/package.json +9 -8
- package/templates/apps/api-search/queries/articles.list.query.ts +21 -8
- package/templates/apps/api-status/README.md +10 -3
- package/templates/apps/api-status/app.config.ts +8 -3
- package/templates/apps/api-status/authz.ts +5 -3
- package/templates/apps/api-status/package.json +9 -8
- package/templates/apps/api-status/queries/components.list.query.ts +14 -6
- package/templates/apps/api-status/queries/incidents.live.query.ts +23 -10
- package/templates/apps/api-status/queries/updates.list.query.ts +16 -9
- package/templates/apps/api-status/tests/status.test.ts +9 -1
- package/templates/apps/api-versioning/actions/documents.asOf.action.ts +12 -0
- package/templates/apps/api-versioning/actions/documents.history.action.ts +13 -0
- package/templates/apps/api-versioning/mutations/documents.create.mutation.ts +9 -0
- package/templates/apps/api-versioning/mutations/documents.update.mutation.ts +12 -0
- package/templates/apps/api-versioning/package.json +9 -8
- package/templates/apps/api-webhooks/mutations/orders.fulfill.mutation.ts +16 -0
- package/templates/apps/api-webhooks/package.json +10 -9
- package/templates/apps/api-webhooks/queries/orders.list.query.ts +10 -0
- package/templates/apps/changelog/package.json +8 -6
- package/templates/apps/edge-functions/package.json +2 -2
- package/templates/apps/frontend-admin/package.json +10 -8
- package/templates/apps/frontend-admin/src/lib/admin.ts +20 -9
- package/templates/apps/frontend-admin/src/locales/de.ts +11 -1
- package/templates/apps/frontend-admin/src/locales/en.ts +13 -1
- package/templates/apps/frontend-admin/src/pages/admin/[entity]/page.tsx +65 -22
- package/templates/apps/frontend-admin/src/pages/admin/entity.test.tsx +130 -23
- package/templates/apps/frontend-admin/src/pages/admin/layout.tsx +3 -2
- package/templates/apps/frontend-admin/src/pages/admin/page.test.tsx +19 -2
- package/templates/apps/frontend-admin/src/pages/admin/page.tsx +9 -4
- package/templates/apps/frontend-app/app.config.ts +4 -3
- package/templates/apps/frontend-app/package.json +11 -8
- package/templates/apps/frontend-app/src/lib/api.ts +25 -0
- package/templates/apps/frontend-app/src/pages/page.test.tsx +130 -82
- package/templates/apps/frontend-app/src/pages/page.tsx +14 -18
- package/templates/apps/frontend-auth/package.json +10 -8
- package/templates/apps/frontend-blank/package.json +9 -7
- package/templates/apps/frontend-cms/package.json +11 -9
- package/templates/apps/frontend-collab/package.json +12 -9
- package/templates/apps/frontend-collab/src/pages/page.test.tsx +122 -78
- package/templates/apps/frontend-contact/package.json +9 -7
- package/templates/apps/frontend-dashboard/package.json +9 -7
- package/templates/apps/frontend-docs/package.json +9 -7
- package/templates/apps/frontend-i18n/package.json +8 -6
- package/templates/apps/frontend-landing/package.json +9 -7
- package/templates/apps/frontend-portal/package.json +10 -8
- package/templates/apps/frontend-saas/app.config.ts +10 -6
- package/templates/apps/frontend-saas/package.json +10 -8
- package/templates/apps/frontend-saas/src/lib/api.ts +27 -32
- package/templates/apps/frontend-saas/src/pages/dashboard/billing/page.tsx +7 -8
- package/templates/apps/frontend-saas/src/pages/dashboard/page.test.tsx +27 -3
- package/templates/apps/frontend-saas/src/pages/dashboard/page.tsx +4 -4
- package/templates/apps/frontend-saas/src/pages/dashboard/team/page.tsx +3 -4
- package/templates/apps/frontend-spa/package.json +9 -7
- package/templates/apps/frontend-ssr/package.json +9 -7
- package/templates/apps/frontend-ssr-api/package.json +10 -8
- package/templates/apps/frontend-static-blog/package.json +8 -6
- package/templates/apps/frontend-status/package.json +10 -8
- package/templates/apps/mobile-app/README.md +1 -0
- package/templates/apps/mobile-app/package.json +4 -2
- package/templates/apps/mobile-app/src/app/index.tsx +22 -12
- package/templates/apps/mobile-app/src/app/orders/[id].tsx +1 -1
- package/templates/apps/mobile-app/src/lib/api.ts +34 -0
- package/templates/apps/mobile-app/voltro.mobile.ts +4 -2
- package/templates/baselines/bare/.env.example +14 -0
- package/templates/baselines/bare/baseline.json +4 -4
- package/templates/baselines/compose/.env.example +14 -0
- package/templates/baselines/compose/README.md +1 -1
- package/templates/baselines/compose/baseline.json +5 -5
- package/templates/baselines/compose-mariadb/.env.example +14 -0
- package/templates/baselines/compose-mariadb/README.md +1 -1
- package/templates/baselines/compose-mariadb/baseline.json +5 -5
- package/templates/baselines/helm/.env.example +14 -0
- package/templates/baselines/helm/baseline.json +4 -4
- package/dist/apiBuild-C-x9YacA.js +0 -2
- package/dist/appGraph-CvQCte0z.js +0 -2
- package/dist/checkCommand-DRovTKza.js +0 -2
- package/dist/commands-CJfepbm4.js +0 -11541
- package/dist/dbCommand-b1gum4td.js +0 -2
- package/dist/dev-iiMtlkfs.js +0 -3
- package/dist/devActivity-BhIu6ncs.js +0 -159
- package/dist/frameworkTableAssembly-BwIrO5nv.js +0 -638
- package/dist/frameworkTableAssembly-D-EebUQX.js +0 -2
- package/dist/inspect-mmBuRXmy.js +0 -2
- package/dist/manifestBuild-Dj8Jjoto.js +0 -2
- package/dist/seedRunner-Bqxgp7HZ.js +0 -230
- package/dist/serveCommand-DdaM4Hup.js +0 -1608
- package/dist/start-C0koT0UO.js +0 -1084
- /package/templates/apps/api-ai/actions/{summarize.action.server.tsx → summarize.action.server.ts} +0 -0
|
@@ -22,6 +22,35 @@ Please report suspected vulnerabilities **privately** — never in public issues
|
|
|
22
22
|
|
|
23
23
|
You'll get an **acknowledgement within 3 business days**, an assessment once we've reproduced the issue, and a **coordinated disclosure** timeline agreed with you — with credit in the release notes if you'd like it. We don't run a paid bug-bounty program yet, but we genuinely value responsible disclosure.
|
|
24
24
|
|
|
25
|
+
## Procedures are default-DENY
|
|
26
|
+
|
|
27
|
+
Every wire-exposed procedure must declare **who may call it**. A `*.query.ts` /
|
|
28
|
+
`*.mutation.ts` / `*.action.ts` / `*.stream.ts` that declares neither `guards:`
|
|
29
|
+
nor `openAccess:` is refused at boot — by `voltro dev`, by `voltro serve`, and by
|
|
30
|
+
`voltro doctor` as a preflight.
|
|
31
|
+
|
|
32
|
+
Before this, `guards:` defaulted to "allowed": a discovered procedure with no
|
|
33
|
+
guard was callable by any authenticated session. Nothing in the code said so,
|
|
34
|
+
and the only structural check was a report a human had to run.
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
guards: [{ scope: 'invoices:read' }] // the caller must hold a scope
|
|
38
|
+
openAccess: 'public pricing, no caller data' // anyone may call it — and why
|
|
39
|
+
internal: true // not on the wire at all
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`openAccess` takes a reason rather than a boolean on purpose: without an explicit
|
|
43
|
+
way to declare an endpoint open, the only way to satisfy the gate is to invent a
|
|
44
|
+
guard, and the guard people invent is one every caller already holds. That reads
|
|
45
|
+
as protection and enforces nothing.
|
|
46
|
+
|
|
47
|
+
The whole app can opt out with one field — `security: { defaultDeny: false }` in
|
|
48
|
+
`app.config.ts`. There is no environment variable for it: the only direction
|
|
49
|
+
anyone reaches for is off, and an env var is how that becomes permanent in one CI
|
|
50
|
+
job with no diff to review. Full detail — including how plugin routes are
|
|
51
|
+
covered per-request in the dispatch spine rather than by the boot gate — in
|
|
52
|
+
[Authorization](/docs/authentication/authorization).
|
|
53
|
+
|
|
25
54
|
## Outbound HTTP is SSRF-guarded by default
|
|
26
55
|
|
|
27
56
|
The `HttpClient` your handlers `yield*` refuses internal targets:
|
|
@@ -88,6 +117,403 @@ DNS is not resolved. A public hostname that *resolves* to a private address (DNS
|
|
|
88
117
|
rebinding) still passes. That vector needs network-layer egress control; it is
|
|
89
118
|
stated here rather than silently implied.
|
|
90
119
|
|
|
120
|
+
## Where the transport settings live
|
|
121
|
+
|
|
122
|
+
Everything in the next three sections — the origin check, which proxies may
|
|
123
|
+
forward a client IP, and the response headers — is one `security:` block in
|
|
124
|
+
`app.config.ts`, read identically by `voltro dev` and `voltro serve`:
|
|
125
|
+
|
|
126
|
+
```ts
|
|
127
|
+
// app.config.ts
|
|
128
|
+
export default {
|
|
129
|
+
security: {
|
|
130
|
+
// Cross-site protection for every state-changing request. Default 'same-origin'.
|
|
131
|
+
originGuard: 'same-origin',
|
|
132
|
+
// Extra origins to accept — a split web/api deployment.
|
|
133
|
+
allowedOrigins: ['https://app.example.com'],
|
|
134
|
+
// Whose x-forwarded-for to believe. Empty (the default) ignores the header.
|
|
135
|
+
trustedProxies: ['private'],
|
|
136
|
+
// Security response headers. On by default; 'strict' applies the api policy to HTML too.
|
|
137
|
+
headers: { mode: 'default' },
|
|
138
|
+
},
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Every field has a safe default, so an app that writes none of this is already
|
|
143
|
+
protected. Each also has an environment override, listed with its section below
|
|
144
|
+
— use those to correct a deployment you cannot rebuild, and the config file for
|
|
145
|
+
everything else. A security decision that exists only as an exported variable on
|
|
146
|
+
one machine is invisible to code review.
|
|
147
|
+
|
|
148
|
+
## Cross-site requests are refused
|
|
149
|
+
|
|
150
|
+
**Every request that can change state is checked** — anything but `GET`, `HEAD`
|
|
151
|
+
and `OPTIONS` — plus the `/ws` upgrade, which is a GET. That is deliberately a
|
|
152
|
+
rule about the METHOD rather than a list of paths, because a mutation reaches
|
|
153
|
+
your api by more than one road: `POST /rpc`, every REST route projected from a
|
|
154
|
+
`publicApi:` mutation, everything in `apiConfig.restRoutes`, and
|
|
155
|
+
`POST /v1/api-keys`. They all resolve the caller through the same auth chain,
|
|
156
|
+
so they are all reachable **with the session cookie automatically attached**.
|
|
157
|
+
|
|
158
|
+
A browser request is accepted when its `Origin` matches the `Host` it was
|
|
159
|
+
addressed to, or is in your allowlist. Anything else gets `403 origin not
|
|
160
|
+
allowed` — including `Origin: null`, which is what a sandboxed iframe sends, and
|
|
161
|
+
a request with no `Origin` whose `Sec-Fetch-Site` says `cross-site`.
|
|
162
|
+
|
|
163
|
+
Before this, framework-wide CSRF protection rested entirely on the
|
|
164
|
+
`SameSite=Lax` cookie default — one caller-overridable attribute that still
|
|
165
|
+
permits top-level-navigation POST, and that does nothing at all for a
|
|
166
|
+
bearer/JWT flow.
|
|
167
|
+
|
|
168
|
+
Reads are not checked: a `GET` cannot be a cross-site write, and checking it
|
|
169
|
+
would break every link into your api.
|
|
170
|
+
|
|
171
|
+
### A split web/api deployment must declare its origins
|
|
172
|
+
|
|
173
|
+
If your web app is served from a different origin than your api, say so — or
|
|
174
|
+
every mutation, every REST write and every socket from that page will 403:
|
|
175
|
+
|
|
176
|
+
```ts
|
|
177
|
+
// app.config.ts
|
|
178
|
+
export default {
|
|
179
|
+
security: {
|
|
180
|
+
allowedOrigins: ['https://app.example.com', 'https://admin.example.com'],
|
|
181
|
+
},
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Same-origin apps (the default layout, and anything behind one ingress) need no
|
|
186
|
+
change. `security: { originGuard: 'off' }` disables the check; a misspelled
|
|
187
|
+
value falls back to **enforcing**, never to off.
|
|
188
|
+
|
|
189
|
+
Environment overrides, for a deployment you cannot rebuild:
|
|
190
|
+
`VOLTRO_ALLOWED_ORIGINS` (comma-separated) and `VOLTRO_ORIGIN_GUARD=off`. If you
|
|
191
|
+
embed the runtime yourself, the same values are
|
|
192
|
+
`RpcServerOptions.security.originGuard` / `.allowedOrigins`.
|
|
193
|
+
|
|
194
|
+
### Server-to-server callers are exempt, and that is deliberate
|
|
195
|
+
|
|
196
|
+
A request carrying neither `Origin` nor `Sec-Fetch-Site` did not come from a
|
|
197
|
+
browsing context, so it is allowed. That covers:
|
|
198
|
+
|
|
199
|
+
- **SSR loaders** — `voltro dev` / `voltro start` render pages server-side and
|
|
200
|
+
their loaders call the api from node, which attaches no origin header. Without
|
|
201
|
+
this exemption, server-side first paint would break on every page with a
|
|
202
|
+
loader.
|
|
203
|
+
- mobile SDKs, `curl`, and any other service calling your api.
|
|
204
|
+
|
|
205
|
+
A CSRF attack needs the victim's ambient credentials, and only a browser attaches
|
|
206
|
+
those — a browser cannot be made to omit `Origin` on a cross-origin POST or a
|
|
207
|
+
WebSocket handshake. An attacker's own server can POST without one, but it has no
|
|
208
|
+
session to ride: that is simply an unauthenticated request, and auth and guards
|
|
209
|
+
still apply to it.
|
|
210
|
+
|
|
211
|
+
Origins are compared by **authority** (host + port), not scheme. Behind a
|
|
212
|
+
TLS-terminating ingress your app sees plain http while the browser reports
|
|
213
|
+
`https://…`, and there is no unforgeable way to learn the external scheme.
|
|
214
|
+
|
|
215
|
+
### Local development is not a special case you have to configure
|
|
216
|
+
|
|
217
|
+
When **both** the origin and the `Host` are loopback (`localhost`, `127.0.0.1`,
|
|
218
|
+
`*.localhost`), the request is accepted. That is the `voltro dev` layout: the web
|
|
219
|
+
dev server proxies the api with `changeOrigin: true`, so the api sees
|
|
220
|
+
`Host: localhost:4000` while the browser correctly reports
|
|
221
|
+
`Origin: http://localhost:5190`. Strictly compared those differ, and every dev
|
|
222
|
+
session would lose its websocket.
|
|
223
|
+
|
|
224
|
+
Both sides must be loopback. A production api on `api.example.com` still refuses
|
|
225
|
+
`Origin: http://localhost:5190` — a browser cannot be made to claim a loopback
|
|
226
|
+
origin it is not on.
|
|
227
|
+
|
|
228
|
+
**Testing a dev server from a phone on your wifi is the one case that needs a
|
|
229
|
+
line.** The page is then `http://192.168.1.5:5190`, which is not loopback, so the
|
|
230
|
+
socket is refused and the api logs `refused cross-site request` naming the
|
|
231
|
+
origin. Add it while you test:
|
|
232
|
+
|
|
233
|
+
```ts
|
|
234
|
+
// app.config.ts
|
|
235
|
+
export default {
|
|
236
|
+
security: {
|
|
237
|
+
allowedOrigins: ['http://192.168.1.5:5190'],
|
|
238
|
+
},
|
|
239
|
+
}
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
The carve-out is deliberately not widened to "both sides are private
|
|
243
|
+
addresses": on a self-hosted internal deployment that would accept any other
|
|
244
|
+
host on the same network, which is a different claim than "the attacker already
|
|
245
|
+
runs code on this machine".
|
|
246
|
+
|
|
247
|
+
### Routes that a third party legitimately POSTs to
|
|
248
|
+
|
|
249
|
+
A few surfaces are outside the check, because their caller cannot be a browser
|
|
250
|
+
riding your user's cookie:
|
|
251
|
+
|
|
252
|
+
- **The inspect surface** (`/_voltro/inspect/*`) — token-gated, and meant to be
|
|
253
|
+
read cross-origin by the local and cloud dashboards.
|
|
254
|
+
- **Incoming webhooks** (`*.webhook.tsx`) — authenticated by the sender's
|
|
255
|
+
signature, which the framework refuses to boot without.
|
|
256
|
+
- **Plugin routes that declare it.** First-party examples:
|
|
257
|
+
`@voltro/plugin-sso-saml` (`/saml` — the IdP makes the browser form-POST a
|
|
258
|
+
signed assertion, which IS a cross-site POST), `@voltro/plugin-storage`'s
|
|
259
|
+
upload routes (a signed upload ticket, plus their own CORS allowlist),
|
|
260
|
+
`@voltro/plugin-billing`'s webhook and `@voltro/plugin-scim` (bearer-only).
|
|
261
|
+
|
|
262
|
+
If you write a plugin HTTP route in that category, declare it:
|
|
263
|
+
|
|
264
|
+
```ts
|
|
265
|
+
import type { PluginHttpRoute } from '@voltro/protocol'
|
|
266
|
+
|
|
267
|
+
const route: PluginHttpRoute = {
|
|
268
|
+
method: 'POST',
|
|
269
|
+
path: '/partner/callback',
|
|
270
|
+
// The signature on the body is the authority — not the session cookie.
|
|
271
|
+
originGuard: 'exempt',
|
|
272
|
+
handle: async (req) => {
|
|
273
|
+
if (!verifySignature(req)) return { status: 401 }
|
|
274
|
+
return { status: 204 }
|
|
275
|
+
},
|
|
276
|
+
}
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
Apply exactly one test before you write that line: *if an attacker's page makes
|
|
280
|
+
a browser send this request with your user's cookies attached, does anything
|
|
281
|
+
happen?* If the answer is "no — it still needs a signature, a bearer token or a
|
|
282
|
+
signed ticket the attacker does not have", the route is exempt. Otherwise it is
|
|
283
|
+
not. The exemption covers the route's whole path prefix, not the sub-paths its
|
|
284
|
+
handler branches on.
|
|
285
|
+
|
|
286
|
+
## The client IP comes from the socket, not from a header
|
|
287
|
+
|
|
288
|
+
`x-forwarded-for` is a request header, so **any client can write it**. Voltro
|
|
289
|
+
ignores it unless you declare which proxies are allowed to forward — the address
|
|
290
|
+
used for rate limiting, geo-blocking and audit records is
|
|
291
|
+
`socket.remoteAddress`, the one value nobody upstream of the kernel can forge.
|
|
292
|
+
|
|
293
|
+
If you run behind a load balancer, ingress or CDN **and** rate-limit or geo-block
|
|
294
|
+
per IP, declare it:
|
|
295
|
+
|
|
296
|
+
```ts
|
|
297
|
+
// app.config.ts
|
|
298
|
+
export default {
|
|
299
|
+
security: {
|
|
300
|
+
trustedProxies: ['private'], // RFC1918 + CGNAT + link-local + unique-local
|
|
301
|
+
// ['loopback'] — local / docker-compose
|
|
302
|
+
// ['10.0.0.0/8', 'fc00::/7'] — explicit CIDRs
|
|
303
|
+
// ['2'] — trust two hops
|
|
304
|
+
// ['*'] — any peer; only when your ingress
|
|
305
|
+
// OVERWRITES rather than appends
|
|
306
|
+
},
|
|
307
|
+
}
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
The environment override is `VOLTRO_TRUSTED_PROXIES`, comma-separated
|
|
311
|
+
(`VOLTRO_TRUSTED_PROXIES=10.0.0.0/8,fc00::/7`).
|
|
312
|
+
|
|
313
|
+
With a list configured, the peer that opened the connection must itself be
|
|
314
|
+
trusted, and the chain is walked right-to-left past every declared proxy — so a
|
|
315
|
+
client that prepends a fake hop cannot push the resolved address further left.
|
|
316
|
+
The same setting decides whether `x-forwarded-proto` is believed, which is what
|
|
317
|
+
gates HSTS below.
|
|
318
|
+
|
|
319
|
+
Without it, a per-IP limiter still works; it just counts every request against
|
|
320
|
+
your proxy's address instead of the caller's. Embedders pass the same list as
|
|
321
|
+
`RpcServerOptions.security.trustedProxies`.
|
|
322
|
+
|
|
323
|
+
### The same address reaches your plugin routes
|
|
324
|
+
|
|
325
|
+
A plugin HTTP route receives it as `req.remoteAddr` — already resolved through
|
|
326
|
+
the policy above. **Use that, never `req.headers['x-forwarded-for']`.** The
|
|
327
|
+
built-in auth routes do: what `@voltro/plugin-auth` and
|
|
328
|
+
`@voltro/plugin-auth-social` write into `sessions.ipAddress` is the resolved
|
|
329
|
+
address, so the column a breach investigation reads cannot be chosen by the
|
|
330
|
+
caller.
|
|
331
|
+
|
|
332
|
+
```ts
|
|
333
|
+
import type { PluginHttpRoute } from '@voltro/protocol'
|
|
334
|
+
|
|
335
|
+
const route: PluginHttpRoute = {
|
|
336
|
+
method: 'POST',
|
|
337
|
+
path: '/partner/callback',
|
|
338
|
+
handle: async (req) => {
|
|
339
|
+
// Resolved once per request; `undefined` only when there is no socket
|
|
340
|
+
// address (a unix socket, or a hand-built request in a test).
|
|
341
|
+
await audit(req.remoteAddr ?? null)
|
|
342
|
+
return { status: 204 }
|
|
343
|
+
},
|
|
344
|
+
}
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
## Security headers ship by default
|
|
348
|
+
|
|
349
|
+
Every response from the api listener carries:
|
|
350
|
+
|
|
351
|
+
```
|
|
352
|
+
content-security-policy: default-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none'
|
|
353
|
+
x-frame-options: DENY
|
|
354
|
+
referrer-policy: no-referrer
|
|
355
|
+
x-content-type-options: nosniff
|
|
356
|
+
strict-transport-security: max-age=15552000; includeSubDomains (https only)
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
`default-src 'none'` is safe here because this listener serves the **api** —
|
|
360
|
+
rpc JSON, the socket upgrade, inspect routes, webhooks, plugin routes — not your
|
|
361
|
+
web app's HTML, which is a different listener. A `text/html` response from a
|
|
362
|
+
plugin route gets a relaxed policy instead
|
|
363
|
+
(`frame-ancestors 'none'; base-uri 'none'; object-src 'none'`), so a docs page
|
|
364
|
+
still renders while clickjacking and base-tag injection stay closed.
|
|
365
|
+
|
|
366
|
+
HSTS is sent **only over https** and deliberately never carries `preload`:
|
|
367
|
+
preload is effectively irreversible for a domain, so it has to be your decision.
|
|
368
|
+
`Cross-Origin-Opener-Policy` and `Cross-Origin-Resource-Policy` are not defaulted
|
|
369
|
+
either — guessing them breaks a legitimate cross-origin dashboard — but you can
|
|
370
|
+
add them.
|
|
371
|
+
|
|
372
|
+
A route that sets a header itself always wins; the framework only fills gaps.
|
|
373
|
+
|
|
374
|
+
The whole policy is one field, including an `extra` bag for headers the
|
|
375
|
+
framework does not default:
|
|
376
|
+
|
|
377
|
+
```ts
|
|
378
|
+
// app.config.ts
|
|
379
|
+
export default {
|
|
380
|
+
security: {
|
|
381
|
+
headers: {
|
|
382
|
+
mode: 'default', // 'off' | 'default' | 'strict'
|
|
383
|
+
hsts: 'max-age=31536000; includeSubDomains', // or false to drop it
|
|
384
|
+
extra: { 'permissions-policy': 'geolocation=()' },
|
|
385
|
+
},
|
|
386
|
+
},
|
|
387
|
+
}
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
`mode: 'strict'` applies the api policy to HTML too. That is the documented
|
|
391
|
+
trade: it blanks `@voltro/plugin-openapi`'s `/docs` page, which loads its viewer
|
|
392
|
+
from a CDN. Choose it when this listener serves no HTML you own.
|
|
393
|
+
|
|
394
|
+
Environment overrides: `VOLTRO_SECURITY_HEADERS` (`off|default|strict`),
|
|
395
|
+
`VOLTRO_CSP`, `VOLTRO_CSP_HTML`, `VOLTRO_HSTS` — each accepting `off` to drop
|
|
396
|
+
just that one. Embedders pass the same object as
|
|
397
|
+
`RpcServerOptions.security.headers`.
|
|
398
|
+
|
|
399
|
+
## Incoming webhooks must verify their caller
|
|
400
|
+
|
|
401
|
+
An incoming webhook is a **public POST that runs your application code**. A
|
|
402
|
+
`defineIncomingWebhook({...})` that declares no signature scheme now refuses to
|
|
403
|
+
boot, naming the endpoint.
|
|
404
|
+
|
|
405
|
+
```ts
|
|
406
|
+
export default defineIncomingWebhook({
|
|
407
|
+
id: 'orders.paid',
|
|
408
|
+
provider: stripeWebhookProvider(), // HMAC scheme + replay window + idempotency
|
|
409
|
+
payload: OrderPaid,
|
|
410
|
+
handler: async (ctx) => { /* the caller is already verified here */ },
|
|
411
|
+
})
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
Four ways to satisfy it:
|
|
415
|
+
|
|
416
|
+
| Declaration | Means |
|
|
417
|
+
|---|---|
|
|
418
|
+
| `provider: stripeWebhookProvider()` | a preset brings the scheme, replay window and idempotency key |
|
|
419
|
+
| `signature: { _tag: 'hmac', … }` | a hand-declared scheme for a sender with its own convention |
|
|
420
|
+
| `verification: 'provider'` | your handler verifies with the provider's own SDK |
|
|
421
|
+
| `verification: 'none'` | deliberately public — a gateway or IP allow-list owns the boundary; logged as a warning on every boot |
|
|
422
|
+
|
|
423
|
+
A signature-verified webhook reads its shared secret from an env var named after
|
|
424
|
+
its id:
|
|
425
|
+
|
|
426
|
+
```bash
|
|
427
|
+
VOLTRO_WEBHOOK_SECRET_ORDERS_PAID=<the value the sender holds>
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
**A missing secret is now a 503, not a skipped check.** It used to run the
|
|
431
|
+
handler unverified, so forgetting one variable silently converted a verified
|
|
432
|
+
webhook into an open one — in production, with nothing in the code to review.
|
|
433
|
+
The framework never invents this value: the sender holds the other half, so a
|
|
434
|
+
generated secret would authenticate nobody.
|
|
435
|
+
|
|
436
|
+
### `voltro doctor` tells you before a deploy does
|
|
437
|
+
|
|
438
|
+
The preflight reports the same verdict the boot reaches:
|
|
439
|
+
|
|
440
|
+
```
|
|
441
|
+
incoming webhook verification
|
|
442
|
+
✓ signature / provider verified 2
|
|
443
|
+
! deliberately public (none) 1
|
|
444
|
+
internal.sync — `verification: 'none'`; a gateway / IP allow-list owns this URL
|
|
445
|
+
✗ nothing verifies the caller 0
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
An endpoint in the last row exits non-zero — that app does not start. The middle
|
|
449
|
+
row never fails; it is there so a deliberately public URL is visible in a review
|
|
450
|
+
instead of only in a boot log. Both rows are in `voltro doctor --json` under
|
|
451
|
+
`webhookVerification`.
|
|
452
|
+
|
|
453
|
+
## The rpc body cap counts bytes as they arrive — and answers 413 either way
|
|
454
|
+
|
|
455
|
+
`POST /rpc` is capped at 8 MiB (`VOLTRO_MAX_RPC_BODY_BYTES`). The limit is
|
|
456
|
+
enforced **while the body streams**, so a `Transfer-Encoding: chunked` request
|
|
457
|
+
with no `Content-Length` is cut off at the cap rather than buffered without
|
|
458
|
+
bound. A declared oversize length is still rejected up front, so an honest client
|
|
459
|
+
gets its `413` without uploading anything.
|
|
460
|
+
|
|
461
|
+
**Both shapes end in the same response:**
|
|
462
|
+
|
|
463
|
+
| Request | Response |
|
|
464
|
+
| --- | --- |
|
|
465
|
+
| `Content-Length` over the cap | `413 Payload Too Large`, before the upload |
|
|
466
|
+
| `Transfer-Encoding: chunked` over the cap | `413 Payload Too Large`, once the counter crosses the cap |
|
|
467
|
+
| Either, under the cap | handled normally |
|
|
468
|
+
|
|
469
|
+
The refusal is logged server-side under the `voltro:security` scope, with the cap
|
|
470
|
+
and the number of bytes read before the server stopped (never the body's real
|
|
471
|
+
size — it is not read).
|
|
472
|
+
|
|
473
|
+
Uploads ride separate plugin routes with their own `limits.maxBytes`, and are
|
|
474
|
+
unaffected.
|
|
475
|
+
|
|
476
|
+
## Logs are redacted by default
|
|
477
|
+
|
|
478
|
+
Every logger surface — `createLogger`, `makeLogger`, `LoggerLayer` — installs a redactor with no configuration, and it runs **before** formatting and **before** the sink fan-out, so a masked value reaches neither stdout nor any downstream sink (the CLI buffer, logship, Datadog).
|
|
479
|
+
|
|
480
|
+
```ts
|
|
481
|
+
import { createLogger } from '@voltro/logger'
|
|
482
|
+
|
|
483
|
+
const log = createLogger({ scope: 'orders' })
|
|
484
|
+
log.info('request', { headers: req.headers, body: input })
|
|
485
|
+
// → headers.authorization = [redacted]
|
|
486
|
+
// headers.cookie = [redacted]
|
|
487
|
+
// body.newPassword = [redacted]
|
|
488
|
+
// headers['user-agent'] = curl/8 ← untouched
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
Three rules, all on:
|
|
492
|
+
|
|
493
|
+
- **Key names** (`DEFAULT_REDACT_KEYS`) — matched exactly on a normalised key, so `apiKey`, `api_key`, `API-KEY` and `Api Key` are one key. Credentials, bearer/API tokens, `authorization` / `cookie` / `set-cookie` / `x-api-key`, session and second-factor values, and the payment / government identifiers (`creditCard`, `cvv`, `iban`, `ssn`, …) that must never reach a log index.
|
|
494
|
+
- **Key fragments** (`DEFAULT_REDACT_KEY_PARTS`) — substring matches for the compound names real code writes: `newPassword`, `oldPassword`, `stripeApiKey`, `userAccessToken`.
|
|
495
|
+
- **Value shapes** — a credential is masked regardless of its key: `Bearer …`, `Basic …`, a JWT, `sk_` / `pk_` / `whsec_`-prefixed keys, GitHub / Slack / AWS key ids, a PEM private key.
|
|
496
|
+
|
|
497
|
+
There is deliberately **no entropy heuristic**. A trace id, a content hash, a git sha and a base64 thumbnail are all long and high-entropy — a redactor that eats the fields an incident is read through gets switched off wholesale, which costs more than the gap it closed.
|
|
498
|
+
|
|
499
|
+
### Adding to the list, and why nothing subtracts
|
|
500
|
+
|
|
501
|
+
`redactKeys` **adds**; it does not replace:
|
|
502
|
+
|
|
503
|
+
```ts
|
|
504
|
+
createLogger({ redactKeys: ['patientId', 'policyNumber'] })
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
There is no way to remove a built-in key. The only reason to un-mask `password` is to read it while debugging, and the answer to that is to log a non-secret projection — not to publish the value to stdout and to whatever log shipper the deployment happens to have. A subtraction knob would also apply to every **dependency** logging under that key, including plugins you never read, which is a blast radius an app cannot assess.
|
|
508
|
+
|
|
509
|
+
The deliberate escape hatch is `redact`, a transform you write and own — and it runs *after* the built-in redactor, so it can mask more and structurally cannot unmask:
|
|
510
|
+
|
|
511
|
+
```ts
|
|
512
|
+
createLogger({ redact: (record) => ({ ...record, fields: { ...record.fields, region: 'eu' } }) })
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
> **If a log line you relied on went quiet**, rename the field rather than reaching for an exemption. `traceId`, `requestId` and `cookieName` are all untouched — the framework's own session diagnostic was renamed from `cookie` to `cookieName` for exactly this reason.
|
|
516
|
+
|
|
91
517
|
## Auditing what your log tables actually hold
|
|
92
518
|
|
|
93
519
|
```bash
|