gina 0.6.2 → 0.6.3
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 +57 -0
- package/README.md +41 -12
- package/ROADMAP.md +3 -2
- package/bin/cli +19 -2
- package/framework/v0.6.3/VERSION +1 -0
- package/framework/{v0.6.2 → v0.6.3}/core/asset/plugin/dist/vendor/gina/html/statusbar.html +6 -0
- package/framework/v0.6.3/core/asset/plugin/dist/vendor/gina/html/statusbar.html.br +0 -0
- package/framework/v0.6.3/core/asset/plugin/dist/vendor/gina/html/statusbar.html.gz +0 -0
- package/framework/{v0.6.2 → v0.6.3}/core/asset/plugin/dist/vendor/gina/inspector/index.html +1 -0
- package/framework/{v0.6.2 → v0.6.3}/core/asset/plugin/dist/vendor/gina/inspector/inspector.css +50 -1
- package/framework/{v0.6.2 → v0.6.3}/core/asset/plugin/dist/vendor/gina/inspector/inspector.js +457 -57
- package/framework/{v0.6.2 → v0.6.3}/core/asset/plugin/dist/vendor/gina/js/gina.js +1457 -58
- package/framework/v0.6.3/core/asset/plugin/dist/vendor/gina/js/gina.min.js +625 -0
- package/framework/v0.6.3/core/asset/plugin/dist/vendor/gina/js/gina.min.js.br +0 -0
- package/framework/v0.6.3/core/asset/plugin/dist/vendor/gina/js/gina.min.js.gz +0 -0
- package/framework/{v0.6.2 → v0.6.3}/core/connectors/couchbase/index.js +114 -0
- package/framework/{v0.6.2 → v0.6.3}/core/connectors/couchbase/lib/connector.js +14 -3
- package/framework/{v0.6.2 → v0.6.3}/core/connectors/couchbase/lib/session-store.js +14 -3
- package/framework/{v0.6.2 → v0.6.3}/core/connectors/couchbase/lib/session-store.v3.js +29 -2
- package/framework/{v0.6.2 → v0.6.3}/core/connectors/couchbase/lib/session-store.v4.js +29 -2
- package/framework/{v0.6.2 → v0.6.3}/core/connectors/mongodb/lib/session-store.js +24 -2
- package/framework/{v0.6.2 → v0.6.3}/core/connectors/redis/lib/session-store.js +33 -9
- package/framework/{v0.6.2 → v0.6.3}/core/connectors/scylladb/lib/session-store.js +25 -2
- package/framework/{v0.6.2 → v0.6.3}/core/connectors/sqlite/lib/session-store.js +26 -2
- package/framework/{v0.6.2 → v0.6.3}/core/controller/controller.js +201 -21
- package/framework/{v0.6.2 → v0.6.3}/core/controller/controller.render-nunjucks.js +8 -0
- package/framework/{v0.6.2 → v0.6.3}/core/controller/controller.render-swig.js +85 -4
- package/framework/{v0.6.2 → v0.6.3}/core/gna.js +23 -1
- package/framework/{v0.6.2 → v0.6.3}/core/plugins/lib/csrf/src/main.js +9 -5
- package/framework/{v0.6.2 → v0.6.3}/core/plugins/lib/security-headers/hide-powered-by/README.md +20 -0
- package/framework/{v0.6.2 → v0.6.3}/core/plugins/lib/validator/src/form-validator.js +119 -43
- package/framework/{v0.6.2 → v0.6.3}/core/plugins/lib/validator/src/main.js +451 -9
- package/framework/{v0.6.2 → v0.6.3}/core/server.isaac.js +50 -54
- package/framework/{v0.6.2 → v0.6.3}/core/server.js +240 -3
- package/framework/{v0.6.2 → v0.6.3}/core/template/boilerplate/bundle/index.js +6 -4
- package/framework/{v0.6.2 → v0.6.3}/core/template/conf/settings.json +10 -0
- package/framework/{v0.6.2 → v0.6.3}/lib/cache/README.md +9 -1
- package/framework/{v0.6.2 → v0.6.3}/lib/cmd/audit/verify.js +11 -9
- package/framework/{v0.6.2 → v0.6.3}/lib/cmd/bundle/mcp-start.js +21 -12
- package/framework/{v0.6.2 → v0.6.3}/lib/cmd/connector/help.txt +1 -1
- package/framework/{v0.6.2 → v0.6.3}/lib/cmd/framework/init.js +13 -1
- package/framework/{v0.6.2 → v0.6.3}/lib/dto/src/main.js +18 -11
- package/framework/{v0.6.2 → v0.6.3}/lib/logger/src/containers/file/index.js +29 -1
- package/framework/{v0.6.2 → v0.6.3}/lib/logger/src/containers/mq/speaker.js +30 -1
- package/framework/{v0.6.2 → v0.6.3}/lib/math/index.js +57 -6
- package/framework/{v0.6.2 → v0.6.3}/lib/merge/src/main.js +34 -5
- package/framework/{v0.6.2 → v0.6.3}/lib/render-cache/src/main.js +77 -4
- package/framework/{v0.6.2 → v0.6.3}/lib/routing/src/main.js +13 -1
- package/framework/{v0.6.2 → v0.6.3}/lib/routing-introspect/src/main.js +64 -2
- package/framework/v0.6.3/lib/secrets/src/backends/env.js +75 -0
- package/framework/{v0.6.2 → v0.6.3}/package.json +1 -1
- package/gna.js +5 -4
- package/llms.txt +19 -17
- package/package.json +5 -4
- package/schema/connectors.json +2 -2
- package/schema/routing.json +5 -0
- package/schema/settings.json +6 -1
- package/script/generate_gna_types.js +1 -1
- package/script/soak/couchbase-soak.js +1316 -0
- package/types/globals.d.ts +2 -0
- package/types/gna.d.ts +5 -0
- package/types/index.d.ts +1 -0
- package/utils/helper.js +38 -7
- package/framework/v0.6.2/VERSION +0 -1
- package/framework/v0.6.2/core/asset/plugin/dist/vendor/gina/html/statusbar.html.br +0 -0
- package/framework/v0.6.2/core/asset/plugin/dist/vendor/gina/html/statusbar.html.gz +0 -0
- package/framework/v0.6.2/core/asset/plugin/dist/vendor/gina/js/gina.min.js +0 -603
- package/framework/v0.6.2/core/asset/plugin/dist/vendor/gina/js/gina.min.js.br +0 -0
- package/framework/v0.6.2/core/asset/plugin/dist/vendor/gina/js/gina.min.js.gz +0 -0
- package/framework/v0.6.2/lib/secrets/src/backends/env.js +0 -60
- /package/framework/{v0.6.2 → v0.6.3}/AUTHORS +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/LICENSE +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/asset/html/nolayout.html +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/asset/html/static.html +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/asset/img/android-chrome-192x192.png +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/asset/img/android-chrome-512x512.png +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/asset/img/apple-touch-icon.png +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/asset/img/favicon-16x16.png +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/asset/img/favicon-32x32.png +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/asset/img/favicon.ico +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/asset/plugin/README.md +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/asset/plugin/dist/vendor/gina/beemaster/beemaster.css +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/asset/plugin/dist/vendor/gina/beemaster/beemaster.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/asset/plugin/dist/vendor/gina/beemaster/index.html +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/asset/plugin/dist/vendor/gina/css/gina.min.css +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/asset/plugin/dist/vendor/gina/css/gina.min.css.br +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/asset/plugin/dist/vendor/gina/css/gina.min.css.gz +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/asset/plugin/dist/vendor/gina/inspector/have_heart_one-webfont.woff2 +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/asset/plugin/dist/vendor/gina/inspector/logo.svg +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/asset/plugin/dist/vendor/gina/js/gina.onload.min.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/asset/plugin/dist/vendor/gina/js/gina.onload.min.js.br +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/asset/plugin/dist/vendor/gina/js/gina.onload.min.js.gz +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/config.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/connectors/ai/index.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/connectors/ai/lib/connector.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/connectors/couchbase/lib/connector.v3.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/connectors/couchbase/lib/connector.v4.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/connectors/couchbase/lib/n1ql.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/connectors/duckdb/index.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/connectors/duckdb/lib/connector.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/connectors/mongodb/index.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/connectors/mongodb/lib/connector.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/connectors/mongodb/lib/job-store.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/connectors/mongodb/lib/pipeline-loader.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/connectors/mysql/index.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/connectors/mysql/lib/connector.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/connectors/postgresql/index.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/connectors/postgresql/lib/connector.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/connectors/redis/index.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/connectors/redis/lib/connector.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/connectors/redis/lib/job-store.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/connectors/redis/lib/render-cache-store.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/connectors/scylladb/index.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/connectors/scylladb/lib/connector.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/connectors/sql-parser.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/connectors/sqlite/index.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/connectors/sqlite/lib/connector.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/connectors/sqlite/lib/job-store.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/content.encoding +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/controller/controller.framework.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/controller/controller.render-json.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/controller/controller.render-nunjucks-async.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/controller/controller.render-stream.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/controller/controller.render-swig-async.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/controller/controller.render-v1.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/controller/index.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/controller/inspector-window-emit.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/controller/release-banner.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/dev/index.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/dev/lib/class.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/dev/lib/factory.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/dev/lib/tools.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/locales/README.md +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/locales/currency.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/locales/dist/language/en.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/locales/dist/language/fr.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/locales/dist/region/en.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/locales/dist/region/fr.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/locales/index.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/mime.types +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/model/entity.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/model/index.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/model/template/entityFactory.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/model/template/index.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/plugins/README.md +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/plugins/index.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/plugins/lib/csrf/README.md +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/plugins/lib/csrf/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/plugins/lib/security-headers/README.md +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/plugins/lib/security-headers/coep/README.md +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/plugins/lib/security-headers/coep/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/plugins/lib/security-headers/coep/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/plugins/lib/security-headers/coop/README.md +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/plugins/lib/security-headers/coop/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/plugins/lib/security-headers/coop/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/plugins/lib/security-headers/corp/README.md +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/plugins/lib/security-headers/corp/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/plugins/lib/security-headers/corp/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/plugins/lib/security-headers/csp/README.md +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/plugins/lib/security-headers/csp/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/plugins/lib/security-headers/csp/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/plugins/lib/security-headers/hide-powered-by/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/plugins/lib/security-headers/hide-powered-by/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/plugins/lib/security-headers/hsts/README.md +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/plugins/lib/security-headers/hsts/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/plugins/lib/security-headers/hsts/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/plugins/lib/security-headers/origin-agent-cluster/README.md +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/plugins/lib/security-headers/origin-agent-cluster/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/plugins/lib/security-headers/origin-agent-cluster/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/plugins/lib/security-headers/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/plugins/lib/security-headers/referrer-policy/README.md +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/plugins/lib/security-headers/referrer-policy/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/plugins/lib/security-headers/referrer-policy/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/plugins/lib/security-headers/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/plugins/lib/security-headers/x-content-type-options/README.md +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/plugins/lib/security-headers/x-content-type-options/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/plugins/lib/security-headers/x-content-type-options/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/plugins/lib/security-headers/x-dns-prefetch-control/README.md +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/plugins/lib/security-headers/x-dns-prefetch-control/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/plugins/lib/security-headers/x-dns-prefetch-control/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/plugins/lib/security-headers/x-download-options/README.md +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/plugins/lib/security-headers/x-download-options/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/plugins/lib/security-headers/x-download-options/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/plugins/lib/security-headers/x-frame-options/README.md +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/plugins/lib/security-headers/x-frame-options/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/plugins/lib/security-headers/x-frame-options/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/plugins/lib/security-headers/x-permitted-cross-domain-policies/README.md +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/plugins/lib/security-headers/x-permitted-cross-domain-policies/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/plugins/lib/security-headers/x-permitted-cross-domain-policies/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/plugins/lib/security-headers/x-xss-protection/README.md +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/plugins/lib/security-headers/x-xss-protection/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/plugins/lib/security-headers/x-xss-protection/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/plugins/lib/session/README.md +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/plugins/lib/session/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/plugins/lib/session/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/plugins/lib/storage/README.md +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/plugins/lib/storage/build.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/plugins/lib/storage/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/plugins/lib/storage/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/plugins/lib/validator/README.md +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/plugins/lib/validator/build.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/plugins/lib/validator/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/router.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/server.express.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/status.codes +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/template/_gitignore +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/template/boilerplate/bundle/config/app.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/template/boilerplate/bundle/config/connectors.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/template/boilerplate/bundle/config/routing.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/template/boilerplate/bundle/config/settings.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/template/boilerplate/bundle/config/settings.server.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/template/boilerplate/bundle/config/templates.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/template/boilerplate/bundle/config/watchers.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/template/boilerplate/bundle/controllers/controller.content.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/template/boilerplate/bundle/controllers/controller.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/template/boilerplate/bundle/controllers/setup.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/template/boilerplate/bundle/locales/en.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/template/boilerplate/bundle_namespace/controllers/controller.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/template/boilerplate/bundle_public/css/default.css +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/template/boilerplate/bundle_public/css/home.css +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/template/boilerplate/bundle_public/css/vendor/readme.md +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/template/boilerplate/bundle_public/favicon.ico +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/template/boilerplate/bundle_public/js/components/x-checklist.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/template/boilerplate/bundle_public/js/vendor/readme.md +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/template/boilerplate/bundle_public/manifest.webmanifest +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/template/boilerplate/bundle_public/readme.md +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/template/boilerplate/bundle_public/sw.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/template/boilerplate/bundle_templates/handlers/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/template/boilerplate/bundle_templates/html/content/homepage.html +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/template/boilerplate/bundle_templates/html/includes/error-msg-noscript.html +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/template/boilerplate/bundle_templates/html/includes/error-msg-outdated-browser.html +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/template/boilerplate/bundle_templates/html/includes/x-checklist.html +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/template/boilerplate/bundle_templates/html/layouts/main.html +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/template/command/gina.bat.tpl +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/template/command/gina.tpl +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/template/conf/env.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/template/conf/manifest.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/template/conf/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/template/conf/statics.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/template/conf/templates.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/template/error/client/json/401.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/template/error/client/json/403.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/template/error/client/json/404.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/template/error/server/html/50x.html +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/template/error/server/json/500.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/template/error/server/json/503.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/core/template/extensions/logger/config.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/helpers/console.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/helpers/context.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/helpers/data/LICENSE +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/helpers/data/README.md +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/helpers/data/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/helpers/data/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/helpers/dateFormat.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/helpers/index.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/helpers/json/LICENSE +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/helpers/json/README.md +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/helpers/json/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/helpers/json/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/helpers/path.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/helpers/plugins/README.md +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/helpers/plugins/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/helpers/plugins/src/api-error.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/helpers/plugins/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/helpers/prototypes.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/helpers/task.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/helpers/text.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/admin/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/admin/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/archiver/README.md +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/archiver/build.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/archiver/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/archiver/src/dep/jszip.min.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/archiver/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/async/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/async/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/audit/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/audit/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/audit-store.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/authn/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/authn/src/lockout.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/authn/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/authn/src/totp.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/authz-gate/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/authz-gate/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cache/build.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cache/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cache/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/aliases.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/audit/arguments.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/audit/help.txt +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/bundle/add.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/bundle/arguments.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/bundle/build.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/bundle/copy.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/bundle/cp.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/bundle/help.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/bundle/help.txt +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/bundle/inc/name-rewrite.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/bundle/list.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/bundle/man.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/bundle/mcp.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/bundle/oas.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/bundle/openapi.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/bundle/remove.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/bundle/rename.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/bundle/restart.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/bundle/rm.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/bundle/start.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/bundle/status.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/bundle/stop.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/bundle/types.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/cache/arguments.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/cache/clear.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/cache/help.txt +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/cache/stats.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/connector/add.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/connector/arguments.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/connector/help.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/connector/infer.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/connector/list.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/connector/migrate.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/connector/models.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/connector/remove.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/connector/rm.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/connector/test.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/container/arguments.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/container/help.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/container/help.txt +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/container/man.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/container/ps.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/container/stop.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/controller/add.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/controller/arguments.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/controller/help.txt +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/controller/inc/args.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/controller/inc/namespace.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/controller/inc/reference-rewrite.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/controller/inc/reference-scan.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/controller/inc/scaffold.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/controller/remove.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/controller/rename.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/controller/rm.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/env/add.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/env/get.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/env/help.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/env/help.txt +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/env/link-dev.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/env/list.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/env/remove.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/env/rm.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/env/set.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/env/unset.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/env/use.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/framework/add.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/framework/arguments.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/framework/build.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/framework/dot.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/framework/get.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/framework/help.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/framework/help.txt +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/framework/link-node-modules.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/framework/link.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/framework/list.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/framework/man.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/framework/msg.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/framework/open.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/framework/remove.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/framework/reset.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/framework/restart.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/framework/set.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/framework/start.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/framework/status.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/framework/stop.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/framework/tail.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/framework/update.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/framework/version.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/gina-dev.1.md +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/gina-framework.1.md +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/gina.1.md +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/helper.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/i18n/add.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/i18n/arguments.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/i18n/export.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/i18n/help.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/i18n/help.txt +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/i18n/import.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/i18n/scan.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/image/_host.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/image/arguments.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/image/build.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/image/help.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/image/help.txt +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/image/list.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/image/man.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/image/rm.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/image/run.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/index.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/inspector/help.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/inspector/help.txt +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/inspector/open.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/man-render.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/minion/arguments.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/minion/help.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/minion/help.txt +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/minion/kill.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/minion/list.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/msg.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/port/help.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/port/help.txt +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/port/inc/scan.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/port/list.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/port/reset.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/port/set.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/project/add.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/project/arguments.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/project/backup.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/project/build.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/project/help.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/project/help.txt +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/project/import.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/project/list.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/project/man.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/project/move.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/project/remove.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/project/rename.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/project/restart.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/project/restore.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/project/rm.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/project/start.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/project/status.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/project/stop.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/protocol/arguments.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/protocol/help.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/protocol/help.txt +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/protocol/list.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/protocol/remove.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/protocol/set.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/scope/add.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/scope/help.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/scope/help.txt +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/scope/link-local.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/scope/link-production.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/scope/list.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/scope/remove.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/scope/rm.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/scope/use.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/secrets/arguments.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/secrets/check.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/secrets/help.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/secrets/help.txt +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/secrets/scan.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/service/help.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/service/help.txt +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/service/list.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/service/man.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/service/start.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd/view/add.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd-status-format/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cmd-status-format/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/collection/README.md +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/collection/build.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/collection/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/collection/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/config.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/connector-config/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/connector-config/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/connector-error/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/connector-error/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/connector-registry/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/connector-registry/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cron/README.md +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cron/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/cron/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/domain/LICENSE +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/domain/README.md +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/domain/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/domain/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/dto/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/dto-pipe/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/dto-pipe/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/dto-types/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/dto-types/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/generator/index.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/i18n/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/i18n/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/image-build/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/image-build/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/index.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/inherits/LICENSE +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/inherits/README.md +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/inherits/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/inherits/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/inspector-events/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/inspector-events/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/inspector-redact/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/inspector-redact/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/instrument/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/instrument/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/job/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/job/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/job-store.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/json-config-header/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/json-config-header/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/logger/README.md +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/logger/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/logger/src/containers/default/index.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/logger/src/containers/file/lib/logrotator/README.md +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/logger/src/containers/file/lib/logrotator/index.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/logger/src/containers/mq/index.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/logger/src/containers/mq/listener.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/logger/src/helper.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/logger/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/mcp-dispatch/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/mcp-dispatch/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/mcp-http/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/mcp-http/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/mcp-server/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/mcp-server/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/merge/README.md +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/merge/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/metrics/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/metrics/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/model.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/net-locality/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/net-locality/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/nunjucks-filters/README.md +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/nunjucks-filters/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/nunjucks-filters/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/nunjucks-resolver/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/nunjucks-resolver/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/proc.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/release-watch/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/release-watch/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/render-cache/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/render-cache-store.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/routing/README.md +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/routing/build.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/routing/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/routing/src/radix.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/routing-introspect/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/secrets/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/secrets/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/session-store.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/shell.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/sqlite-driver.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/state.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/swig-filters/README.md +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/swig-filters/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/swig-filters/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/swig-resolver/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/swig-resolver/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/template-loaders/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/template-loaders/src/loaders/http.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/template-loaders/src/loaders/memory.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/template-loaders/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/url/README.md +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/url/index.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/url/routing.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/uuid/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/uuid/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/validator.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/watcher/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/watcher/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/ws-framing/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/ws-framing/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/ws-query/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/ws-query/src/main.js +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/ws-session/package.json +0 -0
- /package/framework/{v0.6.2 → v0.6.3}/lib/ws-session/src/main.js +0 -0
package/llms.txt
CHANGED
|
@@ -524,7 +524,7 @@ Output path override: `--output=/path/to/mcp.json`.
|
|
|
524
524
|
|
|
525
525
|
Baseline architecture lives in two new libs: `lib/mcp-server/` (transport-agnostic JSON-RPC + lifecycle) and `lib/mcp-dispatch/` (HTTP loopback). Both are registered in `lib.mcpServer` / `lib.mcpDispatch` — **go through the registry, bare-module resolution does not work in CLI daemon scope**.
|
|
526
526
|
|
|
527
|
-
Streamable HTTP transport (MCP Phase 2b) shipped in 0.3.7 — `bundle:mcp-start --transport=http` (library `lib/mcp-http`; flags `--http-host`/`--http-port`/`--auth-token`/`--cors-origin`/`--max-in-flight`/`--allow-insecure`). **Fail-closed start gate:** the transport binds loopback and enforces a built-in `Origin` allowlist, and a bearer token is optional on that default because those two ARE the protection — but remove either (a non-loopback `--http-host`, or `--cors-origin=*`, which disables the only defence against DNS rebinding: a loopback bind does not help there, the browser is already local) and `start()` REJECTS unless a token is configured or `--allow-insecure` / `mcp.json > server > allowInsecure` (strict boolean) asserts an upstream boundary — a mesh, a NetworkPolicy, or an authenticating reverse proxy. It refuses BEFORE binding, so no unauthenticated port is ever reachable. Bearer validation sha256-hashes BOTH sides before `timingSafeEqual`, so the compare is fixed-length and cannot short-circuit on token length. **Read `GINA_MCP_AUTH_TOKEN` via `getEnvVar`, never `process.env`** — `filterArgs()` (`bin/cli`) moves every `GINA_*` key into `process.gina` and DELETES it from `process.env`, so a direct read is always undefined under the CLI and the token silently never applies; the
|
|
527
|
+
Streamable HTTP transport (MCP Phase 2b) shipped in 0.3.7 — `bundle:mcp-start --transport=http` (library `lib/mcp-http`; flags `--http-host`/`--http-port`/`--auth-token`/`--cors-origin`/`--max-in-flight`/`--allow-insecure`). **Fail-closed start gate:** the transport binds loopback and enforces a built-in `Origin` allowlist, and a bearer token is optional on that default because those two ARE the protection — but remove either (a non-loopback `--http-host`, or `--cors-origin=*`, which disables the only defence against DNS rebinding: a loopback bind does not help there, the browser is already local) and `start()` REJECTS unless a token is configured or `--allow-insecure` / `mcp.json > server > allowInsecure` (strict boolean) asserts an upstream boundary — a mesh, a NetworkPolicy, or an authenticating reverse proxy. It refuses BEFORE binding, so no unauthenticated port is ever reachable. Bearer validation sha256-hashes BOTH sides before `timingSafeEqual`, so the compare is fixed-length and cannot short-circuit on token length. **Read `GINA_MCP_AUTH_TOKEN` via `getEnvVar`, never `process.env`** — `filterArgs()` (`bin/cli`) moves every `GINA_*` key into `process.gina` and DELETES it from `process.env`, so a direct read is always undefined under the CLI and the token silently never applies; since 0.6.3 the `${secret:KEY}` env backend reads the framework environment first, then `process.env`, so `${secret:GINA_*}` placeholders resolve under the CLI too (still fail-closed when unset). `resolveHttpHost`'s former `GINA_HOST_V4` tier was REMOVED in 0.6.3 and replaced with `GINA_BIND_HOST` (framework env reader first) — `host_v4` is the address clients CONNECT to (often a LAN address), and reviving it as a bind tier would have moved the default bind off loopback.
|
|
528
528
|
|
|
529
529
|
---
|
|
530
530
|
|
|
@@ -712,7 +712,7 @@ The Inspector is a dev-mode SPA embedded in every bundle at `/_gina/inspector/`.
|
|
|
712
712
|
- **Server-side (SSE agent):** `/_gina/agent` endpoint `event: log` — combined stream in standalone mode. Same log format as `/_gina/logs`.
|
|
713
713
|
- **Server-side (engine.io):** `{ type: 'log', data: {...} }` WebSocket messages pushed from the `ioServer` connection handler when engine.io is configured.
|
|
714
714
|
|
|
715
|
-
**Lazy activation** — `process.gina._inspectorActive` is `false` at startup. Set to `true` when the Inspector SPA, `/_gina/logs`, or `/_gina/
|
|
715
|
+
**Lazy activation** — `process.gina._inspectorActive` is `false` at startup. Set to `true` when the Inspector SPA, `/_gina/logs`, `/_gina/agent`, or `/_gina/indexes` is first accessed. All profiling infrastructure (timeline init, query log wiring, Inspector payload emission) is gated on this flag. JSON responses stay clean until a developer actually opens the Inspector. The flag is per-process, so in a proxy-routed multi-bundle project each bundle activates independently — and the Inspector SPA follows the monitored tab across bundles (#B205): every applied payload's `environment.webroot` is compared with the base its server-side channels point at, and on a change the `/_gina/logs` (and passive `/_gina/agent`) streams are closed and re-opened against the new bundle, whose first hit latches that bundle's activation. The first page rendered on a newly-visited bundle predates its activation, so its own Flow/Query data is absent — entries appear from the next render on. The pid freshness guard treats a different-pid payload as stale only within the SAME webroot; a different-webroot payload is a legitimate bundle crossing and is applied.
|
|
716
716
|
|
|
717
717
|
**Dev mode injections** (both added to user pages in dev mode, before `</body>`):
|
|
718
718
|
- **`__logsScript`** — patches `console.log/info/warn/error/debug` to push `{ t, l, b, s }` entries to `window.__ginaLogs`. The Inspector reads this via `window.opener.__ginaLogs`.
|
|
@@ -752,7 +752,7 @@ Dev-mode query instrumentation captures every database query tied to the current
|
|
|
752
752
|
- **Drag-to-select log rows** — `mousedown` → `mousemove` → `mouseup` builds a range selection in real time. Plain click (no movement) copies the single row with a green flash + left accent feedback.
|
|
753
753
|
- **Copy badge fade-out** — after copy, badge shows "Copied", fades out (400ms opacity transition via `setTimeout`), then clears the selection. `transitionend` was unreliable — replaced with `setTimeout`.
|
|
754
754
|
- **Selection styling** — 3px left amber accent (`::before` pseudo-element), subtle amber background, rounded corners (6px) on first/last rows of contiguous groups. CSS `:has()` and `+` sibling combinators detect group boundaries.
|
|
755
|
-
- **Tab layout presets** — segmented control (joined buttons with SVG icons) in settings panel, split from other settings by a thin divider. Four presets: Balanced (default: Data, View, Logs, Forms, Query, Flow), Backend (Data, Query, Flow, Logs, View, Forms), Frontend (View, Data, Forms, Logs, Query, Flow), Custom (user-defined drag-to-reorder). Color-coded preview pills below the control show the active order at a glance. `applyTabLayout()` reorders DOM nodes (preserving listeners). `renderLayoutPreview()` rebuilds the pill row. Persisted in `localStorage.__gina_inspector_tab_layout`. Custom order persisted separately in `localStorage.__gina_inspector_tab_layout_custom` as a JSON array. Custom mode adds `.bm-drag-mode` to the tab bar — tabs become draggable with grab cursor, 2px amber drop indicator, and 4px drag threshold.
|
|
755
|
+
- **Tab layout presets** — segmented control (joined buttons with SVG icons) in settings panel, split from other settings by a thin divider. Four presets over the full 8-tab roster: Balanced (default: Data, View, Logs, Forms, Query, Flow, Stream, Events), Backend (Data, Query, Flow, Logs, View, Forms, Stream, Events), Frontend (View, Data, Forms, Logs, Query, Flow, Stream, Events), Custom (user-defined drag-to-reorder). Color-coded preview pills below the control show the active order at a glance — labels/colors resolve through `pillLabel()`/`pillColor()` fallbacks (#B194, 0.6.3: the two newest tabs had been added to the presets but not the preview maps, so their pills rendered the literal "undefined"; an unmapped future tab now renders its capitalized name in a neutral color, and adding a tab means updating the presets AND both preview maps — the roster-completeness test asserts every preset tab has entries). `applyTabLayout()` reorders DOM nodes (preserving listeners). `renderLayoutPreview()` rebuilds the pill row. Persisted in `localStorage.__gina_inspector_tab_layout`. Custom order persisted separately in `localStorage.__gina_inspector_tab_layout_custom` as a JSON array, accepted up to the preset roster length (#B194: the former hardcoded 6-tab cap silently rejected a 7-8-tab custom order on read, so it never survived a reload). Custom mode adds `.bm-drag-mode` to the tab bar — tabs become draggable with grab cursor, 2px amber drop indicator, and 4px drag threshold. In drag mode each tab button shows an injected `×` close button: `hideTab()` hides the tab (persisted to `localStorage.__gina_inspector_tab_layout_hidden`, custom order rewritten, active tab switched away if needed) and its pill re-renders dimmed + struck-through at the end of the row. Since 0.6.3 those dimmed pills are click-to-restore controls: each carries `data-tab`, a native `Restore <Tab> tab` tooltip and a leading `+` glyph (inline-block, so it escapes the strikethrough; amber on hover), with the click delegated on `#bm-layout-preview` because the row is rebuilt via innerHTML. `restoreTab()` — the per-tab inverse of `hideTab()` — removes the one name, re-shows the button at the END of the tab bar (hidden buttons park at the FRONT of the nav, so an explicit appendChild makes the landing deterministic), persists the order, and deliberately does not activate the restored tab. The Reset link (`restoreAllTabs()`) remains the restore-everything bulk path.
|
|
756
756
|
- **Performance anomaly alerts** — View tab dot indicator (8px circle, heartbeat animation) activates when page metrics exceed thresholds (load > 3s warn / 10s critical, transfer > 1MB / 5MB, FCP > 2.5s / 4s, query total > 500ms / 2s, query count > 20 / 50). Affected badges get `bm-perf-warn` (amber border) or `bm-perf-critical` (red border) class. Tooltip shows threshold details. `checkPerfAnomalies(metrics, queries)` runs on every poll cycle.
|
|
757
757
|
|
|
758
758
|
---
|
|
@@ -805,11 +805,11 @@ Dev-mode query instrumentation captures every database query tied to the current
|
|
|
805
805
|
|
|
806
806
|
72. **Supply-chain visibility — tarball enumeration, peerDependencies, security-commit bundle coupling.** Enumerate a published tarball via `npm view <pkg>@<ver> dist.tarball` + `curl` + `tar tzf` (NOT `npm pack --dry-run` — it triggers the prepare script's "Prerelease update" commit). **`peerDependencies` aggregate into the dep graph regardless of `optional: true`** — Socket / Dependabot / `npm audit` all read peerDeps unconditionally; the fix pattern is removing them entirely in favour of a lib-local version registry (gina's `lib/connector-registry/`). **Security-tagged source commits MUST rebuild the browser bundle in the same commit** (pre-commit hook + the bundle-freshness CI workflow enforce it). The vendored-deps era is CLOSED: `core/deps/` was deleted 2026-07-18 (`4c173422` require-swap + `4a8eb1e6` deletion — busboy became the `@rhinostone/busboy` npm fork; the OSV workflow now queries the fork's tracked base versions (`busboy@1.6.0`, `streamsearch@1.1.0`) explicitly via a `TRACKED_FORKS` table in the vendored-CVE scan script, which still walks any future vendored sub-manifest). If a dep is ever vendored again: its sub-`package.json` dependency edges are BY DESIGN and load-bearing for CVE visibility — never strip them (the `4a29ca0c` strip was reverted in `e5d5d0a2` for exactly this).
|
|
807
807
|
|
|
808
|
-
77. **Session cookies in gina are NOT framework-owned — they are issued by the bundle's own `app.use(session({...}))` call via `express-session`, so framework-level cookie hardening cannot be transparent without regressing intentional bundle choices.** The framework's `lib.SessionStore(session)` factory is a thin wrapper that receives the bundle's `express-session` module reference, reads `connectors.json[session.name].connector`, and returns a connector-specific Store class. It never holds a reference to the cookie options, and it runs once per bundle boot — long after the bundle's `index.js` has already captured `var session = require('express-session')` as a local. Nothing in `core/server.js`, `core/server.isaac.js`, or `core/server.express.js` writes `Set-Cookie` — those three files have zero grep matches on `cookie` and `set-cookie`. **Chokepoint analysis trap**: the single shared response entry at `core/server.js:2324` (`self.instance.all('*', function onInstance(request, response, next) { ... })`) does look like the natural place to wrap `response.setHeader` to post-process every `Set-Cookie` value, and Isaac + Express both route through it. But Set-Cookie strings carry no provenance: `Set-Cookie: sessionid=abc; Path=/; Secure` could be a bundle that never thought about `HttpOnly` (safe to add) OR a bundle that explicitly set `httpOnly: false` because a client-side validator or toolbar has to read `document.cookie` (adding `HttpOnly` breaks it). Inspecting three real bundles (`~/Sites/<consumer-app>/src/<bundle>/index.js`) showed all three deliberately set `httpOnly: false` and comment out `sameSite` — a transparent wrap would have silently regressed every one. This is exactly the failure mode the global "Don't strip what you haven't surveyed" rule warns about, in its dual form: "don't ADD what the source didn't ask for either". **The correct shape for a cookie-hardening feature** is an opt-in plugin at `core/plugins/lib/session/src/main.js` that wraps `expressSession(options)` with a factory: reads `settings.json > session.cookie.{sameSite, httpOnly, secure}`, merges defaults into `options.cookie` only for flags the caller did NOT set (guarded by `Object.prototype.hasOwnProperty.call(caller, key)`), validates the browser-parity invariant (`SameSite=None` without `Secure` throws), and passes through. Adoption is a one-line swap in the bundle bootstrap: `var session = require('gina').plugins.Session(require('express-session'))`. Existing bundles that don't adopt continue working exactly as before; adopting is explicit and visible in the diff. **The measurement step that surfaced the trap**: before writing any code, grep real bundle code for cookie flag patterns — `grep -nE 'httpOnly|sameSite|secure[\s:]' ~/Sites/<project>/src/*/index.js`. If any bundle has deliberate `httpOnly: false` or `sameSite` commented out, transparent wrapping is off the table and the feature must be opt-in. Precedent: #CSRF1 (2026-04-24, shipped in `0.3.7-alpha.8`) — initial design pivoted from per-request `response.setHeader` wrap to opt-in `gina.plugins.Session` plugin after the consumer-app measurement showed three bundles with deliberate `httpOnly: false`. The plugin shape also becomes the natural seam for #CSRF2 (signed double-submit token middleware, planned for `0.3.8`), which needs a session-aware injection point — session hardening and CSRF token plumbing share the same mount scope.
|
|
808
|
+
77. **Session cookies in gina are NOT framework-owned — they are issued by the bundle's own `app.use(session({...}))` call via `express-session`, so framework-level cookie hardening cannot be transparent without regressing intentional bundle choices.** The framework's `lib.SessionStore(session)` factory is a thin wrapper that receives the bundle's `express-session` module reference, reads `connectors.json[session.name].connector`, and returns a connector-specific Store class. It never holds a reference to the cookie options, and it runs once per bundle boot — long after the bundle's `index.js` has already captured `var session = require('express-session')` as a local. Nothing in `core/server.js`, `core/server.isaac.js`, or `core/server.express.js` writes `Set-Cookie` — those three files have zero grep matches on `cookie` and `set-cookie`. **Chokepoint analysis trap**: the single shared response entry at `core/server.js:2324` (`self.instance.all('*', function onInstance(request, response, next) { ... })`) does look like the natural place to wrap `response.setHeader` to post-process every `Set-Cookie` value, and Isaac + Express both route through it. But Set-Cookie strings carry no provenance: `Set-Cookie: sessionid=abc; Path=/; Secure` could be a bundle that never thought about `HttpOnly` (safe to add) OR a bundle that explicitly set `httpOnly: false` because a client-side validator or toolbar has to read `document.cookie` (adding `HttpOnly` breaks it). Inspecting three real bundles (`~/Sites/<consumer-app>/src/<bundle>/index.js`) showed all three deliberately set `httpOnly: false` and comment out `sameSite` — a transparent wrap would have silently regressed every one. This is exactly the failure mode the global "Don't strip what you haven't surveyed" rule warns about, in its dual form: "don't ADD what the source didn't ask for either". **The correct shape for a cookie-hardening feature** is an opt-in plugin at `core/plugins/lib/session/src/main.js` that wraps `expressSession(options)` with a factory: reads `settings.json > session.cookie.{sameSite, httpOnly, secure}`, merges defaults into `options.cookie` only for flags the caller did NOT set (guarded by `Object.prototype.hasOwnProperty.call(caller, key)`), validates the browser-parity invariant (`SameSite=None` without `Secure` throws), and passes through. Adoption is a one-line swap in the bundle bootstrap: `var session = require('gina').plugins.Session(require('express-session'))`. Existing bundles that don't adopt continue working exactly as before; adopting is explicit and visible in the diff. **The measurement step that surfaced the trap**: before writing any code, grep real bundle code for cookie flag patterns — `grep -nE 'httpOnly|sameSite|secure[\s:]' ~/Sites/<project>/src/*/index.js`. If any bundle has deliberate `httpOnly: false` or `sameSite` commented out, transparent wrapping is off the table and the feature must be opt-in. Precedent: #CSRF1 (2026-04-24, shipped in `0.3.7-alpha.8`) — initial design pivoted from per-request `response.setHeader` wrap to opt-in `gina.plugins.Session` plugin after the consumer-app measurement showed three bundles with deliberate `httpOnly: false`. The plugin shape also becomes the natural seam for #CSRF2 (signed double-submit token middleware, planned for `0.3.8`), which needs a session-aware injection point — session hardening and CSRF token plumbing share the same mount scope. #B206 (2026-08-02): the `bundle:add` boilerplate taught an impossible store selection — `expressSession.name = 'myRedis'` — but `Function.prototype.name` is READ-ONLY (silent no-op in sloppy mode, TypeError under strict), so the SessionStore factory ALWAYS resolves the literal `"session"` connectors.json entry and the template's `"myRedis"`/`"myDb"` example entries could never be found (boot throws `[SessionStore] Could not be loaded`). The template now shows one entry named `"session"` whose `connector` field selects the backend, and the four store factories' JSDoc no longer teaches the re-key; pinned by `test/core/boilerplate-session-template.test.js`.
|
|
809
809
|
|
|
810
810
|
80. **CSRF — three-phase protection (`#CSRF1` Session plugin / `#CSRF2` signed double-submit token / `#CSRF3` Origin pre-filter)** — Session plugin hardens `express-session` cookie defaults from `settings.json > session.cookie.{sameSite, httpOnly, secure}` (opt-in, one-line bundle bootstrap swap, never transparent because bundles legitimately set `httpOnly: false`); signed token middleware uses `crypto.timingSafeEqual` HMAC-SHA256 bound to `req.session.id` with per-route `routing.json > "csrfExempt": true` opt-out (see `routing-and-http2.md § "Per-route flags"` for `req.routing.csrfExempt` two-step propagation: `lib/routing` extracts → `core/server.js` hoists, top-level on `req.routing` not under `param.*`); Origin pre-filter folded INSIDE `gina.plugins.Csrf()` BEFORE token verify on mutating methods, allowlist via `settings.json > csrf.allowedOrigins` (empty defaults to `[bundleHostname]` auto-derived). Negative-invariant lock: matching token + mismatching Origin still 403s — token layer ≠ Origin layer. Implementation traps from the trilogy: plan-vs-shipped attribute drift (downstream commits must `grep -n '<attribute>' <upstream-source-file>` before referencing — source on develop wins, not the plan); CI flake-vs-regression triage (same-message-different-SHA pairs need `git diff --stat A..B` first); source-inspection `indexOf` matches the function DEFINITION before the call site (use unique assignment LHS like `requestOrigin = parseRequestOrigin(req)`); `Origin: "null"` is a real browser value (sandboxed iframes / `file://`) and needs an explicit `s === 'null'` guard so the parser falls through to Referer instead of 403'ing for "origin not allowed".
|
|
811
811
|
|
|
812
|
-
98. **Secrets — `${secret:KEY}` config placeholders + the `secrets:scan`/`secrets:check` introspection CLI (absorbs former #135).** `lib/secrets` substitutes `${secret:KEY}`
|
|
812
|
+
98. **Secrets — `${secret:KEY}` config placeholders + the `secrets:scan`/`secrets:check` introspection CLI (absorbs former #135).** `lib/secrets` substitutes `${secret:KEY}` at config-load (`core/config.js::loadBundleConfig`, post-merge per bundle), resolving each key from the FRAMEWORK environment first (`getEnvVar`) and falling back to `process.env[KEY]`. **That two-tier read is load-bearing, not defensive (0.6.3, #B156):** the CLI MOVES every `GINA_*`/`USER_*`/`VENDOR_*` key OUT of `process.env` into `process.gina` (`filterArgs`, `utils/helper.js`), so a `${secret:GINA_*}` placeholder was previously unresolvable in ANY CLI-loaded config — `mcp.json` (the published example), the connector commands, `audit:verify` — and failed closed with a refusing boot. Non-`GINA_` keys are untouched by the sweep and resolve as before. The same idiom now backs the CSRF `GINA_CSRF_SECRET` tier and the MCP transport's `GINA_BIND_HOST`/`GINA_DIR` reads (#B157). ANCHORED: the regex matches only when the ENTIRE string value is the placeholder — mixed strings (`"prefix-${secret:K}-suffix"`) pass through unchanged; non-string scalars untouched; nested objects/arrays descend. **Fail-closed:** an unset/empty env var throws a GENERIC `Secret resolution failed` — the key name rides only a non-enumerable `_ginaSecretKey`, surfaced at debug level by the config-load catch naming key + `<bundle>/<env>:<scope>` (#B42), so consumer-visible surfaces never carry key names. Secret rotation needs a process restart (the resolver re-runs on `config.refresh()`, but the env is inherited from container init). Pluggable backend interface (`{resolve(key)}` — only the env backend ships); `getResolvedPaths()` WeakMap path tracking for future log-redaction. **`getRequiredKeys(config)`** is the read-only, non-throwing sibling backing the OFFLINE CLI: `gina secrets:scan [<bundle>] [@project]` (which keys each bundle requires, grouped by file — sources derived from the manifest's `src`, never a guessed path) + `gina secrets:check` (marks SET/UNSET, exits non-zero — the CI gate), with `--scope=<s>` overlaying `config_<s>/` dirs READ-ONLY (the runtime loader stays scope-agnostic) and `--env-file` explicit per invocation. Design of record: the framework RESOLVES secrets, the deployment layer STORES them — no stored env-file default. Consumers: `Csrf` (`settings.csrf.secret`), `bundle:mcp-start` (re-resolves `mcp.json` post-parse), `auth.machine` caller keys.
|
|
813
813
|
|
|
814
814
|
125. **CLI handler authoring & operations — consolidated** (replaces individual entries #17, #23, #24, #55, #56, #58, #59, #60, #62, #63, #102, #112; plus #161, #166, #167, #181, #182, #183, #185, #199, #201, #202, #203 folded 2026-07-05):
|
|
815
815
|
- **CLI stubs** — `gina --status` / `gina -t` appear in help.txt or docs but have no handler (not in `aliases.json`); tracked in `ROADMAP.md § CLI`; never suggest to users without checking the handler file first. `bundle:status` / `project:status` / `minion:list` / `minion:kill` / `protocol:remove` / `bundle:copy` (+ `cp` alias) / `bundle:rename` shipped 0.4.1-alpha.2; `project:move` / `project:backup` / `project:restore` / `framework:update` / `framework:man` (+ `project:man` / `bundle:man` / `service:man`) — the **CLI Tier 3** finals — shipped 0.5.x (full coverage in the two CLI Tier 3 sub-bullets below). Both minion commands are run-dir-driven process-truth (the "minion" abstraction is half-wired — nothing sets `process.isMinion` or writes `*minion*.pid`, so a minion == any running bundle child-process): `minion:list` lists every live `<bundle>@<project>.pid` grouped by project via `lib.cmdStatusFormat`; `minion:kill @<project>` reaps them (hybrid kill-set = run-dir pidfiles + a `ps -ef | grep 'gina: ...@<project>'` sweep for pidfile-less orphans bundle:stop misses), SIGTERM→grace→SIGKILL escalation, `--dry-run` preview, unlinks stale/killed pidfiles, never touches mount symlinks or its own PID. `protocol:remove <bundle> @<project>` reverts a bundle to the project default protocol by deleting ONLY its `server.protocol/scheme/allowHTTP1` override from the bundle's `settings.json` (config.js `:1014/:1020` auto-defaults an absent protocol to `def_protocol`/`def_scheme`); it deliberately does NOT mutate the shared `ports*.json` — `project:add` pre-allocates the full protocol×scheme×env matrix, so the default-protocol port already exists and pruning the set's port would be wrong; a per-env port-presence guard refuses (unless `--force`) when the default-protocol port is missing; `--dry-run` preview, header-preserving JSON rewrite (connector:rm pattern). `bundle:copy <source> <new> @<project>` (+ `cp`) duplicates a bundle under a new name in the SAME project: copies the `src/<source>` tree, then word-boundary-rewrites the name footprint (PascalCase `<Src>`→`<Dst>` for controller class names + lowercase whole-word `<src>`→`<new>` for the gina require-var / `app.json` name / webroot path, `.js`/`.json` files only — embedded tokens like `apiClient` are untouched; a first-bundle webroot `/` is repointed to `/<new>`), allocates a fresh FULL protocol×scheme×env port matrix via the shared `setPorts` (a single-port insert would later emerg in `config.js`, which expects the complete matrix), and clones+repoints the source's manifest entry (`src`/`link`/`releases` target paths). `--dry-run` previews every rewrite site before writing; `--force` overwrites an existing target (its `removeDest` mirrors `bundle:remove`'s deletions). TWO positionals leave `self.name` null (CmdHelper sets it only for a single positional), so the handler reads `self.bundles[0]`/`[1]` directly — which also slips past the `cmd.name`-gated existence guard so the not-yet-registered new name isn't rejected. `bundle:rename <old> <new> @<project>` is the move-sibling — it renames a bundle IN PLACE in the same project (`fs.renameSync` move, NOT a copy) reusing the same `inc/name-rewrite.js` engine but with `fixWebroot:false` (rename moves the only bundle, so there's no first-bundle/collision case; a name-derived `/<old>` webroot is still rewritten by the lowercase pass). Its ports are REKEYED, not reallocated — port NUMBERS are preserved: `ports.json` rewrites the `<old>@<project>/` owner prefix back into the SAME `[protocol][scheme][portKey]` slot (avoiding the two `project/rename.js` bugs: the wrong `[protocol][portKey]` slot, and a project-wide owner replace), then `ports.reverse.json` is rekeyed (`pr[new]=pr[old]; delete pr[old]`) and flipped LAST as the canonical existence record. It REFUSES a running bundle with NO `--force` bypass (`--force` only overwrites an existing dest); the whole multi-surface mutation (symlink → renameSync dir → rewrite tree → env → manifest → ports → ports.reverse) is snapshot-guarded (the `bundle:add` rollback model) so any post-move failure reverses the dir move and restores env/manifest/ports/ports.reverse from in-memory snapshots. NOTE: the ROADMAP's "fix the help.txt remouve typo" item was stale — no such typo existed.
|
|
@@ -860,7 +860,7 @@ Dev-mode query instrumentation captures every database query tied to the current
|
|
|
860
860
|
- **Framework-level header-emission gate — closure-helper inside request-handler-factory scope cleanly wraps N object-literal `writeHead` headers blocks; use inline `if` for sibling `setHeader` sites** — pattern: define `var _setX = function(headers) { if (!options.X) { headers['Y'] = value; } return headers; }` once in the request-handler-factory scope (closing over `options`). Wrap each object-literal site via `var foo = _setX({...other keys...});`. DRY (N reads of `options.X` collapsed to 1 closure capture), single point of truth, headers var declaration shape preserved. For non-object-literal sibling sites (`response.setHeader(...)`) use inline `if (!options.X) { ... }` gate. Test pattern: 3-section (source-structure pins / pure-logic replica / docs cross-references). Precedent: #HDR8 Phase 2 `_setPoweredByHeader(headers)` inside `onPath`, 14 object-literal sites wrapped + 1 routing.json asset `setHeader` site gated inline.
|
|
861
861
|
- **Wrapper-into-namespace collapse — when a wrapper plugin's PascalCase JS name is derived from its parent namespace dir while the namespace dir already hosts per-feature siblings, the wrapper sub-dir is structurally redundant; collapse INTO the namespace dir to restore dir-name ↔ JS-name symmetry without flattening the siblings** — Node's CommonJS resolver natively handles the resulting shape (namespace dir becomes both a package AND parent of N child-packages). Workflow: 3 `git mv` calls + `rmdir wrapper/` + edit plugin-registry index + refresh test PLUGIN constant + registration-pin regex + JSDoc `@module` tag. **Critical gotcha**: the relative-require math is NOT "strip-N-prefixes-from-OLD-path" — it's recomputed from the file's NEW depth. The collapsed `src/main.js` is still INSIDE `src/`, so requires to siblings at the namespace root need `'../<sibling>/src/main.js'` (one `../`), NOT `'./<sibling>/src/main.js'` (which would resolve to `<namespace>/src/<sibling>/...` and crash with `MODULE_NOT_FOUND`). Verification: full local suite is invariant under a pure-rename refactor.
|
|
862
862
|
|
|
863
|
-
128. **Routing configuration (`routing.json` semantics) —
|
|
863
|
+
128. **Routing configuration (`routing.json` semantics) — 8 lessons consolidated** (replaces individual entries #19, #20, #27, #35, #36, #105-first, #106):
|
|
864
864
|
- **`namespace` is required to target a namespace controller** — omitting `"namespace": "content"` makes the router look in `controller.js` instead of `controller.content.js`. Error surface is `"control not found: list"` pointing at `controller.js`, which is confusing. Always set `namespace` when the action lives in a namespace controller file.
|
|
865
865
|
- **URL params need `"id": ":id"` in `param` to reach `req.params`** — declaring `"url": "/notes/:id"` alone is not enough. You MUST also add `"id": ":id"` inside the `param` block so the router binds the segment to `req.params.id`. `requirements` are optional (any non-empty segment is accepted when omitted); the `param` binding is not.
|
|
866
866
|
- **`fitsWithRequirements` requires `"slug": ":slug"` in `param` even when `requirements` is set** — the router only increments the route-match score (and populates `req.params`) for a URL parameter if that parameter key exists in the `param` block. Having `requirements: { "slug": "..." }` alone is not enough — the route 404s without the binding. The `requirements` block adds regex validation on top of the binding; it does not imply the binding.
|
|
@@ -868,18 +868,19 @@ Dev-mode query instrumentation captures every database query tied to the current
|
|
|
868
868
|
- **Routing loop cross-route contamination** — `fitsWithRequirements()` mutates `request.params` and `request[method]` during each route comparison. Leftover values from non-matching routes cause `parseRouting` ~line 419 to inject phantom URL segments, compounding work exponentially. Fix: `req.params = Object.assign({}, _origParams)` + restore `req[method]` from `_origReqMethod` (`req[method] = Object.assign({}, _origReqMethod)`, `server.js:4976`) before each `compareUrls()` — NEVER `delete req[method]` (that destroys the parsed POST/PUT body) and never `delete req.params` — `fitsWithRequirements` gates on `typeof(request.params) != 'undefined'`. Never use `JSON.clone` on request state in the routing loop — `Object.assign` is sufficient and safe.
|
|
869
869
|
- **`server.isaac.js:1954` initializes `req.params`** — `request.params = {}` and `request.params[0] = url` are set before `handle()` runs. Any code that deletes `req.params` breaks `fitsWithRequirements` which checks `typeof(request.params) != 'undefined'` before setting param values.
|
|
870
870
|
- **internal-bundle `setting-get-one-design` was a latent crash** — an internal bundle's routing had `"url": "/settings/get/design/:designId"` with no `requirements` block. Before the `fitsWithRequirements` fix, every GET to that URL threw a 500 TypeError. The fix makes it work correctly. Worth testing that route after the next ff-only merge.
|
|
871
|
+
- **`negotiate: true` opts a route into content negotiation, and is mutually exclusive with the render cache (#SPA1, 0.6.x).** A route declaring the top-level boolean `negotiate` (sibling of `method`/`url`/`cache`, NOT under `param` — the #CSRF2 trap) answers a request carrying `X-Gina-Navigate: fragment` with its LAYOUTLESS body (the same path `renderWithoutLayout()` uses) instead of the full page; any other header value, or none, renders exactly as before, so the value vocabulary can grow without breaking clients. The signal is deliberately a gina-namespaced header rather than `X-Requested-With` — popin, link and validator all already send the latter and the render + error paths already fork on it, so overloading it would change their behaviour. A negotiable route ALWAYS emits `Vary: X-Gina-Navigate` (appended to any existing `Vary`, never replacing it — it is a list header and the CORS paths set `vary: Origin`), including on responses that did not themselves vary: that is how a shared cache learns the URL varies at all. **The cache exclusion is not a limitation to be optimised away later without work** — the render-cache key (`[<token>:]<kind>:<bundle>:<url>`) carries NO shape dimension, and BOTH serve points run before the shape is resolved, so a stored entry could replay a fragment to a page request. Enforcement is at the WRITERS (`render-swig` + `render-nunjucks` `writeCache`, the same guard shape as #B158's gated-route refusal) and that placement is LOAD-BEARING, not defence-in-depth: isaac's cache read runs PRE-ROUTING (keyed off the raw `request.url`, no `request.routing` in scope), so it can never be guarded on the flag — keeping the URL out of the cache entirely is what protects that engine. The express-side read guard is the belt. Resolution lives in `controller.js this.render` before the delegate is chosen — NOT in `completeHeaders`, which runs before the params block builds `req.routing` and would therefore never fire. Undeclared ⇒ byte-identical: no new header, no behaviour change, no cache impact. **The client consumer of the flag is the opt-in `gina/nav` navigation module (#SPA1, 0.6.x, unreleased):** a page carrying a `data-gina-nav` element (the swap region AND the page's opt-in in ONE marker; the value `"false"` is reserved for the per-link opt-out and does not opt a page in — a page without the marker is byte-identical, nothing constructs) gets SPA-style navigation: a delegated document-level click listener resolves same-origin left-clicks (deferring to `data-gina-link*`, `data-gina-dialog*`/`data-gina-popin-*` triggers, `target`/`download` links, modified/middle clicks, in-page hash moves and `defaultPrevented`), matches the pathname against the served routing table with the server scan's own semantics — first match in table order wins, and interception happens ONLY when that match declares `negotiate: true` and accepts GET (a non-negotiable or DELETE-only first match BLOCKS, so the browser navigates normally; mixed literal+param segments and keys with neither a requirement nor a served param binding are conservatively unmatchable) — then fetches the fragment over a FRESH XMLHttpRequest per navigation carrying BOTH `X-Gina-Navigate: fragment` AND `X-Requested-With: XMLHttpRequest` (without the latter the render path re-wraps a layoutless body into an `<html>` shell), applying it ONLY when the 2xx HTML answer advertises `Vary: X-Gina-Navigate`; everything else — non-2xx, timeout, an un-negotiated answer (a client/server matching disagreement), a JSON `{location}` redirect — falls back to a normal full-page navigation. Post-swap: src-bearing scripts the document lacks are re-injected (inline scripts are NOT executed — the same contract popin content has), id-bearing forms are rebound through the live validator, an open popin is closed (a full navigation would have unloaded it), an optional `[data-gina-nav-title]` attribute in the fragment sets `document.title`, and history/scroll/focus are handled (pushState + popstate re-fetch, manual scroll restoration, focus on the region). Programmatic surface: `gina.nav.navigate(url)` and `gina.nav.matchUrl(pathname)`; the routing table is read lazily per navigation (it arrives from an async fetch and can land after `isFrameworkLoaded`). **Extends-template fragments (the block-preserving fragment parent, shipped with the module):** a negotiated layoutless render of an `{% extends %}` template renders the child's blocks — the layoutless path targets its own `fragments/` namespace under the layout cache, primed from the child's OWN block roster (declaration order, deduped) with the shell tail (xhr inputs / scripts) composed after the placeholders, and the swig compiled-template cache key carries a `:fragment` shape suffix so fragment and full-page compiles never share a slot in cached mode; the shared full-page cached layout is never touched by a fragment render (this also removed a dev cross-request poison window where one fragment request blanked the parent every full-page render compiles from). Full-page renders are byte-identical; a popin pointed at an extends-template route changes from empty to content. An extends-less template still auto-renders layoutless and cannot serve the full-page shape. ⚠️ On the nunjucks engine this class is NOT healed: the layoutless flag there filters assets only and native extends resolves the real layout, so a negotiated fragment answers the FULL page — do not ship `negotiate: true` on nunjucks extends routes (tracked separately; opt-in engine).
|
|
871
872
|
|
|
872
873
|
142. **`bin/cli` resolves the framework dir from `GINA_VERSION` (env/persisted) falling back to `package.json` version, then `require()`s `framework/v<version>/lib/generator` — now guarded by `fs.existsSync(frameworkPath)` BEFORE that require.** A `GINA_VERSION` pointing at a non-installed version (a stale pin, or a bind-mounted dev tree at a different version) otherwise throws `MODULE_NOT_FOUND` that the surrounding `try/catch` mislabels as `gina: could not load [ package.json ]` (package.json actually loaded fine), and the legitimate `existsSync` guard sits AFTER the require so it never fires — every CLI command then fails opaquely (in a container, `project:import` silently fails, so bundles report "not registered" on start). The guard now fails fast with a clear `gina framework:add <version>` message via `process.stderr.write` (`console` is not reassigned to the framework logger until later, so `console.alert` is undefined there). Diagnostic tell: `could not load package.json` followed by `Cannot find module '…/framework/v<X>/lib/generator'` means framework version `<X>` is not installed, not that package.json is broken. (`8fa9c278`, 2026-05-30) **Sibling (2026-07-03) — same guard-before-dynamic-require rule, applied to command DISPATCH.** `lib/cmd/framework/init.js` `run()` — the single chokepoint that `require()`s `/cmd/<topic>/<action>.js` for BOTH offline framework AND online bundle commands — now `fs.existsSync`-guards the handler path BEFORE requiring it. A known group (listed in `bin/cli`'s `allowedOffline`) with an UNKNOWN ACTION previously threw `Cannot find module .../cmd/<group>/<action>.js`, dumped as a raw stack by `run()`'s catch: `gina framework:connector` (connector is its OWN top-level group), `gina connector` / `gina connector --help` (auto-prefixed to the same `framework:connector` by `bin/cli:373`), and `gina -V` (→ `framework:-V`; only lowercase `-v` and `--version` are aliased) all hit it. The guard now prints a clean `'<group>:<action>' is not a valid command.` + a `gina help` / `gina help <group>` pointer, plus a did-you-mean when the action names a real command group (gated on the SAME `/^[a-z][a-z0-9-]*$/` + `<action>/help.txt`-exists test `gina help <group>` uses), exits 1, and mirrors the message to `opt.client` on the socket path; the try/catch still keeps the stack for errors thrown INSIDE a resolved handler (real crash vs. typo cleanly split by the `existsSync` check). `bin/cli:426-432` already rejected an unknown GROUP cleanly — this closes the known-group/unknown-action gap. Rule: `fs.existsSync`-guard every dynamic handler require and emit a clean unknown-command message; reserve the stack for errors from inside a resolved handler. Tests `test/lib/cmd-unknown-command.test.js`; server-side (no dist rebuild).
|
|
873
874
|
|
|
874
875
|
151. **`templates.json` pre-process pass in `core/config.js` (right after the `hasViews` line, BEFORE the routing↔template GET auto-vivify) expands two additive section-key shapes once per bundle — gina-io/gina#8 comma-separated keys + #10 `_common.config`.** #8: a comma-separated section key (`"a, b": {…}`) is split on `/\s*,\s*/` (each name `.trim()`med, empty segments skipped) and the block is replicated under each named section, MERGING into any section that already exists so a section's own keys win — `merge(existing, JSON.clone(block))`, and `lib/merge` keeps its FIRST argument on a leaf collision (`override=false` default). #10: an optional `_common.config` block is `merge(_common, _common.config)`-flattened back into `_common` then deleted, so the existing `_common.*` read sites are unchanged and a direct `_common.X` overrides `_common.config.X`. **Placement is load-bearing:** the pass must run before the GET auto-vivify `files['templates'][rule.toLowerCase()] = {}` (which keys off CLEAN route names from routing.json) — otherwise a comma key leaves the real route names "missing" → empty `{}` sections get minted AND the comma key survives to produce a dead `"a, b@bundle"` route. **Both are no-ops when absent** (no comma → single-element split → identical; no `_common.config` → untouched), so existing bundles are byte-identical — verified zero comma keys across known consumer + gina fixture `templates.json` before shipping. **#7 (a Swig-like `{ "inherit": … }` directive) was closed un-shipped**, so #8 is NOT redundant: `_common` shares to ALL routes, #8 shares a chosen SUBSET — a gap nothing else fills. Collect-then-mutate (gather comma keys first) avoids changing the object mid-`for…in`. Tests: `test/core/config-templates-preprocess.test.js` (source pins incl. placement-before-auto-vivify + a real-`merge` pure-logic replica: split / union-merge / own-keys-win / trim / empty-segment / flatten / no-op). Established 2026-06-05.
|
|
875
876
|
|
|
876
|
-
174. **`throwError` — call signatures, status-code preservation, render-error interception, and fail-closed error-response `stack` hygiene** (replaces individual entries #10, #21, #26, #105, #110, #132) — `self.throwError` accepts four shapes: `(errorObj|Error)` 1-arg; `(code, Error|string)` 2-arg, dispatched via a function-top normalization shift that preserves the explicit code (`throwError(404, new Error('not found'))` sends 404 — earlier releases fell back to 500, and the `new Error(...)` wrapping workaround from that era still works unchanged); `(code, errorObj)` 2-arg, intentionally NOT shifted — it flows through the `arguments.length < 3` branch whose `code = res || 500` reaches the explicit code, and including errorObj in the shift detection would break exactly that case; `(res, code, string|Error)` 3-arg (explicit code preserved). Always `return self.throwError(...)` immediately in a controller action — it is a terminal response, and any later response-API call runs against a released response (guarded to warn + no-op since 0.5.1-alpha.2 instead of crashing the bundle). Calling `self.render(err)` with a non-2xx `data.page.data.status` and a defined `data.page.data.error` is intercepted before template rendering and routed through `throwError` automatically; object-valued `error`/`message` fields are normalised to strings first (no `[object Object]`), and the same normalized string feeds the server-side `console.error('[render] ...')` log, so wire and log carry identical readable text. Error responses are scope-gated FAIL-CLOSED: unless `NODE_SCOPE_IS_LOCAL` is explicitly `true`, the server-side `stack` is stripped from BOTH the JSON error body AND the fallback HTML error page's `<pre class="stack">` block — the gate is strip-unless-local, not strip-only-if-prod, so an unset scope on a fresh deployment still strips and internals (file paths, frames, library versions) never leak; local scope keeps the stack on the wire for the dev toolbar's data-xhr panel. Custom error templates remain consumer-owned (a view rendering the error object's `stack` is the consumer's call), and passing `err.stack` as the message argument surfaces it in the un-gated `error` STRING — sanitize that at the call site. **The sibling `renderJSON()` status surface had an HTTP/2-only hole (#B172, fixed 2026-07-30): the `status` key in the payload correctly resolves onto `response.statusCode`, but the HTTP/2 body path hand-builds its header frame for the raw `stream.respond()` and hardcoded `':status': 200` — and the pending-header merge below can never supply the real code, since `setHeader(':status', …)` throws ERR_HTTP2_PSEUDOHEADER_NOT_ALLOWED — so every `renderJSON({status: 4xx})` on a genuine HTTP/2 stream was served as 200 with the error payload in the body (HTTP/1.1, the HEAD branch, and the HTML delegates were already correct). Fixed to `':status': response.statusCode || 200`, matching the HEAD branch. The `errno` half of the status branch is now guarded like the swig/v1 delegates (enter only when `statusCodes[jsonObj.status]` is defined): an `errno`-only payload used to assign `statusCode = undefined`, which HTTP/1.1's `response.end()` rejects with the throw swallowed by an empty catch (the response was never sent — the client hangs) and HTTP/2's compat setter rejects at assignment (→ 500); guarded, such a payload is served as a normal 200 with the payload in the body — measured wire-identical to dropping the errno clause on every input. Rule: any hand-built HTTP/2 header frame must carry `response.statusCode || 200`, never a literal — the compat layer's pending-header merge structurally cannot repair a pseudo-header. Tests: `render-json.test.js §07-§08` (extract-and-execute on the real branch bytes).** **Body field semantics — which key carries the human sentence (docs-corrected 2026-07-30): on a known status code `error` carries the STATUS TEXT (`statusCodes[code]`) and the caller's text lands in `message` (the Error argument's `.message`, or the trailing string), so a client wanting the sentence reads `message`, NOT `error`; only for an unknown status code (warned) can the caller's text land in `error` instead. The sibling `ApiError` + `renderJSON` path cannot carry a message on the wire AT ALL: the server-error forms (`new ApiError(msg)`, `new ApiError(msg, code)`) return a real `Error` whose `message` is a non-enumerable own property that `renderJSON`'s single `JSON.stringify` skips (readable server-side, absent from the body — and reassigning `e.message` does NOT make it enumerable, only `defineProperty` would), while the field forms (`new ApiError(msg, field)`, `new ApiError(msg, field, code)`, the array form) return a PLAIN OBJECT built by `merge({tag,fields,path}, e)`, which copies enumerable properties only — so `message` is absent outright there and the text travels in `fields[field]` (those bodies also carry `tag` + `path`). To place a sentence in the body use `throwError` or hand-build the payload.**
|
|
877
|
+
174. **`throwError` — call signatures, status-code preservation, render-error interception, and fail-closed error-response `stack` hygiene** (replaces individual entries #10, #21, #26, #105, #110, #132) — `self.throwError` accepts four shapes: `(errorObj|Error)` 1-arg; `(code, Error|string)` 2-arg, dispatched via a function-top normalization shift that preserves the explicit code (`throwError(404, new Error('not found'))` sends 404 — earlier releases fell back to 500, and the `new Error(...)` wrapping workaround from that era still works unchanged); `(code, errorObj)` 2-arg, intentionally NOT shifted — it flows through the `arguments.length < 3` branch whose `code = res || 500` reaches the explicit code, and including errorObj in the shift detection would break exactly that case; `(res, code, string|Error)` 3-arg (explicit code preserved). Always `return self.throwError(...)` immediately in a controller action — it is a terminal response, and any later response-API call runs against a released response (guarded to warn + no-op since 0.5.1-alpha.2 instead of crashing the bundle). Calling `self.render(err)` with a non-2xx `data.page.data.status` and a defined `data.page.data.error` is intercepted before template rendering and routed through `throwError` automatically; object-valued `error`/`message` fields are normalised to strings first (no `[object Object]`), and the same normalized string feeds the server-side `console.error('[render] ...')` log, so wire and log carry identical readable text. Error responses are scope-gated FAIL-CLOSED: unless `NODE_SCOPE_IS_LOCAL` is explicitly `true`, the server-side `stack` is stripped from BOTH the JSON error body AND the fallback HTML error page's `<pre class="stack">` block — the gate is strip-unless-local, not strip-only-if-prod, so an unset scope on a fresh deployment still strips and internals (file paths, frames, library versions) never leak; local scope keeps the stack on the wire for the dev toolbar's data-xhr panel. Custom error templates remain consumer-owned (a view rendering the error object's `stack` is the consumer's call), and passing `err.stack` as the message argument surfaces it in the un-gated `error` STRING — sanitize that at the call site. **The sibling `renderJSON()` status surface had an HTTP/2-only hole (#B172, fixed 2026-07-30): the `status` key in the payload correctly resolves onto `response.statusCode`, but the HTTP/2 body path hand-builds its header frame for the raw `stream.respond()` and hardcoded `':status': 200` — and the pending-header merge below can never supply the real code, since `setHeader(':status', …)` throws ERR_HTTP2_PSEUDOHEADER_NOT_ALLOWED — so every `renderJSON({status: 4xx})` on a genuine HTTP/2 stream was served as 200 with the error payload in the body (HTTP/1.1, the HEAD branch, and the HTML delegates were already correct). Fixed to `':status': response.statusCode || 200`, matching the HEAD branch. The `errno` half of the status branch is now guarded like the swig/v1 delegates (enter only when `statusCodes[jsonObj.status]` is defined): an `errno`-only payload used to assign `statusCode = undefined`, which HTTP/1.1's `response.end()` rejects with the throw swallowed by an empty catch (the response was never sent — the client hangs) and HTTP/2's compat setter rejects at assignment (→ 500); guarded, such a payload is served as a normal 200 with the payload in the body — measured wire-identical to dropping the errno clause on every input. Rule: any hand-built HTTP/2 header frame must carry `response.statusCode || 200`, never a literal — the compat layer's pending-header merge structurally cannot repair a pseudo-header. Tests: `render-json.test.js §07-§08` (extract-and-execute on the real branch bytes).** **Body field semantics — which key carries the human sentence (docs-corrected 2026-07-30): on a known status code `error` carries the STATUS TEXT (`statusCodes[code]`) and the caller's text lands in `message` (the Error argument's `.message`, or the trailing string), so a client wanting the sentence reads `message`, NOT `error`; only for an unknown status code (warned) can the caller's text land in `error` instead. The sibling `ApiError` + `renderJSON` path cannot carry a message on the wire AT ALL: the server-error forms (`new ApiError(msg)`, `new ApiError(msg, code)`) return a real `Error` whose `message` is a non-enumerable own property that `renderJSON`'s single `JSON.stringify` skips (readable server-side, absent from the body — and reassigning `e.message` does NOT make it enumerable, only `defineProperty` would), while the field forms (`new ApiError(msg, field)`, `new ApiError(msg, field, code)`, the array form) return a PLAIN OBJECT built by `merge({tag,fields,path}, e)`, which copies enumerable properties only — so `message` is absent outright there and the text travels in `fields[field]` (those bodies also carry `tag` + `path`). To place a sentence in the body use `throwError` or hand-build the payload.** **The custom-error-PAGE path had the same inert-status class (#B190, fixed 2026-08-01): `renderCustomError` — the `req.routing.param.error` / `errorFiles` path — never set the response status; the swig and v1 delegates recompute it downstream from `data.page.data.status`, but the nunjucks and both async delegates only read the already-set `res.statusCode || 200` at their write sites, so a configured custom error page was served as HTTP 200 there (live-measured on the same fixture: nunjucks 200 vs swig 500 pre-fix; nunjucks 500 post-fix). Fixed with a guarded stamp in `renderCustomError` immediately before the render dispatch — `res.statusCode = data.status`, gated on `!res.headersSent` + `statusCodes[data.status]` membership, mirroring the swig delegate's own downstream semantics — so every delegate serves the configured page with its real status, and a transient-failure 503 upgrade (#CE1) rides this path with a meaningful `Retry-After`. Tests: `test/core/controller-custom-error-status.test.js` (source pins + behavioral drive of the real bytes via `createTestInstance` with a stubbed `render()`; red-first).** **The same path could DISCARD its resolved template outright (#B191, fixed 2026-08-01, consumer-reported against 0.6.0): `renderCustomError` built its `errOptions` only under the `isLocalOptionResetNeeded` flag, but the server-side throwError twin sets `param.file` WITHOUT that flag (and the flag rode a shared routing object the dispatch mutated per error), so a falsy read left `errOptions` null and the swig delegate fell back to the FAILING route's own `file` — a bare, un-rooted `<file>.html` ENOENT plus a "check the following rule in your routing.json" dump naming a rule that was correct by construction (an upstream outage misread as a routing misconfiguration; the built-in fallback page still served, so end users saw an error page throughout). Fixed threefold: the resolved `param.file` now reaches `errOptions` unconditionally (with `path: null` so the namespace stays ignored — heals nunjucks too, which read the same `localOptions.file`); the controller-side dispatch works on a `JSON.clone` of the injected route instead of mutating `content.routing[<rule>]` (lib/routing `getRoute()` already clones for the server twin, which is also why a clean router dispatch self-rescued); and render-swig's template-not-found diagnostic, when rendering a custom error page, names the custom error template and hands off to throwError's re-entry guard (built-in page, no loop) — never the routing-rule dump. Rule: anything the error dispatch resolves must not depend on optional dispatch flags, and the dispatch must never mutate shared routing state. Tests: `test/core/controller-custom-error-options.test.js` (source pins + real-bytes behavioral for BOTH the file channel and the shared-config non-mutation; red-first validated on pre-fix bytes in a detached worktree).**
|
|
877
878
|
|
|
878
879
|
175. **Dev-mode hot-reload & module lifecycle — what reloads, what doesn't, and the `require.cache` poisoning antipattern** (replaces individual entries #22, #25, #28, #34, #104) — in dev mode (`NODE_ENV_IS_DEV` set; `isCacheless()` true) the framework hot-reloads code via two functions with DIFFERENT triggers: `refreshCoreDependencies()` (`core/router.js`) evicts and re-requires the controller pair ONLY when the dev watcher has marked a watched file dirty (`__hotReload` flags; watched: `controller.js`, `controller.render-swig.js`, and the bundle's `controllers/` directory; falls back to per-request eviction when the watcher context is absent), while `refreshCore()` (`core/server.isaac.js`, isaac engine only) re-exports core-path modules and re-requires `lib/index.js` + `plugins/index.js` on EVERY request. Consequence for controllers: module-level state (`var store = {}`) resets whenever a watched file changes (not per request) — for in-process state that survives hot-reloads (but resets on `bundle:restart`) attach to `global` (`if (!global.__myStore) global.__myStore = {}; var store = global.__myStore;`); for durable state use a database or file. NOT hot-reloaded (a full `gina bundle:stop` + `bundle:start` or `docker restart` is required): `server.js` / `server.isaac.js` / `server.express.js` (loaded once at process start, never evicted); connector code (`core/connectors/*/index.js`, loaded once via entity registration, outside the refresh scope); and bundle-registered plugin middleware — the `onInitialize` → `app.use(gina.plugins.X(...))` factories run ONCE at bootstrap, so a plugin config change needs a bundle restart in BOTH dev and prod, despite the misleading per-request refresh cue. Correctness invariant for any eviction code: `require.cache[path]` must hold a `Module` instance — `require.cache[path] = require(path)` poisons the slot by storing the bare exports object (no `.exports` key), so the next plain `require()` of that path returns `undefined`, surfacing as `Cannot read properties of undefined (reading '<X>')` after a hot reload; use `delete require.cache[require.resolve(path)]` + the `require()` return value, or swap `require.cache[c].exports = require(path)` on the existing Module — never the bare assignment. **The eviction cycles also leaked the whole module graph (#B32, folds former #173):** Node pushes every cache-miss require's fresh Module onto the REQUIRING module's `children` array and dedupes only on cache hits, so the per-request delete-and-re-require cycles accumulated one dead Module per eviction on long-lived parents — each pinning its entire evaluated exports graph (~1.8 MB post-GC live heap per request on a minimal dev bundle; heap-limit OOM/SIGABRT at ~2400 requests, presenting upstream as HTTP/2 PING timeouts, then ECONNREFUSED, then a supervisor respawn loop that keeps every process cold). Fixed by a `pruneDeadModuleChildren()` sweep at the end of BOTH eviction cycles — `children` is diagnostic metadata (nothing in Node resolution reads it), so pruning never unloads a module still referenced elsewhere; prod was never affected (no eviction + cache-hit dedup). The sweep walks `require.cache` keys ONLY, so a second residual of the same class existed OFF-cache: hot-evicted leaf/singleton libs captured at gen-0 by load-once modules and re-required per request pushed dead children onto the evicted-but-retained gen-0 parent, prune-blind — those libs are plain-`require`d now (never evicted → cache-hit → children deduped), and a completeness audit closed the gen-0-binding class. Rules: any new delete-`require.cache` + re-require cycle in a long-lived process must end with the prune sweep; a hot-evicted lib captured as a gen-0 binding by a load-once module leaks PAST the prune — plain-require leaf/singleton libs that don't need hot reload.
|
|
879
880
|
|
|
880
|
-
176. **Inspector & `/_gina/*` built-in endpoints — in-process architecture, admin IP-allowlist, agent-stream auth, live index coverage** (replaces individual entries #31, #32, #38, #111, #134, #138) — the dev Inspector (formerly Beemaster) is a built-in SPA served at `/_gina/inspector/` inside the bundle's own process: no project registration, no separate port, no auto-start spawn; dev-mode only (production bundles never expose it), and same-origin with the monitored bundle so `window.opener.__ginaData` always works. Every `/_gina/*` route (healthcheck, assets, cache/stats, info, inspector, logs, agent, indexes, reveal, instrument, metrics) is a handler in the same HTTP server process; the Isaac engine is the source of truth and may carry fast-paths, but base functionality belongs in the engine-agnostic dispatcher so Express bundles get the same endpoints. Admin-grade endpoints exposing process/cache internals (`/_gina/info` — memory/uptime/version/HTTP-2 session counters; `/_gina/cache/stats` — full cache contents) are IP-allowlisted via the `admin.allowFrom` block in `app.json`: the client IP is read from the socket only (the spoofable `X-Forwarded-For` is never trusted), `::ffff:`-mapped IPv4 is normalised, the list defaults to loopback (`127.0.0.1`, `::1`) when omitted, an empty list denies everyone, and denied callers get a 403 JSON error; `/_gina/health/check` stays deliberately open for liveness probes, and `/_gina/metrics` keeps its own separate `metrics.allowFrom` axis. The `/_gina/agent` stream (combined data + log events) is dev-only by default but can be enabled outside dev behind an API key (`settings.json > inspector.agent.{enabled, key}`, `${secret:KEY}`-capable, constant-time compared, fail-closed when no key is configured); browsers pass `?key=` as a query param because `EventSource` and WebSocket handshakes cannot set custom headers — and any `$`-anchored endpoint gate regex must become `(?:\?|$)` the moment its endpoint accepts a query param, or the handler silently stops matching the query'd URL. A time-boxed, separately-keyed production instrumentation window (`POST /_gina/instrument`, hard-capped at one hour) can stream per-request query + flow capture over that authenticated channel — channel AUTH, not redaction, is what protects raw query text (redaction masks only secret-NAMED fields, never the statement or its positional params). The Query tab computes live index coverage client-side, so bundles WITHOUT an `indexes.sql` get a correct "no index for filter" badge on the first render too (cached live-index descriptors are cloned per query before stamping coverage — the cache is shared across queries that filter different columns). Inspector toolbar CSS: native macOS `<select>` ignores `line-height` — use explicit vertical padding, and keep `select` (sans font) and `input` (mono font) on separate CSS rules. **SPA + statusbar hardening (folds former #165/#190/#191/#196):** every per-bundle `/_gina/*` consumer in the SPA derives its base URL via the shared 3-fallback `resolveBundleBase()` (`?target=` → opener pathname → path strip) — a bare `window.location.pathname` strip misroutes to the proxy's default bundle in reverse-proxy multi-bundle setups. The Inspector binds to its opener tab via a per-tab `BroadcastChannel` (`?ch=<tabId>`): pages sending COOP `same-origin` sever `window.opener` for the popup, and the bundle-global fallbacks (the shared localStorage slot + the worker-wide agent SSE) reflect whichever render last touched them — a diagnostic channel that must track ONE page needs a per-tab transport (the statusbar publishes that tab's data at its existing write points and answers a request/reply handshake; no `?ch=` keeps the prior behaviour, and `?target=` agent mode keeps priority). Structured-localStorage reads need a SHAPE check on top of the try/catch parse (a tampered key holding a JSON primitive otherwise breaks the un-guarded consumer), and every value interpolated into `innerHTML` goes through an HTML-escape helper — untrusted model/app text renders via `.textContent` instead. Dev inline-script splices before `</body>` use FUNCTION replacers: `String.replace(/re/, str)` expands `$`-sequences in a STRING replacement (`` $` ``/`$'`/`$&`), so dynamic content carrying a stray dollar-sequence spliced the whole document into the statusbar `<script>` (SyntaxError → statusbar and launch link vanish) on content-heavy pages while a near-empty smoke page stayed falsely green — any `String.replace(re, dynamicX)` with dynamic content must use a function replacer or escape `$`. Inspector SPA files are copied VERBATIM to dist (no minification) — an inspector-only edit rebuilds only those dist files, never the main bundle artifacts.
|
|
881
|
+
176. **Inspector & `/_gina/*` built-in endpoints — in-process architecture, admin IP-allowlist, agent-stream auth, live index coverage** (replaces individual entries #31, #32, #38, #111, #134, #138) — the dev Inspector (formerly Beemaster) is a built-in SPA served at `/_gina/inspector/` inside the bundle's own process: no project registration, no separate port, no auto-start spawn; dev-mode only (production bundles never expose it), and same-origin with the monitored bundle so `window.opener.__ginaData` always works. Every `/_gina/*` route (healthcheck, assets, cache/stats, info, inspector, logs, agent, indexes, reveal, instrument, metrics) is a handler in the same HTTP server process; the Isaac engine is the source of truth and may carry fast-paths, but base functionality belongs in the engine-agnostic dispatcher so Express bundles get the same endpoints. Admin-grade endpoints exposing process/cache internals (`/_gina/info` — memory/uptime/version/HTTP-2 session counters; `/_gina/cache/stats` — full cache contents) are IP-allowlisted via the `admin.allowFrom` block in `app.json`: the client IP is read from the socket only (the spoofable `X-Forwarded-For` is never trusted), `::ffff:`-mapped IPv4 is normalised, the list defaults to loopback (`127.0.0.1`, `::1`) when omitted, an empty list denies everyone, and denied callers get a 403 JSON error; `/_gina/health/check` stays deliberately open for liveness probes, and `/_gina/metrics` keeps its own separate `metrics.allowFrom` axis. The `/_gina/agent` stream (combined data + log events) is dev-only by default but can be enabled outside dev behind an API key (`settings.json > inspector.agent.{enabled, key}`, `${secret:KEY}`-capable, constant-time compared, fail-closed when no key is configured); browsers pass `?key=` as a query param because `EventSource` and WebSocket handshakes cannot set custom headers — and any `$`-anchored endpoint gate regex must become `(?:\?|$)` the moment its endpoint accepts a query param, or the handler silently stops matching the query'd URL. A time-boxed, separately-keyed production instrumentation window (`POST /_gina/instrument`, hard-capped at one hour) can stream per-request query + flow capture over that authenticated channel — channel AUTH, not redaction, is what protects raw query text (redaction masks only secret-NAMED fields, never the statement or its positional params). The Query tab computes live index coverage client-side, so bundles WITHOUT an `indexes.sql` get a correct "no index for filter" badge on the first render too (cached live-index descriptors are cloned per query before stamping coverage — the cache is shared across queries that filter different columns). The live-index refetch's success re-render lands in `#tree-query` — the container the tab renderer owns and silently bails on when missing — never the scroll wrapper around it (#B222: replacing the wrapper's children destroyed `#tree-query`, so after the first render carrying an `indexes: null` query every later payload — navigation, XHR, the refresh button — froze the Query pane and badge until the Inspector window itself was reloaded; the ⟳ button re-armed the freeze via its forced refetch). Inspector toolbar CSS: native macOS `<select>` ignores `line-height` — use explicit vertical padding, and keep `select` (sans font) and `input` (mono font) on separate CSS rules. **SPA + statusbar hardening (folds former #165/#190/#191/#196):** every per-bundle `/_gina/*` consumer in the SPA derives its base URL via the shared 3-fallback `resolveBundleBase()` (`?target=` → opener pathname → path strip) — a bare `window.location.pathname` strip misroutes to the proxy's default bundle in reverse-proxy multi-bundle setups. The Inspector binds to its opener tab via a per-tab `BroadcastChannel` (`?ch=<tabId>`): pages sending COOP `same-origin` sever `window.opener` for the popup, and the bundle-global fallbacks (the shared localStorage slot + the worker-wide agent SSE) reflect whichever render last touched them — a diagnostic channel that must track ONE page needs a per-tab transport (the statusbar publishes that tab's data at its existing write points and answers a request/reply handshake; no `?ch=` keeps the prior behaviour, and `?target=` agent mode keeps priority). Structured-localStorage reads need a SHAPE check on top of the try/catch parse (a tampered key holding a JSON primitive otherwise breaks the un-guarded consumer), and every value interpolated into `innerHTML` goes through an HTML-escape helper — untrusted model/app text renders via `.textContent` instead. Dev inline-script splices before `</body>` use FUNCTION replacers: `String.replace(/re/, str)` expands `$`-sequences in a STRING replacement (`` $` ``/`$'`/`$&`), so dynamic content carrying a stray dollar-sequence spliced the whole document into the statusbar `<script>` (SyntaxError → statusbar and launch link vanish) on content-heavy pages while a near-empty smoke page stayed falsely green — any `String.replace(re, dynamicX)` with dynamic content must use a function replacer or escape `$`. Inspector SPA files are copied VERBATIM to dist (no minification) — an inspector-only edit rebuilds only those dist files, never the main bundle artifacts. Bound-window data fidelity (#B225): a preload-consumed popin open sets the dev toolbar's XHR overlay on BOTH consume branches exactly like a cold click (dev-gated `updateToolbar(body)` immediately before each dispatch — in consumePreload, never in the shared dispatcher, which would double-fire the cold path); the statusbar-bound Inspector window's refresh button never reopens the passive `/_gina/agent` stream (`source !== 'broadcast'` on the reopen gate — bound mode takes page-scoped data from the statusbar publisher, and a leaked stream applied bundle-wide payloads over it, blinking the Query badge at every XHR overlay); and every Query-pane re-render (filter/search/show-all plus the live-index refetch success path) derives from the live payload via renderTab('query')'s own preference (reveal swap included) instead of the module query cache, which the empty render path never clears and which survives overlay eras ending while the Query tab is inactive — a refresh no longer resurrects a closed popin's badge count. A `?ch=`-less embedded Inspector (direct URL, bookmark, a window predating bound mode) no longer silently runs those bundle-global channels (#B231): every dev page's statusbar advertises its per-tab channel id in localStorage (`__gina_last_tab_ch`) on each publish, a `?ch=`-less Inspector adopts the most-recently-published tab's channel at boot behind a liveness handshake (the bind's request must be answered by a data frame within 1.5 s — a stale advert from a closed tab tears the bound mode down and falls back to the legacy acquisition, closing the bound-mode `/_gina/logs` stream so log entries are not double-delivered once the passive agent stream attaches), and a footer badge names the active data-source mode (`bound` / `agent` / warn-tinted `global` with a tooltip pointing at the statusbar link) so a degraded bundle-global mode is visible instead of silent. The footer memory gauge sits its unfilled track on `--bg3` with a theme-scoped inset groove shadow (#B237) — the track previously shared `.bm-footer`'s own `--bg2` background token, so the empty portion rendered invisible in both themes and a low fill read as a floating green dot; a gauge track must contrast the surface it sits on, and an inset box-shadow paints under the fill child, so the groove shades only the empty region.
|
|
881
882
|
|
|
882
|
-
177. **Couchbase connector — install-derived SDK resolution (v2 removed), `connectors.json` semantics, `getCluster()`, dev-mode index reporting** (replaces individual entries #29, #40, #41, #57, #136, #153) — connectors are keyed in `schema/connectors.json` by LOGICAL name (`primary`, `sessionStore`, `cache`, …) with the driver selected by the `connector` enum field (`couchbase`/`mysql`/`postgresql`/`sqlite`/`redis`/`ai`/…) — never introduce a separate `driver` field; the optional `version` field carries a semver range used by `connector:add --driver-version=…` for the npm-install hint. The Couchbase SDK major is derived from the project's INSTALLED `couchbase` npm version — the leading major of `dependencies.couchbase` selects `connector.v<major>.js`, which stamps `conn.sdk = { version: N }` — never from a config key, so migrating SDK majors is a driver bump (`npm install couchbase@^4`), not a config edit. SDK v2 is REMOVED as of 0.4.0: the resolver now throws a clear "SDK v2 is no longer supported — upgrade couchbase@^3/^4" error when the installed major is ≤ 2 or the connector file is missing (previously a silent fallback that crashed later with an opaque MODULE_NOT_FOUND); the v3-vs-v4 split remains for param shaping. Generalises: when a connector's behavior-version derives from an installed dependency rather than config, fail fast once the installed major drops below the supported floor. Couchbase entities expose a public `getCluster()` (on both the model-entity and N1QL-entity prototypes) returning the underlying SDK `Cluster` handle for features the ORM doesn't wrap — chiefly multi-document ACID transactions (`cluster.transactions().run(...)`, needs SDK 3.2+/4.x) — without touching private `_*` internals; it throws a coded `GINA_COUCHBASE_CLUSTER_UNRESOLVED` error when neither connection shape resolves. Dev-mode index reporting: the SDK v4 C++ binding never populates `meta.profile` despite `profile: 'timings'` being sent (confirmed on v4.6.0), so an async `EXPLAIN <statement>` fallback with a per-process per-statement cache supplies the plan instead (the first request for a new statement may show N/A; subsequent requests hit the cache), and `USE KEYS` plans surface as "KV lookup" via `ExpressionScan`/`KeyScan` operator detection.
|
|
883
|
+
177. **Couchbase connector — install-derived SDK resolution (v2 removed), `connectors.json` semantics, `getCluster()`, dev-mode index reporting** (replaces individual entries #29, #40, #41, #57, #136, #153) — connectors are keyed in `schema/connectors.json` by LOGICAL name (`primary`, `sessionStore`, `cache`, …) with the driver selected by the `connector` enum field (`couchbase`/`mysql`/`postgresql`/`sqlite`/`redis`/`ai`/…) — never introduce a separate `driver` field; the optional `version` field carries a semver range used by `connector:add --driver-version=…` for the npm-install hint. The Couchbase SDK major is derived from the project's INSTALLED `couchbase` npm version — the leading major of `dependencies.couchbase` selects `connector.v<major>.js`, which stamps `conn.sdk = { version: N }` — never from a config key, so migrating SDK majors is a driver bump (`npm install couchbase@^4`), not a config edit. SDK v2 is REMOVED as of 0.4.0: the resolver now throws a clear "SDK v2 is no longer supported — upgrade couchbase@^3/^4" error when the installed major is ≤ 2 or the connector file is missing (previously a silent fallback that crashed later with an opaque MODULE_NOT_FOUND); the v3-vs-v4 split remains for param shaping. Generalises: when a connector's behavior-version derives from an installed dependency rather than config, fail fast once the installed major drops below the supported floor. Couchbase entities expose a public `getCluster()` (on both the model-entity and N1QL-entity prototypes) returning the underlying SDK `Cluster` handle for features the ORM doesn't wrap — chiefly multi-document ACID transactions (`cluster.transactions().run(...)`, needs SDK 3.2+/4.x) — without touching private `_*` internals; it throws a coded `GINA_COUCHBASE_CLUSTER_UNRESOLVED` error when neither connection shape resolves. Dev-mode index reporting: the SDK v4 C++ binding never populates `meta.profile` despite `profile: 'timings'` being sent (confirmed on v4.6.0), so an async `EXPLAIN <statement>` fallback with a per-process per-statement cache supplies the plan instead (the first request for a new statement may show N/A; subsequent requests hit the cache), and `USE KEYS` plans surface as "KV lookup" via `ExpressionScan`/`KeyScan` operator detection. Three historical traps locked by tests: `conn._cluster.query()` must receive the full `queryOptions` object, not the raw params array (the raw form silently dropped `profile`/`scanConsistency`/`adhoc` from every query); of the connector's two `register()` dispatch paths, Option B (`!_isRegisteredFromProto`) is the ALWAYS-active one — instrumentation or logging added to Option A never executes; and (#B193, 0.6.3) the plan walker must visit the NESTED scan containers, not just `~child`/`~children` — the multi-index operators (`IntersectScan`/`UnionScan`/`OrderedIntersectScan`) put their child `IndexScan3` nodes under a `scans` array and `DistinctScan` under a singular `scan`, so before the fix any plan the planner served with more than one index extracted `[]` and the Inspector rendered the red "no index — full bucket scan" badge/banner for a fully-indexed query — a false negative inviting a pointless (write-amplifying) index build. Both extraction paths (SDK `meta.profile` and the EXPLAIN fallback) share the walker, so one fix covers both; index names still dedupe, and the Query tab already renders one chip per index so multi-index plans display correctly with no client change. A consumer-runnable SDK soak harness ships at `script/soak/couchbase-soak.js` (#CN12): it scaffolds a fully isolated throwaway project, installs a candidate `couchbase` SDK into it (`--sdk=<version>` / `--sdk-path=<dir>` — the install IS the version selector, since the connector resolves the SDK from the project node_modules and derives v3/v4 dispatch from its dependency pin), builds + boots prod, and drives N1QL (incl. a `request_plus` arm) + entity-handle KV (promise AND 4-arg callback forms) + the couchbase session store under sustained concurrent load for `--duration`, FAILING on premature process exit (a clean exit 0 counts as failure — the silent-death class it screens for), unbounded RSS growth, error-rate drift, or a dead arm. A screen, not proof: run it as the first filter on an SDK-bump candidate, ahead of a workload-shaped soak. Pure parts (arg parsing, RSS slope, verdict) are unit-tested in `test/lib/couchbase-soak-evaluator.test.js`; the live harness needs a real cluster and never runs in CI. Named scopes/collections stance (2026-08-02): document-field partitioning (`_scope`/`_collection` fields, one bucket, default collection) IS the data model; the `useScopeAndCollections` option (+ `scope`/`collection` defaults) is accepted but INERT — declared and merged, consumed by nothing — and `schema/connectors.json` says so honestly; named-collection KV is reachable per call via `entity.getConnection(scope, collection)`; native scope/collection routing is deliberately not built (demand-gated). #B203 (2026-08-02): both SDK-major resolver twins derive the major as the dependency pin's FIRST integer — the former caret-only strip mangled range pins (`~4.5.0` → `~4`, `>=4.5` → `>=4`), which slipped the v2 floor (parseInt NaN) into a misdirecting existsSync "supported majors are 3 and 4" error; a digit-less pin (`*`, `latest`) now refuses naming the pin, a package.json without a `dependencies` key no longer TypeErrors, and the v2 floor fires for range v2 pins too (`~2.5.0`). #B243 (2026-08-04): a query parameter the SDK cannot serialize — a bare `undefined`, a function, or a Symbol — was PROCESS-FATAL rather than throwable: the SDK maps `JSON.stringify` over the parameter list, those three types yield no string at all, the native binding coerces that to `""`, and the C++ core's JSON parse of `""` throws on an internal thread reaching `std::terminate()`/`abort()` — uncatchable by `try/catch`, `uncaughtException` or `unhandledRejection`, so the whole bundle died instead of the request 500-ing (measured against a live cluster on SDK 4.1.3 AND 4.7.1; 4.2.0+ maps a bare `undefined` to null but still aborts on functions and Symbols). Reachable with NO misuse of the driver: the cursor-style assembly branch (queries matching `\w+\.($|%)`) fills `queryParams[i]` for every `i < params.length` while guarding only `undefined`, so a call one argument SHORT with a trailing callback puts the CALLBACK into a parameter slot — the arity check cannot catch that shape because it only fires when the last argument is not a function. `getUnserializableParamError()` now gates the single `queryOptions.parameters` assignment (so both assembly branches and any future one are covered) and surfaces a `TypeError` coded `GINA_COUCHBASE_UNSERIALIZABLE_PARAM` naming the offending position, routed through the query callback when there is one and thrown otherwise. Serializable values are untouched: `null`, `0`, `''`, `false` and objects carrying `undefined` PROPERTIES still reach the SDK. An object whose own `toJSON()` returns undefined is deliberately left unguarded — detecting it costs a full `JSON.stringify` per parameter on every query for a shape no realistic call site produces.
|
|
883
884
|
|
|
884
885
|
178. **HTTP/2 query paths must tolerate a released response — retry re-entries and late upstream responses run after terminal exits (#B33, 2026-06-12, commit `9c2d802a`).** Terminal exits release the per-request refs, and `redirect()` releases them and THEN calls next(), so an inter-bundle HTTP/2 query can outlive its own request in three measured ways, each previously an uncaughtException → SIGTERM bundle kill: (1) every retry re-entry (the 502/timeout/stream-error/preflight setTimeout paths) re-executes the header-forward prep block, which read the released request's headers — null deref from a timer callback outside all try/catch; (2) a late upstream response's success path calls isHaltedRequest → getSession, which read the released request's session from the stream end handler; (3) a parsed upstream payload claiming a 3xx status routed into the redirect intercepts, which wrote headers to the released response (the emitter-mode intercept uncaught; the callback-mode one contained only via a fragile catch chain). All sites null-guarded: forwards and intercepts no-op on a released request (the same options object travels through retries, so options.headers already carries the attempt-1 values — nothing is lost), getSession reports no session, and the emitter-mode intercept falls through to the query#complete emit so listeners still learn the outcome. Live-request behaviour is byte-identical. Runtime-verified by driving the REAL query() through a standalone controller harness against local h2c servers (six probe modes, crash reproduced pre-fix and clean post-fix per site). Sibling unguarded request-header reads exist in OTHER lifecycle functions; the §23 guard pin is block-scoped to the fixed functions for exactly that reason. **Follow-up #B35 (2026-06-13, `714d816f`+next):** the 5 directly-callable SYNCHRONOUS siblings were MEASURED (standalone harness: createTestInstance → renderTEXT() releases the triplet → call → confirmed `uncaughtException`-class crash) and guarded with top-of-function early-returns — `isPopinContext` → false, `setRequestMethod` → null, `setRequestMethodParams` → (void), `getRequestMethodParams` → the cached value, `getFormsRules` → {} (tests `controller.test.js §25`). **#B36 (`c5eaeeb7`+next):** `renderJSON()` is the same shape — its delegate reads `local.res.stream` synchronously before any `headersSent` guard, measured (standalone harness driving the real `render-json.js` delegate: CONTROL live rendered, RELEASE after `renderTEXT()` threw `reading 'stream'`) to crash a released response → SIGTERM; guarded with a top-of-function `if (local.res == null) return` (tests `render-json.test.js §03`). The render-swig / render-nunjucks delegates share the same read but are the NON-FATAL async class (measured 2026-06-13, initially NOT guarded — GUARDED as of #B45, see below): their `render()` is `async` and `this.render` returns the delegate promise un-awaited, so a released-instance read rejects → `unhandledRejection` → `gna.js:726` logs it (no SIGTERM). This is the canonical severity-axis example — a SYNCHRONOUS released read (render-json `#B36`, the `#B35` helpers) is a SIGTERM bundle-kill worth guarding; the same read in an ASYNC delegate is a logged unhandledRejection not worth editing the hot render path for. **#B45 (2026-06-14, `d9bfc5af`) reversed that for the render delegates:** production surfaced exactly the predicted `unhandledRejection` (a controller firing several parallel `self.query()` calls against a downed upstream — the first failure callback renders+releases the triplet, a later callback re-enters `render()` at `local.res === null` → render-swig.js:259 `res.stream` throws), which is the #B36 "revisit only if the log noise proves operationally costly" trigger. All four async delegates (render-swig / render-nunjucks + both async variants) now carry the same top-of-function `if (local.res == null) return` guard as render-json/render-stream — one null check at the top, byte-identical on live requests; the rarer in-flight #M1 `setResources` race (render-swig.js:613 → controller.js:896, caught + #B31-guarded) is unchanged. The severity-axis lesson still holds (sync → SIGTERM → always guard; async → unhandledRejection → guard only once the noise is shown operationally costly) — #B45 is the worked example of the async "guard-when-costly" branch firing (tests `render-swig.test.js §19` / `render-nunjucks.test.js §08` / `render-engine-dispatch.test.js §08`). `redirect` GUARDED too (**#B37**, `a03e6f84`+next): measured synchronous, so a released second-call (redirect-then-redirect, or render-error-then-redirect) crashed `reading 'originalMethod'` → SIGTERM; top-of-function `if (local.req == null) return` (tests `controller.test.js §26`). **#B38 (2026-06-13) — the #B37 "SIGTERM class CLOSED" claim was premature: an exhaustive sweep of EVERY synchronous controller surface found SIX more lethal sync residuals, each measured (CONTROL no-throw / RELEASE positive crash, then no-throw post-guard) and guarded top-of-function with the same `if (local.req|res == null) return <default>` shape:** `downloadFromLocal` (`reading 'setHeader'`), the inner `start` of `store` (`reading 'files'` — reached SYNCHRONOUSLY through the documented `store(target).onComplete(cb)` wrapper, which calls `start` OUTSIDE the async `store` body; the #B35 probe had only tried `store('t')`, which returns the wrapper WITHOUT calling `start`, so `store` was wrongly filed as async-deferred here), `renderStream` (`reading 'stream'` — its controller wrapper AND the delegate are both synchronous, and the read precedes the delegate's headersSent guard, mirroring the #B36 render-json placement), `push` (`reading 'method'`), `pauseRequest` (`reading 'url'`), `resumeRequest` (`reading 'session'`/`'method'`) — tests `controller.test.js §27` + `render-stream.test.js §11`. Genuinely document-skipped (measured NON-FATAL, by the same async-boundary reasoning — the render delegates themselves since GUARDED, see #B45 above): `downloadFromURL` (its reads sit inside an `async function`, so any throw is a rejected promise → `unhandledRejection`), and the render-path inner fns `setResources` / `getNodeRes` (sync, but invoked ONLY from the async render delegates, so a throw rejects the delegate promise — and the captured-req fix-shape was declined because the captured req is itself null in the released-response case). **Rule: lethality follows the NEAREST async boundary, not the function's own sync/async keyword — a sync read with no async function between it and the dispatcher is a SIGTERM bundle-kill (guard it); the same read reached only through an async function degrades to a logged unhandledRejection (skip it). Probe-asymmetry corollary: "no throw" is NOT proof of safety — feed inputs that actually REACH the deref (`resumeRequest({})` dodged its `req.session` read via an `&&` short-circuit + an early `throwError` return and looked safe until re-measured with a deref-reaching input).** **#B44 (2026-06-14, commit `d95ac2fe`) — closes the one throwError-OWN residual the #B38 sweep flagged, and is the worked example of the measurement-scope-gap.** `throwError` reads `res.stream` (the HTTP/2 protocol branch) and then builds the error object from `res.error`/`res.stack`/`res.fallback` (~10 reads) BEFORE its OWN #B31 guard. For the 2-arg `throwError(code, Error|string)` and 3-arg `throwError(local.res, code, msg)` shapes `res` is already the released `local.res` (null) at those reads, so a released response crashed at `res.stream` on HTTP/2 bundles and at `res.error` on EVERY bundle (HTTP/1.1 reaches it because the `res.stream` read short-circuits off-h2). The 1-arg shape is unaffected (its `res` stays the truthy errObj until reassigned just before the guard — why #B31 sufficed for ITS scenario). Fixed with an up-front `if (!res) { warn; return false; }` early guard before any deref (tests `controller.test.js §28`); severity LOW (only the async `downloadFromURL` path reaches it today → non-fatal `unhandledRejection`), but it lifts the #B38 qualification for the synchronous 2-arg/3-arg surface on both protocols. **Measurement-scope-gap lesson: the FIRST-proposed fix (guard the `res.stream` read only) was INSUFFICIENT — it does NOTHING on HTTP/1.1 (already short-circuits there; the crash is at the `res.error` build) and merely RELOCATES the HTTP/2 crash to that same build. The prior repro was VERBATIM-TRUNCATED at the first crash line (it ran through the `res.stream` read, saw the TypeError, STOPPED), so it proved "the crash exists" but never executed the NEXT deref and so couldn't see that guarding one site just moves the crash. A repro that stops at the first crash cannot validate a "falls through to the guard" claim — match the repro's SCOPE to the claim's scope (the "match the measurement's scope to the claim's scope" rule, applied to a runtime repro). A per-site `&& res` is whack-a-mole here (~10 derefs before the guard); one early guard covers them all.** **The family's origin (#B31, folds former #172):** the first two guarded entry points were `throwError` — it normalizes every 1-/2-arg call shape to `res = local.res`, then read `typeof(res.getHeaders)` off the null — and `headersSent()` (read `typeof(_res.stream)`); both were uncaughtException → SIGTERM bundle kills with no application frames. Field shape: an auth middleware that 301-redirects unauthenticated requests and lets the chain continue makes the crash deterministic on every unauthenticated hit, and the crash-respawn loop keeps every process cold — masquerading as a separate "valid sessions never authenticate" bug (each request lands on a freshly-respawned process whose session-store connector hasn't warmed). Now `headersSent()` reports a released response as already-sent (a single chokepoint — every `!headersSent()` caller no-ops) and `throwError` logs the serialized late error (naming what previously died opaque) and returns false; live-response paths are byte-identical. Reusable repro recipe: `controller.js` loads standalone — inject the framework dir into NODE_PATH + `Module._initPaths()`, require the framework helpers (injects the path/JSON globals), `setPath('gina', { core: <fw>/core })`, then `SuperController.createTestInstance({req, res, next, options})` with a minimal mock response; `renderTEXT()` is the lightest terminal exit to reproduce any released-response sequence against the real class.
|
|
885
886
|
|
|
@@ -891,7 +892,7 @@ Dev-mode query instrumentation captures every database query tied to the current
|
|
|
891
892
|
|
|
892
893
|
206. **Observable application events surface in the dev Inspector via a per-request signal that mirrors #AISTREAM — `self.emitEvent(name, metadata)` (controller) or `lib/inspector-events.emit()` (model/service code) pushes a `{type:'event',id,name,t[,meta]}` entry and emits a live `inspector#event` frame, delivered as an `event: event` SSE/WS frame plus a `user.events` end-of-request snapshot, shown in a new SPA "Events" tab (#EVTBUS, 2026-06-29).** The per-request buffer `_devEventLog` is a THIRD key on the shared `process.gina._queryALS` store (sibling of `_devQueryLog`/`_devAiLog`, seeded at `controller.js` `setOptions`); `emit()` reaches it via `getStore()` so the snapshot is captured from any code in the request's async context (outside one — a background job / lifecycle hook — the live frame still fires but no snapshot is pushed, per Slice 2b below; a closed gate or invalid name is a no-op). Capture is gated on `NODE_ENV_IS_DEV` OR an open `process.gina._inspectorWindowUntil` (identical to the query/AI gates). The event NAME + framework stamps always ride the wire; the caller's `metadata` VALUES ride ONLY when `settings.inspector.events.captureArgs` is true (default off, seeded onto `process.gina._inspectorEventsCaptureArgs` at boot) — `lib/inspector-redact` matches secret-NAMED keys only and cannot sanitise arbitrary arg VALUES, so the gate + opt-in + authenticated channel are the protection, never redaction (same contract as #AISTREAM `captureText`). Delivery: a `server.isaac.js` SSE forwarder (`event: event`) + a `server.js` WS forwarder (`{event:'event'}`), registered/deregistered beside the data/log/token listeners; the `user.events` snapshot is attached at the SAME 5 render sites as `user.aiStream` (render-json + inspector-window-emit on `_gdUser`, render-swig cache-hit/miss + render-nunjucks on `data.page.events`), gated on `local._eventLog.length`. **Three carry-forward facts:** (1) the SPA `appendAppEvent` ACCUMULATES a capped rolling buffer (NOT single-slot-reset like `appendTokenDelta` — events are discrete, not one token stream), renders via `.textContent` (untrusted app text), and the `renderTab` `case 'events':` prefers the live buffer then falls back to the request snapshot; (2) the live `event: event` frame rides isaac-SSE + server.js-WS, and (#AISTREAM/#EVTBUS parity gap CLOSED 2026-06-29) the server.js HTTP/1 SSE `/_gina/agent` handler now forwards token + event too via `response.write` (mirroring isaac's `_agWrite` shape) — all three live transports now carry all four frame types and `event-inspection.test.js §08` is a positive pin; (3) `event` is NOT an EventSource-reserved name (unlike open/message/error) so `event: event` is collision-free. Adding the 8th SPA tab updates the 3 `TAB_LAYOUTS` presets → trips `inspector.test.js §44`'s exact-tab-set pins (now 8 tabs, updated with approval). `lib/inspector-events` registered via `_require` (stateless). There are zero pre-existing app/domain events in the framework (the only `process.emit` namespaces are `inspector#`/`logger#`/`gina#`, all internal), so the emit API is the headline — without it the signal surfaces nothing. Slice 2a (SHIPPED 2026-06-29): a curated allow-list (`settings.inspector.events.topics`, default `[]`) bridges entity-trigger emits onto the live signal via a gated block in `entity.js`'s `emit` chokepoint — skips `error`, gated on a non-empty allow-list FIRST (the opt-in IS the flood control), ships a framework-controlled `{ok,error}` summary (never raw rows; captureArgs-gated like all metadata — name+source always ride, the `{ok,error}` summary reaches the wire only when captureArgs is on, default off) tagged `source:'framework'` (a new optional 3rd arg to `inspector-events.emit` + an exported `matchTopics` helper: exact / single leading-or-trailing `*`); reached via `require('lib/inspector-events')`; request-scoped so it reuses the MVP live+snapshot delivery (no new transport/ALS key/tab). The chokepoint catches every entity emit with ZERO per-instance `.on()` / `ENTITY_MAX_LISTENERS` use (the original deferred note had conflated flood with listener-exhaustion). Only the couchbase connector emits named CRUD (`N1QL:<entity>#<method>`); the other 5 don't, and CRUD is largely redundant with the Query tab — so 2a's genuinely-new value is custom (non-query) entity methods. Slice 2b (SHIPPED 2026-06-29): an emit refactor SPLITS the store-gated buffer push from an always-on live emit — once the gate passes the live `inspector#event` frame fires store-or-not (a no-store background-job / lifecycle caller now reaches the stream; only the per-request snapshot needs a store), return `true`⟺live-frame-emitted / `false` only for gate-closed or invalid-name; this flipped §01 (`:58-63`) and CHANGED the no-op-outside-request contract, deliberately diverging from #AISTREAM (whose live `inspector#token` emit is itself store-gated at `ai/index.js` `if(_aiLog)`). On top of it a connector-lifecycle bridge surfaces a connector's recurring `ready` emit (re-fired on every reconnect) via a single additive `connector.on('ready')` at the construct-once site in `core/model/index.js this.connect` (attached once in the cache-miss branch → no per-reconnect accumulation; the existing `onReady` consumer is a one-shot `once('ready')`; couchbase is the only connector emitting `ready` today, the other 5 use a direct onReady callback; redis session-store connect/disconnect — the only other recurring pair — has no framework seam so it is deferred), live-only (lifecycle events have no request context → no snapshot, exactly the no-store path the refactor unblocked), reusing the same `_inspectorEventTopics` allow-list + `matchTopics` + `source:'framework'` tag + `{ok,error}` summary (no new config key). Tests: `test/core/event-inspection.test.js` (§01 real-module behaviour incl. the no-store live-emit, §02-§09 server wiring, §10-§11 SPA + dist propagation, §12-§14 Slice 2a: source/matchTopics + entity-bridge source-pins/replica + topics seed, §15 Slice 2b connector-lifecycle bridge: model/index.js source-pins + replica). Slices A1a `b2157113` / A1b `bb3103f1` / A1c `4f844bd6`; Slice 2b emit refactor `ea55a489` + connector-lifecycle bridge. Slices 2a + the 2b always-on-emit substrate LIVE-VERIFIED e2e 2026-06-30 (entity op → `inspector#event` frame + `user.events` snapshot; a no-store background emit → live frame), which also corrected the captureArgs note above; the couchbase-specific `couchbase#ready` reconnect→frame e2e is source + unit-replica only (no self-controlled couchbase env).
|
|
893
894
|
|
|
894
|
-
209. **FormValidator — engine disambiguation, string inputs, a11y reflection, and live-check message visibility (consolidates former #42/#130/#150/#186/#197).** The live form/data rule engine is `core/plugins/lib/validator/src/form-validator.js` (single source, `isGFFCtx`-branched: runs server-side via `backendInit` AND compiled into the browser bundle) with the client orchestration in `validator/src/main.js` — `framework/v*/lib/validator.js` is a DEAD standalone fluent validator with overlapping `is*` rule names; never edit it for form-rule work (tell them apart fast: the live engine's `isRequired` rejects whitespace-only input, the dead one passes it). Rule bodies must handle STRING inputs — both contexts feed strings (`.value` + urlencoded bodies) — so a typed/numeric rule coerces or parses explicit components, never assumes a typed JS value: `isFloat` coerces via `Number()` (#B46); `isDate` builds from explicit mask components + a round-trip check so non-ISO slash masks aren't US-misparsed and impossible dates still reject (#B47); `isDate` returns the FIELD again on its valid path (#B48, 0.5.4 — parsed `Date` preserved on the field's `.value`, the `isDate(mask).format(...)` idiom unchanged), so rule chaining works. An empty value is adjudicated by `isRequired` ALONE (#B78): the per-rule empty-bypass became unconditional on empty (`if (this.value == '')`, its old `!errors['isRequired']` gate dropped) for `isEmail`/`isJsonWebToken`/`isFloat`/`isInList` — each regating `this.valid = isValid && !errors['isRequired']` — and `isString` keeps the field invalid without recording a second message, so a required-empty field shows ONE message (`is required`) not two, optional empty fields still pass, a filled-but-invalid value still reports its own error, and custom `is` (coercion-sensitive) is untouched by #B78 - but a cross-field `is` (`"$a === $b"`) no longer THROWS when the referenced field is empty (#B82): the client dynamised-rules substitution (`getCastedValue` in `main.js`) now renders an empty referenced operand as a quoted `""` (it was spliced RAW, leaving a dangling `"7654321" === ` that `is()`'s binary-comparison grammar `_SCS_BINARY_RE` rejected -> an uncaught throw that aborted the whole-form validity pass and left the submit trigger ungated on an invalid form, breaking the documented `is`+`isRequired` value-confirmation pattern while the confirm field was blank), mirroring `getDynamisedRules`' own sibling substitution default (`: '\"\"'`); `null`/`undefined` stay raw (already valid operands). Hardening: `is()`'s grammar mismatch now FAILS-THE-FIELD (`console.warn`+`isValid=false`) instead of throwing, so a per-keystroke live check can never abort the gate on a residually-unparseable condition (e.g. a field literally valued `"NaN"`, which the root fix leaves raw). Browser-bundled -> prod dist rebuilt; the `#SCS1e`/`#SCS1h` eval-safety pins target the untouched `_SCS_BINARY_RE`/`_scsParseOperand`/regex-literal constructs, so the hardening flips none of them. The form's validity comes from `getErrors().count()` (the surviving `isRequired` error), never the per-field `.valid` flag (whose only error-dropping reader, `setErrors`, is dead). A rule-body edit needs a prod dist rebuild AND flips the section-locked characterization tests by design. Editing trap: `form-validator.js` embeds hidden NO-BREAK SPACE bytes (U+00A0) where a normal space appears inside several `||`/ternary sequences, so a literal-space find/replace spanning one silently fails — patch such regions with a byte-scoped script over clean-ASCII substrings, not a space-spanning match. The blur-time global validation pass sets submit-button state but renders errors ONLY for the touched field — untouched invalid fields stay quiet until interacted-with or submit. Accessibility (#A11Y1): the rule-agnostic chokepoint `handleErrorsDisplay` reflects committed errors into `aria-invalid="true"` (gated on committed-not-warning; `"false"` on clear mirrors native `ValidityState` so it agrees with `:user-invalid`; hidden fields skipped), auto-wires `aria-errormessage` to a gina-owned message div UNLESS the consumer provided their own, focuses the first DOM-order invalid field on a failed submit, and announces blur-time errors via a per-form visually-hidden `aria-live="polite"` region; the per-field aria passes fire only under live-check — the always-on submit pass covers every bound form regardless. Error-MESSAGE visibility has THREE write paths (create / refreshWarning's un-hide / the refresh re-create) and the re-create runs LAST in the live-check pass, so it owns the steady state: it is focus-aware — message hidden while the edited field is the active element, revealed on blur (soft warning border while typing). Rule: when an element is written by multiple paths in a single validation pass, guard the LAST writer — an earlier-writer fix is silently overridden. The invalid submit trigger is marked `aria-disabled="true"` + the class `gina-form-submit-disabled`, NEVER native `disabled` (#B76 — a natively-disabled button emits no click, so the validate-render-focus guard could never run); `isValid()` is the real send gate, and **consumers style that state — the framework ships no button CSS**. Form-associated custom elements (FACEs) participate in binding + live-check (#CC2 — hyphenated members of `form.elements`; their own `.value` accessor is honoured, live-check rides the composed bubbling `change`; author contract: `static formAssociated`, a `name` attribute, a `.value` getter, composed `change` on commit).
|
|
895
|
+
209. **FormValidator — engine disambiguation, string inputs, a11y reflection, and live-check message visibility (consolidates former #42/#130/#150/#186/#197).** The live form/data rule engine is `core/plugins/lib/validator/src/form-validator.js` (single source, `isGFFCtx`-branched: runs server-side via `backendInit` AND compiled into the browser bundle) with the client orchestration in `validator/src/main.js` — `framework/v*/lib/validator.js` is a DEAD standalone fluent validator with overlapping `is*` rule names; never edit it for form-rule work (tell them apart fast: the live engine's `isRequired` rejects whitespace-only input, the dead one passes it). Rule bodies must handle STRING inputs — the two DECLARATIVE contexts feed strings (`.value` + urlencoded bodies) — so a typed/numeric rule coerces or parses explicit components, never assumes a typed JS value; but "always a string" is NOT true of every path, and reading it that way shipped #B198 (see below): a JSON request body keeps real Numbers (`JSON.parse` → `req.body`/`req.post`, which the `validator::{}` routing path MERGES into the validated data before spreading array bounds through `apply()`), and `toInteger` leaves `Math.round()`'s real Number on `this.value`, so a `toInteger` → `is*` chain hands the next rule a Number even in the browser — a rule must therefore be correct for a typed value too, not merely tolerant of strings: `isFloat` coerces via `Number()` (#B46); `isDate` builds from explicit mask components + a round-trip check so non-ISO slash masks aren't US-misparsed and impossible dates still reject (#B47); `isDate` returns the FIELD again on its valid path (#B48, 0.5.4 — parsed `Date` preserved on the field's `.value`, the `isDate(mask).format(...)` idiom unchanged), so rule chaining works. An empty value is adjudicated by `isRequired` ALONE (#B78): the per-rule empty-bypass became unconditional on empty (`if (this.value == '')`, its old `!errors['isRequired']` gate dropped) for `isEmail`/`isJsonWebToken`/`isFloat`/`isInList` — each regating `this.valid = isValid && !errors['isRequired']` — and `isString` keeps the field invalid without recording a second message, so a required-empty field shows ONE message (`is required`) not two, optional empty fields still pass, a filled-but-invalid value still reports its own error, and custom `is` was deliberately excluded by #B78 and re-declined by #B82 — an exclusion REVERSED by #B233 (2026-08-03, unreleased, `307721f2`): `is` now carries the same canonical strict bypass (`if ( this.value === '' ) { isValid = true; }`) and the same regate, taking the Shape-A population from four rules to FIVE, so a required+EMPTY field carrying an `is` condition records `isRequired` ALONE instead of also collecting a second `Condition not satisfied`. `isBoolean` joined the same contract at #B235 (2026-08-03, unreleased, `aa1c2035`), taking that population to SIX: its pre-switch rescue `errors['isRequired'] && this.value == false` was LOOSE (`'' == false`), so a required+EMPTY boolean field LOST its isRequired error and reported `Must be a valid boolean` instead of `Cannot be left empty`; the rule now takes the canonical strict `=== ''` self-pass, and the rescue moves AFTER the accept-set switch gated on the value having been ACCEPTED (`val !== null`) — which keeps the documented unchecked-but-required-toggle case working, since a recognized `false`/`0` is a present answer, while emptiness returns to `isRequired` alone. Paired in the same commit with #B236, the SERVER-side half: the plugin's `getCastedValue` funneled EVERY value on an isBoolean-ruled field through `/^true$/i ? true : false` BEFORE the engine ran (client AND server — `validate` calls `formatFields` unconditionally), so on the server auto path junk validated CLEAN and PERSISTED as `false` — `nope`, the HTML checkbox default `on` (a CHECKED box storing UNchecked), the strings `1`/`0`, `TRUE`/`True` — and the NUMBER 1 stored `false` where the engine reads it as `true`. The pre-cast now survives ONLY in dynamised-rules mode, where a referenced boolean field must splice into a stringified `is` condition as an unquoted operand (measured NECESSARY: deleting it outright breaks a server `$flag === true` condition); the ENGINE is the single adjudicator on every surface, which is what the routing `validator::` surface always enforced and what the published reference already promised. Disclosed both directions: values that silently stored `false` now ERROR, the number 1 flips its stored value `false`→`true` on a verdict that was already valid, an optional blank boolean field now PASSES instead of erroring, and a required blank field's message changes from isBoolean to isRequired. A sibling server-path crash in the same plugin is fixed by #B234 (2026-08-03, unreleased, `7c56565d`): `getDynamisedRules` substitutes in two passes, and the SECOND is a DOM fallback re-deriving each splice value from the live element (`$fields[...].value`) — which `backendInit` calls with `$fields = null`, so it threw `TypeError: Cannot read properties of null` on its FIRST iteration for ANY `$` surviving pass 1: a regex end-anchor in an `is` condition, a `$` inside a human-readable message string, or a `$` in any array-rule element after the first. Plain cross-field `$peer === $me` never crashed, because pass 1 consumes tokens that NAME fields. The loop is now gated `$fields && ...`, joining the #B127 precedent one function later; `validate`'s same-text gate is deliberately left UNGUARDED, being reachable only with a live DOM. Residual, disclosed — and since FIXED (#B239): a `$` token in an ARRAY rule's FIRST argument that names no field (`isInList: ['$100']`) threw one site later at `checkFieldAgainstRules`' `d[<token>].value` — NOT DOM-dependent, so it reached the client too. The substitution is now gated on the token resolving to a REAL field (an existing `d` key with a defined `.value` — two clauses, both load-bearing: an engine-METHOD-name collision like `'$isValid'` resolves to a defined key with no `.value`, and pre-fix spliced the string "undefined" into the rule for a silent wrong verdict rather than a crash); anything else stays LITERAL so strict comparison applies (`'$100'` matches its own literal, rejects non-members with the rule's own error; bare-`$` and mixed elements covered). `$` is therefore the engine's RESERVED cross-field sigil: whether an authored `$` stays literal depends on a runtime field-name collision — a token naming a sibling field is consumed UPSTREAM by getDynamisedRules loop 1, substituted with quoting fit for `is`-condition splices, not array elements (`"yes"` with quotes can never match `yes`), so real cross-field refs in array-rule elements are always-invalid, fail-closed, never-worked, undocumented (the reference scopes `$name` to `is` expressions) — tracked as #B240 (demand-gated; the fix is relocating array-element substitution into checkFieldAgainstRules, whose guarded loop is deliberately preserved as the substrate). This reserved-sigil model is also the #DTO2 `$` guard's CURRENT rationale (the crash rationale is retired — deterministic literal semantics are impossible for any `$`, so toRules() refuses at boot rather than validate collision-dependently). The reversal is measured rather than re-argued: the old bypass was gated on `!errors['isRequired']` — off exactly when #B78 wants it on — beside a two-disjunct guard that was DEAD CODE (`x == '' && x != 0` has no witness), optional+empty ALREADY self-passed through the live else-if, and on required+empty the condition is VERDICT-IRRELEVANT (form validity is `getErrors().count()` and `isRequired` has already errored), so form validity and the request payload are identical in both directions and only the message list changes; the "coercion-sensitive" premise had already been retired by #B199's strict test, which leaves `0`/`false`/`null` as operands that still evaluate the condition. #B82 is neither regressed nor retired — its root `getCastedValue` quoting and its `is()` grammar guard stay necessary and reachable with a FILLED host; #B233 only closes that crash path a second time for an empty HOST, whose condition is no longer compiled at all. Same commit drops the dead `_defaultErrorLabels['isApiError']` entry (zero consult sites: the API path assigns the server's message directly and never calls `replace()`) - but a cross-field `is` (`"$a === $b"`) no longer THROWS when the referenced field is empty (#B82): the client dynamised-rules substitution (`getCastedValue` in `main.js`) now renders an empty referenced operand as a quoted `""` (it was spliced RAW, leaving a dangling `"7654321" === ` that `is()`'s binary-comparison grammar `_SCS_BINARY_RE` rejected -> an uncaught throw that aborted the whole-form validity pass and left the submit trigger ungated on an invalid form, breaking the documented `is`+`isRequired` value-confirmation pattern while the confirm field was blank), mirroring `getDynamisedRules`' own sibling substitution default (`: '\"\"'`); `null`/`undefined` stay raw (already valid operands). Hardening: `is()`'s grammar mismatch now FAILS-THE-FIELD (`console.warn`+`isValid=false`) instead of throwing, so a per-keystroke live check can never abort the gate on a residually-unparseable condition (e.g. a field literally valued `"NaN"`, which the root fix leaves raw). Browser-bundled -> prod dist rebuilt; the `#SCS1e`/`#SCS1h` eval-safety pins target the untouched `_SCS_BINARY_RE`/`_scsParseOperand`/regex-literal constructs, so the hardening flips none of them. The form's validity comes from `getErrors().count()` (the surviving `isRequired` error), never the per-field `.valid` flag (whose only error-dropping reader, `setErrors`, is dead). Length bounds are ARITY-sensitive, and the source JSDoc was WRONG about it until 0.6.3: `"isString": [N]` (same for `isInteger`/`isNumber`) supplies `minLength` ONLY — identical in effect to the scalar `N` — because the exact-length branch fires only when `minLength === maxLength`, so an exact length needs `[N, N]`; the stale comment had propagated verbatim into the published reference page, so correct BOTH surfaces when one is found. Those bounds measure the value's STRING FORM (`val.toString().length`) — until 0.6.3 `isInteger` alone measured a bare `val.length`, which is `undefined` on a real Number, so BOTH its bounds were silently inert on every numeric value: no error, no warn, field left `valid` (#B198, a fail-OPEN bypass reachable from a JSON body, a `validator::{}` requirement, or a preceding `toInteger` — the browser included). `isString` reads the same bare `val.length` at two sites and is CORRECT there because a `typeof(val) == 'string'` guard precedes it, so this class of fix is line-scoped: a whole-file replace of the bound expression hits four sites, two of which must not change. One consequence of measuring the string form, intended: a negative number counts its sign toward the length (parity with the same value arriving as a string). The zero-swallow residual #B198 initially left open is CLOSED by #B199 (0.6.3): loose `== ''` emptiness tests conflated `0`/`-0`/`false`/`[]` with the empty string at FIVE sites — the isInteger/isNumber bounds gates AND the isEmail/isJsonWebToken/isFloat empty-bypasses, where a JSON body's `{"email": 0}` validated as a correct email — all five now compare strictly, so only the literal `''` bypasses (the designed empty-is-adjudicated-by-isRequired contract, preserved byte-exactly); `isString` stays loose behind its typeof guard (operators identical for strings), `isInList` was already strict, and `isDate`'s broader `!val` swallow (a silent half-state: `valid` false, NO error recorded, so the form passes) is deliberately untouched. STILL OPEN sibling (#B200): a TRUTHY non-string in an isEmail/isJsonWebToken field (`{"email": 123}`) hits an unguarded `.toLowerCase()` and the rule driver RE-THROWS, killing the whole validation run — the falsy/truthy non-string space is partitioned between the fixed bug and this one. Custom validators (`bundle/validators/<name>/main.js`) are a BROWSER-ONLY affordance, NEVER a server-side guarantee: the server gate reads `getContext('gina').forms` while the loop it guards reads a bare `gina` that is undefined in Node, so a custom rule never attaches server-side and the engine then silently skips the unknown rule name with no warn — re-validate such constraints in the action. (Publishing that context without also fixing the loop would make EVERY validator construction throw, including for bundles shipping no custom validators.) A rule-body edit needs a prod dist rebuild AND flips the section-locked characterization tests by design. Editing trap: `form-validator.js` embeds hidden NO-BREAK SPACE bytes (U+00A0) where a normal space appears inside several `||`/ternary sequences, so a literal-space find/replace spanning one silently fails — patch such regions with a byte-scoped script over clean-ASCII substrings, not a space-spanning match. The blur-time global validation pass sets submit-button state but renders errors ONLY for the touched field — untouched invalid fields stay quiet until interacted-with or submit. Accessibility (#A11Y1): the rule-agnostic chokepoint `handleErrorsDisplay` reflects committed errors into `aria-invalid="true"` (gated on committed-not-warning; `"false"` on clear mirrors native `ValidityState` so it agrees with `:user-invalid`; hidden fields skipped), auto-wires `aria-errormessage` to a gina-owned message div UNLESS the consumer provided their own, focuses the first DOM-order invalid field on a failed submit, and announces blur-time errors via a per-form visually-hidden `aria-live="polite"` region; the per-field aria passes fire only under live-check — the always-on submit pass covers every bound form regardless. Error-MESSAGE visibility has THREE write paths (create / refreshWarning's un-hide / the refresh re-create) and the re-create runs LAST in the live-check pass, so it owns the steady state: it is focus-aware — message hidden while the edited field is the active element, revealed on blur (soft warning border while typing). Rule: when an element is written by multiple paths in a single validation pass, guard the LAST writer — an earlier-writer fix is silently overridden. The invalid submit trigger is marked `aria-disabled="true"` + the class `gina-form-submit-disabled`, NEVER native `disabled` (#B76 — a natively-disabled button emits no click, so the validate-render-focus guard could never run); `isValid()` is the real send gate, and **consumers style that state — the framework ships no button CSS**. Form-associated custom elements (FACEs) participate in binding + live-check (#CC2 — hyphenated members of `form.elements`; their own `.value` accessor is honoured, live-check rides the composed bubbling `change`; author contract: `static formAssociated`, a `name` attribute, a `.value` getter, composed `change` on commit). **Radio-group collection (#B221):** an unchecked non-boolean radio group whose rule declares a truthy `isRequired` is collected as an EMPTY value by BOTH collectors (`getFormValidationInfos` + the native-submit inline copy) so `isRequired` adjudicates it via the standard emptiness test — pre-fix no collection arm admitted the shape (each required `.checked`, a `true|false`-shaped value, or an `isBoolean` rule), the DOM handle was held in `$fields` but the VALUE never entered `fields`, so no rule ran against the group and a radio-group-only form short-circuited BOTH submit guards (field count 0 reads as nothing-to-validate → synthetic `isValid() === true`) and submitted its XHR with zero client-side validation. Other unchecked groups stay absent-when-unchecked (native parity: no rule / `isRequired: false` unchanged; `isBoolean`-declared groups keep the force-false arm), checked members post exactly as before, and on the auto path a required-empty form is invalid and never sends — so the wire only changes for the newly-gated shape. Enforcement-tightening: forms that silently submitted with nothing picked now gate on the pick (trigger `aria-disabled` at bind under default-on live-check, message on submit attempt, re-enabled after picking). **The re-enable is real only since #B228:** the radio live-check listener was registered under a `changed.<id>` event name nothing dispatches on a user pick — the form-level click proxy short-circuits into the radio state updater (which never dispatches any gina event), and the change proxy dispatches ONLY names present in the event registry, which radios never registered (checkboxes have that registration via their state-updater relay; radios' equivalent relay is keyed on the bare element id, which nothing triggers) — so the whole-form silent pass never re-ran after a pick and the trigger kept its bind-time disabled state indefinitely, while submit-time validation (a separate call chain) accepted the checked group and let the click-guard send: flows completed, only the trigger state was wrong (announced disabled to assistive tech; automation actionability checks refuse `aria-disabled`). Radios now ALSO register the proxy-dispatched `change.<id>` name alongside `changed.<id>` — the handler's radio arm accepted `change.`-typed events all along, so one registration line closes the loop: mouse, label and keyboard picks all re-run the field + whole-form passes (single delivery per pick — native `change` fires only on real state changes; the legacy `changed.<id>` name stays registered for the relay/programmatic path; checkboxes byte-identical). Latent since the live-check's introduction, invisible until #B221 armed it. **A field that DRIVES its own conditional block lost its BASE rules until #B229:** `forEachField`'s per-field tail read `if (isInCase || caseName == field) continue;`, and `caseName` is assigned inside the `_case_` scan loop that re-runs in full on EVERY field iteration, so it always held the LAST scanned `_case_` key's driver name — when the iterated field WAS that driver the `continue` skipped the rest of the iteration, base-rule check included. A rule shape `{ "group": { "isRequired": true }, "_case_group": { "conditions": [...] } }` therefore never adjudicated `group`'s own `isRequired` on the bind pass, the live-check global pass OR the submit pass: the form never gated and an empty submit went out with zero client-side validation — the silent-submit class above, resurfacing for the self-driving shape and structurally DOWNSTREAM of the collection fix (the group IS collected as `''`; only adjudication was missing). The tail is now split: `isInCase` keeps its own `continue` (it is dead code — never assigned truthy — and is preserved as such), and the `caseName == field` arm runs the base-rule check before continuing, restoring the driver's collected value around the call (the check deletes the field from the object it is handed, and that object is where the scan block re-reads the case VALUE on every later field iteration; a deleted entry re-seeds from the DOM, which for a radio group is the FIRST member's value regardless of `.checked`). Which conditions apply is unchanged — the direct-case block is never entered for a self-driving case, pre- or post-fix (measured) — and the fix is order-independent: a driver declared BEFORE another `_case_` block was already adjudicated (the tail's comparison never matched it), so the post-fix union is every driver carrying base rules. Client-only: the server form-body path throws earlier on any `_case_`-bearing rule set (conditional rules are unsupported there). Enforcement-tightening: a form built on this shape starts gating where it silently submitted. Known interplay, pre-existing: on a rule set with NO `$` tokens, a pick whose value matches a `_case_` condition lets the case machinery PERSISTENTLY replace injected fields' rules in the live store (the site-B replacement), so a later `reBind()` can re-arm the gate from the mutated store — `$`-bearing rule sets are immune (the dynamised-rules path clones). **A conditional driver's collected VALUE survives every full-form pass since #B230:** the base-rule check deletes each adjudicated field from the object it is handed, and until #B230 only the last-declared driver's entry was restored (the #B229 arm above) — any OTHER field that both carries base rules and drives a `_case_` lost its stored case value the moment its own rules were adjudicated, so later field iterations re-read it from the DOM (a radio group's FIRST member regardless of `.checked`) and matched conditions against a value the user never picked — spuriously requiring the wrong flow's fields (a correctly-completed picked flow could not submit), or with excluding condition rules under-validating the picked flow — while the driver's own direct-case block read `undefined` in the same pass and matched nothing. The entry is now backed up and restored around the base-rule check for any field driving a `_case_` in the live rules OR the pass-entry rule clone; the union matters because inside a direct-case recursion the pass's rule set is the condition's own rules, which carry no `_case_` keys, so the live-rules test alone is blind there. Non-driver fields keep the deletion untouched (the condition pull-in gate, the direct-case exclude injection and the async-`query` re-validation input all read those absences today), and a driver with no rules of its own is byte-identical — including the legitimate first-scan DOM seed for rule-less unchecked groups, which is preserved. **`setFlash` `[null, "message"]` works client-side since #B226 (the form the reference documents):** it previously lost its custom message in the browser ONLY — `lib/merge` classified a `null` array element as an object (`typeof null`) and dropped it on every no-override merge, and the client rules path re-merges the whispered rules (the `data-gina-form-rule` bind merge, the `gina.hasValidator` instance re-merge, the `_case_` merges), so the engine received a one-element array, bound the message to the ignored first `regex` argument, and rendered the built-in label; `["", "message"]` always survived (empty strings, `false` and `0` are primitives — `null` was the only casualty), and the server was unaffected (it reads the boot-loaded rules without those hops). The fix is in `lib/merge` itself, so no-override merges now preserve `null` array elements as VALUES framework-wide (and the index-merge branch stops manufacturing `{}` from a `null` source element) — a merge consumer relying on the silent compaction sees the `null` slots preserved. **Bracket-notation and nested-authored rule KEYS enforce on the SERVER form-body path since #B241:** the rule parser canonicalizes every rule key to a dotted path (`account[username]` becomes `account.username`; a nested rule tree flattens to its dotted leaves) while the server's fields map kept the RAW posted keys, so such rules never joined — the field was silently skipped with no warning, fail-open for every rule-keyed directive alike: checks (`isRequired`, `isEmail`, ...), the `exclude` drop, and value transforms — on BOTH production wire shapes (flat bracket keys: the client posts its name-keyed data as JSON and the JSON body path deliberately does no bracket expansion; and nested objects: the multipart and urlencoded parsers expand bracket names). The server now synthesizes dotted-canon field aliases ALONGSIDE the raw keys (originals kept, so `$name` cross-field tokens keep resolving off the raw posted names, and an all-flat payload synthesizes nothing — byte-identical behaviour), then folds alias outcomes back at egress: error keys return under the DOM-name bracket form the client renders against, and the validated data output keeps its materialized shape with exclusions and transforms applied (a parent object emptied by an exclusion is pruned along that alias's path only — a posted empty object survives). The client join was always bracket-on-both-sides (a named rule set passes through with its authored keys; nested-authored sets are reconstructed to bracket names at bind time), so this brings the server to parity — quirks included: a caller that posts the dotted key form keeps its own addressing, and the no-rules path still returns the payload verbatim. Behaviour change by design: a bracket-keyed or nested-authored rule that never fired before now enforces — anything relying on the old silent skip starts rejecting or dropping those fields.
|
|
895
896
|
|
|
896
897
|
210. **Multipart upload config is ENFORCED — groups, destination, limits, text-field capture + caps, terminal states (consolidates former #187/#188/#228/#230's server half; #B49/#B50/#B51/#B92-adjacent/#B93/#B97).** Every uploaded file must map to a CONFIGURED upload group: the resolved group (no/empty → the default `untagged`) must exist in `settings.json upload.groups` or the request is rejected 400 BEFORE the temp file is created, and `untagged` obeys its own config like any named group (pre-#B50 the checks ran only for defined non-`untagged` groups — an allow-list bypass; the shipped `untagged` default is `allowedExtensions: '*'` + `isMultipleAllowed: true`, so configure `untagged` restrictively or a client can route around a named group's allow-list). Destination + limits honoured: files stream to `uploadDir || tmpPath || os.tmpdir()` with a per-group `path` override and mkdir-if-missing before `createWriteStream` (the mkdir itself crash-guarded → 500, #B145); `maxFields` caps the per-request file COUNT (400 past the cap; 0/unset disables); `maxFieldsSize` parses its unit suffix (B/KB/MB/GB; bare number = MB) as the whole-body cap (431). **Multipart TEXT fields are captured** — a real busboy `'field'` listener (busboy silently SKIPS all non-file parts when none is registered) exposes them on `req.body` + `req[method]` (POST/PUT/PATCH only), values VERBATIM (no url-decode, no `"true"/"false"/"on"/"null"` coercion — the JSON body contract, deliberately: otherwise the same client `send(fd)` call would change value TYPES with file presence), bracket-notation names nested through the urlencoded path's own layer, duplicate plain names last-wins; caps `upload.maxTextFields` (default 1000) + `upload.maxTextFieldSize` (default 1MB, unit-aware, explicit 0 = no limit) answer 400 on breach instead of busboy's silent skip/truncation. **A multipart request with no successfully-parsed file part reaches a TERMINAL state:** fields-only → the request resumes; malformed/empty → 400 via `busboy.on('error')` with a double-response guard (busboy terminates in exactly `finish` XOR `error`; pre-#B93/#B97 both were unauthenticated pre-routing DoS holes — an eternal hang, and an uncaughtException → SIGTERM bundle kill). Rules: a per-group restriction is only a control if the UNCONFIGURED/default case is denied or constrained, never waved through; a documented config key is only real if a code path reads it — grep the consumer before assuming a setting works; any stream/parser whose SUCCESS path drives a request's continuation must ALSO handle its error/empty terminals. Server-side only. Tests: `test/core/upload-groups.test.js` + `upload-config.test.js` + `multipart-nonfile-terminal.test.js` + `multipart-field-capture.test.js` (+ the client `send(FormData)` half: `validator-send-formdata-multipart-fields.test.js`).
|
|
897
898
|
|
|
@@ -923,7 +924,7 @@ Dev-mode query instrumentation captures every database query tied to the current
|
|
|
923
924
|
|
|
924
925
|
239. **Native schema/DTO builder `lib/dto` (`require('gina').dto`) — JSON-Schema-canonical, three projections (#DTO1 primitive, 0.5.18-alpha.2).** Author a data shape ONCE: `dto.object({ email: dto.string().email().required(), age: dto.integer().min(0).max(120), role: dto.enum(['admin','user']).required() }).as('CreateUser')`. Projects to (a) dialect-aware JSON Schema — `.toJsonSchema('draft-07'|'2020-12'[,{standalone}])`, the IDENTITY/canonical form for OpenAPI 3.1 requestBody/responses + MCP inputSchema; (b) `.toRules()` — the LIVE form-validator rules-object (the SAME engine routing `validator::` + `forms/rules/*.json` use, so a DTO unifies validation rather than adding a parallel validator; measured returning `{isValid():fn, error:{field:{rule:msg}}, data:coerced}` with coercion + `exclude` stripping); (c) `.name` — stable id for the type generator + a route's `param.dto`. Curated vocabulary EXCLUDES `toFloat` (server crash, `form-validator.js:1514`) + `query` (spins a Controller). Zero runtime deps, CJS/`var`; named registry on `process.gina._dtos` (survives dev hot-reload — plain-`require`d in `lib/index.js` for stable `instanceof`). **⚠️ VALUE-RANGE (`.min()/.max()`) is SCHEMA-ONLY in this cut** — carried as `minimum`/`maximum` for OpenAPI/MCP fidelity but NOT runtime-enforced: the `is` evaluator (`form-validator.js:1115`) accepts only a single binary comparison (no `&&`, no `this.value`) and a `$field` self-ref CRASHES the server form-body path (`main.js:7166-7172` reads `$fields.count()` on the null server `$fields` — a #B85-sibling), so `toRules()` compiles NO `$` of its own; runtime value-bound = a follow-on (a first-class engine rule, browser-bundled → dist rebuild). **⚠️ But an AUTHORED `$` can still reach the rules** — via an enum VALUE (`dto.enum(['$100'])`), a field NAME, or a date mask — and ANY `$` in the stringified rules sends the engine down that same null-`$fields` crash path (measured), so `toRules()` now THROWS on one (`toJsonSchema()` is deliberately NOT guarded — a `$` is valid in a JSON Schema enum, so a currency-style DTO still documents and still serves as a `param.responseDto`). **⚠️ A required field the client OMITS entirely PASSES** (isRequired fires only on present-but-empty, not a missing key) — a server-side validation driver MUST inject `''` for rule-declared fields absent from the payload before validating. Also un-collapses inline `validator::{...}` routing requirements into real JSON Schema via `lib.routingIntrospect.requirementToSchema()` (pure; the OpenAPI `.*` fix). Consumed by `bundle:openapi`/`bundle:mcp` (#DTO1c) + the default-on #DTO2 422 validation pipe (next). Server-side — NO dist rebuild. Tests: `test/lib/dto.test.js` (37, behavioural-through-the-real-engine + subtract) + `routing-introspect.test.js`.
|
|
925
926
|
|
|
926
|
-
240. **DTOs drive `bundle:openapi` + `bundle:mcp` schemas via a `dtos/<Name>.js` bundle convention (#DTO1c, 0.5.18-alpha.2).** A route declares `param.dto` / `param.responseDto` (names) in `routing.json`; the generators resolve each to `<bundle>/dtos/<name>.js` via `dto.load(bundleSrcPath, name)` — a new `lib.dto` method: require the file → `mod instanceof DtoObject ? mod : mod(dto)` (factory) → stamp+register → registry-fallback → null; fail-soft (a broken factory THROWS, the caller warns+skips), no `require.cache` eviction. **⚠️ A bundle DTO file MUST use the FACTORY shape `module.exports = function (dto) { return dto.object({...}, 'Name'); }`, NOT `require('gina').dto`** — because the OFFLINE `bundle:openapi`/`bundle:mcp` bootstrap via the lib registry (`bin/cli`), NOT `core/gna.js`, so a `require('gina')` inside a DTO file cold-loads gna.js with no bundle context and THROWS at `core/gna.js:174` (`ctxObj.paths` null). `require('gina').dto` works only at request-time in a booted bundle; the factory works in both (measured — the naive shape's offline throw was reproduced, the factory proven end-to-end). openapi (3.1 → JSON Schema 2020-12): `param.dto` on post/put/patch → `requestBody` + a `422`; `param.responseDto` → the 200 `content`. mcp (draft-07): `param.dto` → `inputSchema.body` on non-GET (else the lenient placeholder); `param.responseDto` → `outputSchema` (the runtime MCP server already passes it through). Both also un-collapse an inline `validator::{...}` URL-param requirement into a real schema fragment (the `.*`-collapse fix). Server-side/CLI-only — NO dist rebuild. Verified end-to-end against the real offline CLI (a scaffold bundle emitted a real requestBody / inputSchema.body / outputSchema / un-collapsed param). Tests: `test/lib/{bundle-openapi,bundle-mcp}.test.js` (source-pins + a source-locked replica through the real dto + introspect) + `dto.test.js §08` (dto.load). **Response-side emission drops `.exclude()`d fields (#B110, 0.5.18-alpha.2):** both response sites emit `toJsonSchema(dialect, { dropExcluded: true })` — an excluded field can never reach the wire (the render transform deletes it before the single stringify), so the 200 `content` / `outputSchema` must not advertise it in `properties` NOR `required[]` (an emptied `required` is deleted); request-side `requestBody`/`inputSchema.body` keep the declared shape (the client DOES send it). The drop reads the SHAPE (`_excluded`), never `toRules()` (stays total for an authored-`$` DTO), and the render-json dev missing-required warn checks the same response projection — a `.required().exclude()` field an action omits no longer warns. Tests: `dto.test.js §11` + `bundle-openapi.test.js 01.7/02.9` + `bundle-mcp.test.js 02.9` + `render-json.test.js §06`. **#MS4 (2026-07-24, unreleased) adds the authorization contract to `bundle:openapi`:** gated routes (`param.requireAuth === true`, a non-empty `param.roles`, or a non-empty `param.policy` — the exact runtime authz-gate predicate; a truthy-STRING `requireAuth` does not gate, matching the runtime) emit a `401` response entry (+ a `403` only when roles/policy add authorization beyond authentication — role/policy NAMES never reach the spec, mirroring the runtime's generic 403 bodies and client-map stripping), and when machine-caller auth is EFFECTIVELY configured (`auth.machine.enabled === true` strictly AND callers non-empty OR authenticator set — fail-closed, a `"true"` string emits nothing) the spec gains `components.securitySchemes.bearerAuth` (`http`/`bearer` — matching the machine 401's own `WWW-Authenticate: Bearer` challenge; its description notes a custom authenticator may accept other credential shapes and that a signed-in session also satisfies gated routes) + per-operation `security: [{bearerAuth: []}]` on gated routes only, never a top-level `security`. NO session-cookie scheme: the cookie name is app-owned (set in app code, unreadable offline) — session-only gated routes carry the credential story in the 401 description instead. Enabler: the previously-dormant settings.json read switched from a plain `require` (throws on the comment lines real settings.json files carry, silently leaving `settings` null) to `requireJSON` + a parse-failure warn. An un-gated, un-configured bundle's spec is byte-unchanged. Tests: `bundle-openapi.test.js` §03-§04 (source pins + executing the shipped helper bytes via control-gated extraction, incl. the role-name-absence and truthy-string-fail-closed cases).
|
|
927
|
+
240. **DTOs drive `bundle:openapi` + `bundle:mcp` schemas via a `dtos/<Name>.js` bundle convention (#DTO1c, 0.5.18-alpha.2).** A route declares `param.dto` / `param.responseDto` (names) in `routing.json`; the generators resolve each to `<bundle>/dtos/<name>.js` via `dto.load(bundleSrcPath, name)` — a new `lib.dto` method: require the file → `mod instanceof DtoObject ? mod : mod(dto)` (factory) → stamp+register → registry-fallback → null; fail-soft (a broken factory THROWS, the caller warns+skips), no `require.cache` eviction. **⚠️ A bundle DTO file MUST use the FACTORY shape `module.exports = function (dto) { return dto.object({...}, 'Name'); }`, NOT `require('gina').dto`** — because the OFFLINE `bundle:openapi`/`bundle:mcp` bootstrap via the lib registry (`bin/cli`), NOT `core/gna.js`, so a `require('gina')` inside a DTO file cold-loads gna.js with no bundle context and THROWS at `core/gna.js:174` (`ctxObj.paths` null). `require('gina').dto` works only at request-time in a booted bundle; the factory works in both (measured — the naive shape's offline throw was reproduced, the factory proven end-to-end). openapi (3.1 → JSON Schema 2020-12): `param.dto` on post/put/patch → `requestBody` + a `422`; `param.responseDto` → the 200 `content`. mcp (draft-07): `param.dto` → `inputSchema.body` on non-GET (else the lenient placeholder); `param.responseDto` → `outputSchema` (the runtime MCP server already passes it through). Both also un-collapse an inline `validator::{...}` URL-param requirement into a real schema fragment (the `.*`-collapse fix). **#B201 (0.6.3) closed three forms that un-collapse silently DROPPED:** the scalar `"isString": N` now emits `minLength` (it was array-only, so even the docs' own scalar example produced nothing), and `isInteger`/`isNumber` digit bounds now emit a `description` + a namespaced `x-gina-digitBounds` `{min?,max?}` extension. Those bounds constrain the value's STRING-FORM length (a negative sign counts), not its range, so they are deliberately NOT mapped to `minimum`/`maximum` — wrong for every negative, since digits `[2,4]` admit `[-999,-1] ∪ [10,9999]`, which no single range expresses — nor to `minLength`/`maxLength`, which are string-only keywords a validator ignores on a numeric type, i.e. the schema-level twin of #B198's "a documented bound nothing enforces". Both generators copy fragment keys verbatim, so annotations flow with zero consumer change and bare-`true` rules stay byte-identical. Server-side/CLI-only — NO dist rebuild. Verified end-to-end against the real offline CLI (a scaffold bundle emitted a real requestBody / inputSchema.body / outputSchema / un-collapsed param). Tests: `test/lib/{bundle-openapi,bundle-mcp}.test.js` (source-pins + a source-locked replica through the real dto + introspect) + `dto.test.js §08` (dto.load). **Response-side emission drops `.exclude()`d fields (#B110, 0.5.18-alpha.2):** both response sites emit `toJsonSchema(dialect, { dropExcluded: true })` — an excluded field can never reach the wire (the render transform deletes it before the single stringify), so the 200 `content` / `outputSchema` must not advertise it in `properties` NOR `required[]` (an emptied `required` is deleted); request-side `requestBody`/`inputSchema.body` keep the declared shape (the client DOES send it). The drop reads the SHAPE (`_excluded`), never `toRules()` (stays total for an authored-`$` DTO), and the render-json dev missing-required warn checks the same response projection — a `.required().exclude()` field an action omits no longer warns. Tests: `dto.test.js §11` + `bundle-openapi.test.js 01.7/02.9` + `bundle-mcp.test.js 02.9` + `render-json.test.js §06`. **#MS4 (2026-07-24, unreleased) adds the authorization contract to `bundle:openapi`:** gated routes (`param.requireAuth === true`, a non-empty `param.roles`, or a non-empty `param.policy` — the exact runtime authz-gate predicate; a truthy-STRING `requireAuth` does not gate, matching the runtime) emit a `401` response entry (+ a `403` only when roles/policy add authorization beyond authentication — role/policy NAMES never reach the spec, mirroring the runtime's generic 403 bodies and client-map stripping), and when machine-caller auth is EFFECTIVELY configured (`auth.machine.enabled === true` strictly AND callers non-empty OR authenticator set — fail-closed, a `"true"` string emits nothing) the spec gains `components.securitySchemes.bearerAuth` (`http`/`bearer` — matching the machine 401's own `WWW-Authenticate: Bearer` challenge; its description notes a custom authenticator may accept other credential shapes and that a signed-in session also satisfies gated routes) + per-operation `security: [{bearerAuth: []}]` on gated routes only, never a top-level `security`. NO session-cookie scheme: the cookie name is app-owned (set in app code, unreadable offline) — session-only gated routes carry the credential story in the 401 description instead. Enabler: the previously-dormant settings.json read switched from a plain `require` (throws on the comment lines real settings.json files carry, silently leaving `settings` null) to `requireJSON` + a parse-failure warn. An un-gated, un-configured bundle's spec is byte-unchanged. Tests: `bundle-openapi.test.js` §03-§04 (source pins + executing the shipped helper bytes via control-gated extraction, incl. the role-name-absence and truthy-string-fail-closed cases).
|
|
927
928
|
|
|
928
929
|
241. **`helpers/context.js` `getLib`/`getConfig` no longer crash with `Cannot read properties of undefined (reading 'conf')` on a PARTIAL Config (module-load / daemon-spawned bootstrap) — the resolver-layer sibling of the #236 detached-`throwError` fix (0.5.18-alpha.2).** `core/config.js` populates `envConf` (`Env.load`, `:391`) BEFORE `bundlesConfiguration` (assigned at `:243-250` only after `loadBundlesConfiguration` succeeds), and `bundlesConfiguration.conf` is an ALIAS of `envConf` (`:247` `conf: self.getInstance()`). When the async Config build ABORTS before completing — `config.js:223 if (err) { …; setTimeout(() => process.exit(1), 0); return; }` returns BEFORE the `:243` assignment (canonically a fail-closed `secrets.resolve` throws on an unset `${secret:KEY}` at `:2811`) — a detached caller (a bundle `onInitialize`, a cron, a bootstrap `getLib()`) observes a PARTIAL Config: `envConf` valid, `bundlesConfiguration` + `Config.initialized` undefined. `getLib` (`:600`) and both `getConfig` branches (`:465` with-confName, `:478` without) dereferenced `conf.bundlesConfiguration.conf[bundle][env]…` UNCONDITIONALLY there → the `reading 'conf'` TypeError → the #236 detached-path `throwError` re-threw → uncaughtException → SIGKILL, MASKING the real boot error. **Grep-PROVEN surface (not whack-a-mole):** a framework-wide grep of `bundlesConfiguration` reads (`core/ helpers/ lib/`, excl. dist) — the ONLY unguarded live consumers are `getLib` + `getConfig`; `config.js`'s own reads are guarded (`:3140 if (Config.initialized && self.bundlesConfiguration)`) or during-build; `lib/routing/src/main.js:1075` is commented-out (dead); 14/16 derefs are `.conf` (=== envConf). **Fix:** ONE shared `resolveBundlesConf(conf)` (`bundlesConfiguration.conf` when built, else the equivalent `envConf`, else null) consumed by `getLib` + BOTH `getConfig` branches so the two siblings cannot drift; PLUS fail-clean — `getConfig`'s two catches, `getLib`'s outer catch and its unresolvable-else all pass `throwError(…, isFatal=true)` (emerg-log + return, never a bare throw that escalates to SIGKILL). Byte-identical on the healthy path (bundlesConfiguration present); only the previously-crashing partial-Config path changes. **The partial Config is usually a SYMPTOM** — something upstream aborted the build (canonically a fail-closed `${secret:KEY}` resolution) — so hardening the resolvers UN-MASKS that real, actionable error (e.g. `Secret resolution failed for <KEY>`) instead of hiding it behind a `reading 'conf'` crash; the abort itself (e.g. secrets not wired for a deploy) is the caller's config to fix. Rule: a global config resolver reachable from a detached / module-load context must accept a partial Config (bundlesConfiguration may not be built yet; fall back to the aliasing envConf) and fail clean when neither source is usable. Server-side only — NO dist rebuild. Tests: `test/core/context-config-resolution.test.js` (source pins on the shared resolver + both resolvers routed through it + isFatal catches; full/partial/broken replicas; a subtract reproducing the exact crash pre-fix). Commit `b2dc67da`, established 2026-07-14.
|
|
929
930
|
|
|
@@ -941,13 +942,13 @@ Dev-mode query instrumentation captures every database query tied to the current
|
|
|
941
942
|
|
|
942
943
|
250. **The module-level `gina.emit` (`require('gina').emit`) is an inert stub — always returns `false`, never dispatches, never throws; the module object is a plain literal with NO `on`/`once`, so it is not an event surface — application events go through the controller's `self.emitEvent()` (#B109, fixed 2026-07-16).** Pre-fix it was a DETACHED copy of the internal lifecycle emitter's `emit` (`this` at call time = the plain module object, no `_events`): it returned false for every name EXCEPT `'error'`, which THREW its argument via Node's unhandled-`'error'` path (ERR_UNHANDLED_ERROR when called bare) — a synchronous crash landmine on the most guessable call of an error-reporting-shaped API. Binding it instead (`e.emit.bind(e)`) was REJECTED by consumer survey: zero callers existed anywhere (framework, services, scaffolding boilerplate, every known consumer), and binding would have opened dispatch INTO the framework's internal lifecycle listeners (a consumer `emit('complete')` would re-run the server-start continuation with a bogus instance; `emit('error')` would invoke the framework error handler with undefined request/response). Rule: a detached EventEmitter method copy (`x.emit = emitter.emit`) is never a working event API — `this` rebinds at the call site; either `.bind()` it deliberately (only with a listener surface and real demand) or stub it inert.
|
|
943
944
|
|
|
944
|
-
251. **Route authorization gate — `param.requireAuth` / `param.roles` / `param.policy` (#COMPLY1 slices 1-3, 2026-07-16/17, unreleased).** A route requires an authenticated session by declaring `"requireAuth": true` in its routing.json `param` block; `lib/authz-gate` (plain-required in lib/index.js, the dtoPipe precedent) enforces it at BOTH `core/router.js` dispatch sites immediately BEFORE `dtoPipe.validateRequestPayload` — the invariant is **401/403 → 422 → controller** (an unauthenticated caller must never learn whether its payload would have validated; a 422 field map is a disclosure). Strict NO-OP for any route that declares nothing. **Contract: authenticated ⇔ `req.session.user` truthy** — read DIRECTLY, never through router.js's conditionally-installed Passport `request.isAuthenticated()` shim (zero framework callers, measured; it itself converges onto `request.session.user`). **The placement decision that dissolved the dist rebuild:** `param.requireAuth`, NOT a top-level csrfExempt-style flag. `param` rides the existing whole-param clone on both cold (`server.js` `param:JSON.clone(routing[name].param)`) and warm (`getCached`) paths, so a new `param.*` key needs ZERO `lib/routing` edits — measured empty dist-pathspec status, `lib/routing` untouched, no bundle-freshness exposure (the `param.dto` precedent; the top-level `csrfExempt` shape would have cost the two-site propagation edit + a prod dist rebuild because `lib/routing` is browser-bundled). Rule: a new per-route flag belongs in `param` unless it genuinely needs to be top-level (COMPLY6 `rateLimit` should default the same). **On-fail:** 401 `throwError({status:401,error:'Authentication required'})` by default; for a BROWSER navigation with a configured `settings.json > auth.loginRoute` AND an existing `req.session`, a hand-emitted login bounce — NOT `controller.redirect()`, which defaults to a **cacheable 301** (`req.routing.param.code || 301`) with a dev-or-proxied-GATED no-store (#B68), i.e. a login LOOP on a direct-prod deployment (the browser replays the 301 for the later authenticated visit). The gate emits it directly, mirroring redirect()'s exit (`res.writeHead(code,headInfos)` on both engines + `res.end(JSON.stringify({status,headers}))` — the body the inter-bundle `query()` 3xx intercept parses — + the `console.info('<METHOD> [302] <path>')` pod-log line) with the status FORCED to 302 and the no-store set UNCONDITIONAL. The request is `pauseRequest`-snapshotted first so the login action replays it via `self.resumeRequest()`. **The bounce needs all THREE** (else 401): a configured target; NOT an XHR (`req.isXMLRequest` — an XHR follows a Location transparently and would get the login PAGE as its body; measured a bundle with no Session plugin → 401s, never bounces because pauseRequest 424s without a session). **Boot-resolve + fail-fast lint** in `core/server.js` (the `_dtos`/`_adminAllowList` mould): `process.gina._authConf = {loginRoute}` resolved ONCE (O(1) request read, no per-request config clone an unauth caller could amplify); a non-boolean `requireAuth` (a truthy STRING is the motivating case — the gate tests `=== true`, so `"requireAuth":"true"` would silently NOT gate), a non-object `auth`, an unknown/multi-url/parameterized `loginRoute` all REFUSE TO BOOT (#B57 shape). **Two traps a unit test could not catch, caught by the live daemonless boot:** (a) `core/config.js:2086-2091` re-keys every rule to `<rule.toLowerCase()>@<bundle>` at normalisation — a lookup of the declared name (`login`) misses, must be `login@web` (the resolve accepts both); (b) `config.js:2138` already composes the bundle webroot INTO `routing[rule].url`, so a rule-name loginRoute resolves to the real served path for free (`login` → `/web/login`), while an absolute path is verbatim — either way root-relative ⇒ same-origin ⇒ clear of the #B65/#B66/#B67 proxy-host family. Server-side only (no dist rebuild); `settings.json > auth` change needs a bundle restart (boot config). Live-verified matrix (ungated 200 / browser-bounce 302+no-store / snapshot / signed-in 200 / XHR 401 / no-Session-plugin 401 / bad-loginRoute boot-emerg). Slice 2 adds `param.roles: ["admin","editor"]` — ANY-of match against `req.session.user.roles` (opaque strings, no framework vocabulary, no role→permission indirection in v1), which IMPLIES requireAuth (evaluation order authN → roles: unauthenticated → the 401/bounce, authenticated-but-role-less → a 403 whose body stays GENERIC — the required roles are never echoed to the wire; the denial detail goes to a server-side console.debug line naming the rule). The boot lint refuses every declared-but-invalid `roles` shape (null / bare string / empty array / non-string or empty-string members — each would otherwise be silently ungated, the truthy-string class). And the boot-built client routing maps (`/_gina/assets/routing.json` — the full map AND the #B66 host-stripped variant, which is cloned FROM the full map AFTER the strip so both inherit it) no longer ship `requireAuth`/`roles`/`policy` to the browser: they name the authorization model, and the client bundle reads none of them (measured: zero readers in src and in the built artifacts). **Slice 3 adds `param.policy: "<name>"`** — the record/attribute escape hatch RBAC cannot express (ownership etc.): `<bundle>/policies/<name>.js` exporting `module.exports = function (user, req) { return boolean; }`, registered at BOOT via `lib.authzGate.registerPolicy(bundleSrcPath, name)` (the `lib.dto.load` mould — returns null when unresolved, THROWS when present-but-broken, caller adds route context) onto `process.gina._policies`, so the request path is an O(1) lookup — no fs, no re-require (which would also feed the dev-mode `module.children` leak class #B32). IMPLIES requireAuth and AND-composes AFTER roles (**authN → roles → policy**; a role denial never runs the policy). **The signature is SYNCHRONOUS because the GATE is** (`authorizeRequest` returns a boolean both `router.js` sites branch on directly), and TWO rules make that safe rather than merely convenient — the measured asymmetry is the whole design: a native `async function` policy reads `constructor.name === 'AsyncFunction'` (the ONLY boot-visible tell — `typeof` says `'function'` for both, measured) so it is **refused at boot**; but a policy that merely RETURNS a promise (the transpiled-async shape) reads `constructor.name === 'Function'` and is **boot-INVISIBLE**, so the allow test is strictly `=== true` — a promise is truthy but not `true`, and a **truthy-allow gate would ALLOW it unconditionally: the quietly-OFF class INVERTED into fail-OPEN** (live-measured: a promise-returning policy whose answer was `true` boots fine and then 403s every request, warning ONCE; a `!!allowed` gate would have allowed a request the policy explicitly DENIED). A non-boolean return warns once per policy naming it (a per-request warn would bury the message it exists to surface); a THROWING policy denies 403 + `console.error` and never 500s the wire nor kills the bundle. Every slice-3 denial keeps the generic 403 — the **policy name never reaches the wire** (measured live: 0 occurrences of the policy name, the rule name, or even the policy's own thrown error text in the 403 body, against a `Forbidden`-fires-twice control); the detail goes to the server-side `[ authz ] denied \`<rule>\` — policy \`<name>\` did not allow|threw` line. Boot lint refuses a non-string/empty `policy`, a missing file, a non-function export and an async policy (#B57 shape) — all four live-verified `exit=1`. `self.hasRole(role)` is the imperative escape hatch for an action authorizing mid-logic: it **delegates to the gate's exported `hasAnyRole`** rather than re-reading `user.roles` itself (one definition of "holding a role" framework-wide — the byte-identical-copy-paste class `lib/cmd-status-format`/`lib/json-config-header` were extracted to end) and carries the #B35 released-response guard (`local.req == null` → false); its `types/index.d.ts` SuperController member is MANDATORY — the #DTO3b parity gate diffs the interface against a real instance. Server-side only, no dist rebuild (slice 2 already strips `policy` from the client blobs). Tests 65→110 (`test/lib/authz-gate.test.js` §11-§13; the roles-before-policy ordering pin + the delegation pin both validated can-fail against verified-changed perturbed copies). **#MS3 (2026-07-24, unreleased) adds MACHINE-CALLER authentication to the same gate** — service-to-service callers that cannot hold a session: `settings.json > auth.machine` (STRICTLY opt-in; `enabled` boolean-linted, fail-closed) declares named callers (`"callers": { "<name>": { "key": "<${secret:KEY}-capable>", "roles": [...] } }`); a GATED session-less request presenting `Authorization: Bearer <key>` is verified against a BOOT-precomputed sha256 hash per caller (on `process.gina._authConf.machine` — the request path compares fixed-length digests via `crypto.timingSafeEqual`: hash-BOTH-sides so no length oracle, raw keys not retained past boot, an invalid token pays the full caller scan) and becomes the effective principal `{ name, roles, machine: true }` stamped on `req.machineCaller` — it satisfies `requireAuth`, its configured roles ride the SAME ANY-of match, policies receive it as their `user` argument (discriminate via `user.machine === true`), audit actors snapshot `{ key: <callerName>, roles, machine: true }`, and `self.hasRole()` answers its roles. SESSION WINS (a signed-in request never consults the machine path); resolution is LAZY (un-gated routes never pay a compare); `enabled: false` and a pre-#MS3 `_authConf` shape are byte-identical legacy (both subtract-locked). A presented-but-invalid Bearer gets a clean 401 + `WWW-Authenticate: Bearer` and NEVER the login bounce (the '401-machine' audit outcome); a custom-scheme miss degrades to the ordinary 401/bounce — the machine 401 stays Bearer-specific. **Deliberately NO per-route opt-in key**: per-route granularity rides `roles` (keep a route human-only by requiring a role no caller holds, machine-only by one only callers hold). The escape hatch — `auth.machine.authenticator` names `<bundle>/authenticators/<name>.js` (the `policies/<name>.js` shape applied to authN): a SYNCHRONOUS `function (req) { return { name, roles } | null; }` run AFTER the built-in map (first success wins) and REGARDLESS of Bearer presence, so it verifies any sync-checkable credential (a sync-`verify` JWT, an HMAC header, an x-api-key); its return is NORMALIZED (`machine: true` forced, non-array roles collapse to `[]`), an `async function` module REFUSES the boot (the policy asymmetry: the transpiled-async promise shape is boot-invisible, which is why the runtime shape check stays strict), and a throw or malformed return at request time is fail-closed unauthenticated (warn-once), never a 500. `schema/settings.json` gains the previously-undeclared `auth` block (loginRoute + machine). Server-side only, no dist rebuild; `auth.machine` is boot config (restart to change). Tests: `test/lib/authz-gate-machine.test.js` (52 — session-wins + never-bounce decisive fixtures, both subtracts, the hash-of-key non-admission control, map-first spy proof, real-audit machine actors, controller hasRole via createTestInstance). **#COMPLY10 (2026-07-25, unreleased) adds an opt-in DENY-BY-DEFAULT mode to the same gate:** `settings.json > auth.requireAuthByDefault: true` INVERTS the strict-NO-OP above — an un-annotated route is GATED, and `param.public: true` is the explicit exemption. `public` is tested INSIDE the un-annotated branch, so precedence is STRUCTURAL: a route carrying `public` alongside an explicit gate key never reaches that branch and stays gated, i.e. a hand-built or tampered config still fails CLOSED without relying on the lint having run. It is a `param.*` key for the same measured reason `requireAuth` was (rides the whole-param clone on both cold and warm paths ⇒ zero `lib/routing` edits, no dist rebuild — a top-level key would have cost the two-site propagation edit AND a prod rebuild), and a POSITIVE marker rather than a reused `requireAuth: false`, so a reviewer can tell an audited exemption from a leftover (the `csrfExempt` rationale). **The mode is keyed PER BUNDLE** (`_authConf.byBundle`, read via `req.routing.bundle`) — merged mode runs every bundle in ONE process and `_authConf` is written once per `init()`, so a FLAT flag would let whichever bundle booted LAST decide the posture for all of them, silently un-gating a bundle that had opted in (a fail-OPEN; the write now merges into the existing map instead of replacing it). ⚠️ `public` is a strict-mode RESERVED WORD, so it cannot join the isaac client-map strip as a bare destructured binding (`const { public, ... }` is a SyntaxError — only a renamed binding compiles); it is stripped with a separate `delete cleanParam.public;`, which also leaves the pinned #COMPLY1 destructuring line byte-identical. Boot refuses three mode-specific contradictions: `public` + an explicit gate key (same axis, opposite answers, no safe precedence); a login route the mode would gate (it bounces to ITSELF — an infinite redirect that locks out every visitor, so a rule-name target must be marked `public` and an unresolvable absolute path refuses, while NO `loginRoute` only WARNS since a pure API/machine bundle legitimately 401s everything); and **a mode-gated route that also declares `cache`** — both render-cache serve points run BEFORE the gate and `buildKey` is `<release>:<kind>:<bundle>:<url>` with NO principal component, so a cached gated route replays the first authenticated body to every later anonymous caller (scoped to the mode, so an EXPLICITLY gated cached route is not newly refused — that pre-existing exposure is tracked separately). The five framework-injected routes (`webroot@<bundle>` — which also matches `/` under `webrootAutoredirect` — `custom-error-page`, `bundle-status`, and both upload routes) ship `param.public: true`, so flipping the mode cannot take the site root or the error renderer offline. `bundle:openapi` threads the mode into its gated predicate, so the emitted spec can never declare a newly-gated route unauthenticated. Server-side only, no dist rebuild (measured: `authorizeRequest` appears 0× in the built browser artifact against a firing `csrfExempt` control); boot config, restart to change. **#B158 — a GATED route may not also declare `cache`, however it is gated (0.5.26).** Both render-cache serve points run BEFORE the gate — the engine-agnostic read serves and RETURNS ahead of `router.route` (inside which the gate runs), and isaac's runs pre-routing, before `req.routing` exists — and `buildKey` composes `<release>:<kind>:<bundle>:<url>` with NO principal component, so a cached gated route replayed the first authenticated caller's rendered body verbatim to every later anonymous one (reproduced live on an isolated boot: a Bearer-authenticated render, same body and same render nonce, served to an unauthenticated caller, while the identical route WITHOUT `cache` correctly 401'd). The boot lint now REFUSES the pairing in both modes (`auth.requireAuthByDefault` had covered only mode-gated routes; the explicitly annotated case it left open was the pre-existing hole) — **ACTION REQUIRED: such a bundle will not start** until `cache` is dropped or the route opened. Second layer: `lib/authz-gate` exports `isRouteGated(req)` (the one runtime answer — explicit key, else `param.public`, else the per-bundle mode) and all three render delegates consult it inside their `writeCache` guard, so a config that never passed the lint (hand-built, tampered, mutated at runtime) still fails CLOSED. **#B159 — an authorization key on a `method: "ws"` route REFUSES the boot (0.5.26).** A ws route is registered on the engine's extended-CONNECT handler and never reaches `handle()`/`router.route`, so `requireAuth`/`roles`/`policy` on one are unenforceable BY CONSTRUCTION — and it previously linted clean, booted, AND was counted in the `Registered N authorization-gated route(s)` line, so the framework actively CONFIRMED an illusion. Refused now (the truthy-string-`requireAuth` precedent: a structurally-OFF gate must refuse exactly like a silently-OFF one), keyed on `/^ws$/i` — byte-identical to the ws registrar's own predicate so the two can never disagree — placed after the policy block so one check covers all three keys. Remedy: authenticate in the `wsHandler`, which receives the full request. The MODE half is a WARN, not a refusal (an explicit key is an author asserting something false; `requireAuthByDefault` sweeps up every un-annotated route, so refusing would break a working ws bundle the moment the mode is switched on): ws routes are excluded from the deny-by-default tally and NAMED at boot instead. Live-verified both arms (refuses with the key, boots without). Tests: `authz-gate.test.js §17` (8 — replica + case-insensitivity + the `websocket`-is-not-`ws` boundary + source pins) and `§15` (20 — per-bundle isolation, structural precedence, a mode-OFF subtract-control, and a boot-lint replica; all 11 source pins + the headline behaviour red-armed against pre-fix source).
|
|
945
|
+
251. **Route authorization gate — `param.requireAuth` / `param.roles` / `param.policy` (#COMPLY1 slices 1-3, 2026-07-16/17, unreleased).** A route requires an authenticated session by declaring `"requireAuth": true` in its routing.json `param` block; `lib/authz-gate` (plain-required in lib/index.js, the dtoPipe precedent) enforces it at BOTH `core/router.js` dispatch sites immediately BEFORE `dtoPipe.validateRequestPayload` — the invariant is **401/403 → 422 → controller** (an unauthenticated caller must never learn whether its payload would have validated; a 422 field map is a disclosure). Strict NO-OP for any route that declares nothing. **Contract: authenticated ⇔ `req.session.user` truthy** — read DIRECTLY, never through router.js's conditionally-installed Passport `request.isAuthenticated()` shim (zero framework callers, measured; it itself converges onto `request.session.user`). **The placement decision that dissolved the dist rebuild:** `param.requireAuth`, NOT a top-level csrfExempt-style flag. `param` rides the existing whole-param clone on both cold (`server.js` `param:JSON.clone(routing[name].param)`) and warm (`getCached`) paths, so a new `param.*` key needs ZERO `lib/routing` edits — measured empty dist-pathspec status, `lib/routing` untouched, no bundle-freshness exposure (the `param.dto` precedent; the top-level `csrfExempt` shape would have cost the two-site propagation edit + a prod dist rebuild because `lib/routing` is browser-bundled). Rule: a new per-route flag belongs in `param` unless it genuinely needs to be top-level (COMPLY6 `rateLimit` should default the same). **On-fail:** 401 `throwError({status:401,error:'Authentication required'})` by default; for a BROWSER navigation with a configured `settings.json > auth.loginRoute` AND an existing `req.session`, a hand-emitted login bounce — NOT `controller.redirect()`, which defaults to a **cacheable 301** (`req.routing.param.code || 301`) with a dev-or-proxied-GATED no-store (#B68), i.e. a login LOOP on a direct-prod deployment (the browser replays the 301 for the later authenticated visit). The gate emits it directly, mirroring redirect()'s exit (`res.writeHead(code,headInfos)` on both engines + `res.end(JSON.stringify({status,headers}))` — the body the inter-bundle `query()` 3xx intercept parses — + the `console.info('<METHOD> [302] <path>')` pod-log line) with the status FORCED to 302 and the no-store set UNCONDITIONAL. The request is `pauseRequest`-snapshotted first so the login action replays it via `self.resumeRequest()`. **The bounce needs all THREE** (else 401): a configured target; NOT an XHR (`req.isXMLRequest` — an XHR follows a Location transparently and would get the login PAGE as its body; measured a bundle with no Session plugin → 401s, never bounces because pauseRequest 424s without a session). **Boot-resolve + fail-fast lint** in `core/server.js` (the `_dtos`/`_adminAllowList` mould): `process.gina._authConf = {loginRoute}` resolved ONCE (O(1) request read, no per-request config clone an unauth caller could amplify); a non-boolean `requireAuth` (a truthy STRING is the motivating case — the gate tests `=== true`, so `"requireAuth":"true"` would silently NOT gate), a non-object `auth`, an unknown/multi-url/parameterized `loginRoute` all REFUSE TO BOOT (#B57 shape). **Two traps a unit test could not catch, caught by the live daemonless boot:** (a) `core/config.js:2086-2091` re-keys every rule to `<rule.toLowerCase()>@<bundle>` at normalisation — a lookup of the declared name (`login`) misses, must be `login@web` (the resolve accepts both); (b) `config.js:2138` already composes the bundle webroot INTO `routing[rule].url`, so a rule-name loginRoute resolves to the real served path for free (`login` → `/web/login`), while an absolute path is verbatim — either way root-relative ⇒ same-origin ⇒ clear of the #B65/#B66/#B67 proxy-host family. Server-side only (no dist rebuild); `settings.json > auth` change needs a bundle restart (boot config). Live-verified matrix (ungated 200 / browser-bounce 302+no-store / snapshot / signed-in 200 / XHR 401 / no-Session-plugin 401 / bad-loginRoute boot-emerg). Slice 2 adds `param.roles: ["admin","editor"]` — ANY-of match against `req.session.user.roles` (opaque strings, no framework vocabulary, no role→permission indirection in v1), which IMPLIES requireAuth (evaluation order authN → roles: unauthenticated → the 401/bounce, authenticated-but-role-less → a 403 whose body stays GENERIC — the required roles are never echoed to the wire; the denial detail goes to a server-side console.debug line naming the rule). The boot lint refuses every declared-but-invalid `roles` shape (null / bare string / empty array / non-string or empty-string members — each would otherwise be silently ungated, the truthy-string class). And the boot-built client routing maps (`/_gina/assets/routing.json` — the full map AND the #B66 host-stripped variant, which is cloned FROM the full map AFTER the strip so both inherit it) no longer ship `requireAuth`/`roles`/`policy` to the browser: they name the authorization model, and the client bundle reads none of them (measured: zero readers in src and in the built artifacts). **#B212 (2026-08-02, unreleased): the maps are built ONCE, engine-agnostically, by `buildClientRoutingAssets` in `core/server.js` and served under EVERY engine** — they used to be built and served by the isaac engine only, so an `engine: "express"` bundle answered the framework 404 for the URL the browser fetches at boot (`core.js` fetches it to populate the client routing table and, lacking a `response.ok` check, installed that 404 JSON body AS the table — the client-side guard is tracked separately as #B213). isaac consumes the pre-built maps via `serverOpt.clientRoutingAssets` for its precompressed-file fast-path (byte-identical serve, md5-measured across engines on one fixture); the engine-agnostic `onRequest` handler mirrors the isaac header set with a per-request #B65-twin proxied classification picking the #B66 variant. **Slice 3 (#SPA1, 2026-08-02, unreleased, operator-gated) rebuilt the cut as an ALLOWLIST with derived flags** — the old denylist meant every future route key shipped to anonymous browsers by default (schema/routing.json is `additionalProperties: true` at both levels; that is how `csrfExempt`, `param.dto` and `queryTimeout` reached the wire). The served map now carries ONLY the measured client contract: `url`/`method`/`webroot`/`bundle`/`hostname`(+`host` raw-only, per #B66), the `negotiate` capability flag, plain-regex `requirements` entries (`validator::` bodies withheld), URL-placeholder `param` bindings (a param entry ships iff the route's own url declares `:key`), and a derived `isRedirect` boolean replacing `param.control` (client `toUrl` reads `isRedirect === true` OR the server-side `param.control`, so both sides work; the `this.param &&` guard also fixed a latent throw on param-less routes). Dispatch keys (`param.control`/`file`/`path`/`title`), `namespace`, `scopes`, `cache` config, `csrfExempt`, `middlewareIgnored` are withheld — a DISCLOSED behaviour change; consumer code reading other keys off `gina.config.routing` must move server-side. Cross-bundle client `getRoute('rule@bundle')`/`toUrl` is measured-unaffected (the global map stays global — bundle-scoping was measured to break production cross-bundle form-rule query URLs and REVERSED at the gate). Delivery: per-variant weak ETag + `cache-control: no-cache` on BOTH engines (was `max-age=86400`) — one conditional GET (normally 304) per page boot closes the 24h post-restart staleness window; #B213 adds the client's missing `response.ok` guard so a non-2xx body can never again be installed as the routing table. **Slice 3 adds `param.policy: "<name>"`** — the record/attribute escape hatch RBAC cannot express (ownership etc.): `<bundle>/policies/<name>.js` exporting `module.exports = function (user, req) { return boolean; }`, registered at BOOT via `lib.authzGate.registerPolicy(bundleSrcPath, name)` (the `lib.dto.load` mould — returns null when unresolved, THROWS when present-but-broken, caller adds route context) onto `process.gina._policies`, so the request path is an O(1) lookup — no fs, no re-require (which would also feed the dev-mode `module.children` leak class #B32). IMPLIES requireAuth and AND-composes AFTER roles (**authN → roles → policy**; a role denial never runs the policy). **The signature is SYNCHRONOUS because the GATE is** (`authorizeRequest` returns a boolean both `router.js` sites branch on directly), and TWO rules make that safe rather than merely convenient — the measured asymmetry is the whole design: a native `async function` policy reads `constructor.name === 'AsyncFunction'` (the ONLY boot-visible tell — `typeof` says `'function'` for both, measured) so it is **refused at boot**; but a policy that merely RETURNS a promise (the transpiled-async shape) reads `constructor.name === 'Function'` and is **boot-INVISIBLE**, so the allow test is strictly `=== true` — a promise is truthy but not `true`, and a **truthy-allow gate would ALLOW it unconditionally: the quietly-OFF class INVERTED into fail-OPEN** (live-measured: a promise-returning policy whose answer was `true` boots fine and then 403s every request, warning ONCE; a `!!allowed` gate would have allowed a request the policy explicitly DENIED). A non-boolean return warns once per policy naming it (a per-request warn would bury the message it exists to surface); a THROWING policy denies 403 + `console.error` and never 500s the wire nor kills the bundle. Every slice-3 denial keeps the generic 403 — the **policy name never reaches the wire** (measured live: 0 occurrences of the policy name, the rule name, or even the policy's own thrown error text in the 403 body, against a `Forbidden`-fires-twice control); the detail goes to the server-side `[ authz ] denied \`<rule>\` — policy \`<name>\` did not allow|threw` line. Boot lint refuses a non-string/empty `policy`, a missing file, a non-function export and an async policy (#B57 shape) — all four live-verified `exit=1`. `self.hasRole(role)` is the imperative escape hatch for an action authorizing mid-logic: it **delegates to the gate's exported `hasAnyRole`** rather than re-reading `user.roles` itself (one definition of "holding a role" framework-wide — the byte-identical-copy-paste class `lib/cmd-status-format`/`lib/json-config-header` were extracted to end) and carries the #B35 released-response guard (`local.req == null` → false); its `types/index.d.ts` SuperController member is MANDATORY — the #DTO3b parity gate diffs the interface against a real instance. Server-side only, no dist rebuild (slice 2 already strips `policy` from the client blobs). Tests 65→110 (`test/lib/authz-gate.test.js` §11-§13; the roles-before-policy ordering pin + the delegation pin both validated can-fail against verified-changed perturbed copies). **#MS3 (2026-07-24, unreleased) adds MACHINE-CALLER authentication to the same gate** — service-to-service callers that cannot hold a session: `settings.json > auth.machine` (STRICTLY opt-in; `enabled` boolean-linted, fail-closed) declares named callers (`"callers": { "<name>": { "key": "<${secret:KEY}-capable>", "roles": [...] } }`); a GATED session-less request presenting `Authorization: Bearer <key>` is verified against a BOOT-precomputed sha256 hash per caller (on `process.gina._authConf.machine` — the request path compares fixed-length digests via `crypto.timingSafeEqual`: hash-BOTH-sides so no length oracle, raw keys not retained past boot, an invalid token pays the full caller scan) and becomes the effective principal `{ name, roles, machine: true }` stamped on `req.machineCaller` — it satisfies `requireAuth`, its configured roles ride the SAME ANY-of match, policies receive it as their `user` argument (discriminate via `user.machine === true`), audit actors snapshot `{ key: <callerName>, roles, machine: true }`, and `self.hasRole()` answers its roles. SESSION WINS (a signed-in request never consults the machine path); resolution is LAZY (un-gated routes never pay a compare); `enabled: false` and a pre-#MS3 `_authConf` shape are byte-identical legacy (both subtract-locked). A presented-but-invalid Bearer gets a clean 401 + `WWW-Authenticate: Bearer` and NEVER the login bounce (the '401-machine' audit outcome); a custom-scheme miss degrades to the ordinary 401/bounce — the machine 401 stays Bearer-specific. **Deliberately NO per-route opt-in key**: per-route granularity rides `roles` (keep a route human-only by requiring a role no caller holds, machine-only by one only callers hold). The escape hatch — `auth.machine.authenticator` names `<bundle>/authenticators/<name>.js` (the `policies/<name>.js` shape applied to authN): a SYNCHRONOUS `function (req) { return { name, roles } | null; }` run AFTER the built-in map (first success wins) and REGARDLESS of Bearer presence, so it verifies any sync-checkable credential (a sync-`verify` JWT, an HMAC header, an x-api-key); its return is NORMALIZED (`machine: true` forced, non-array roles collapse to `[]`), an `async function` module REFUSES the boot (the policy asymmetry: the transpiled-async promise shape is boot-invisible, which is why the runtime shape check stays strict), and a throw or malformed return at request time is fail-closed unauthenticated (warn-once), never a 500. `schema/settings.json` gains the previously-undeclared `auth` block (loginRoute + machine). Server-side only, no dist rebuild; `auth.machine` is boot config (restart to change). Tests: `test/lib/authz-gate-machine.test.js` (52 — session-wins + never-bounce decisive fixtures, both subtracts, the hash-of-key non-admission control, map-first spy proof, real-audit machine actors, controller hasRole via createTestInstance). **#COMPLY10 (2026-07-25, unreleased) adds an opt-in DENY-BY-DEFAULT mode to the same gate:** `settings.json > auth.requireAuthByDefault: true` INVERTS the strict-NO-OP above — an un-annotated route is GATED, and `param.public: true` is the explicit exemption. `public` is tested INSIDE the un-annotated branch, so precedence is STRUCTURAL: a route carrying `public` alongside an explicit gate key never reaches that branch and stays gated, i.e. a hand-built or tampered config still fails CLOSED without relying on the lint having run. It is a `param.*` key for the same measured reason `requireAuth` was (rides the whole-param clone on both cold and warm paths ⇒ zero `lib/routing` edits, no dist rebuild — a top-level key would have cost the two-site propagation edit AND a prod rebuild), and a POSITIVE marker rather than a reused `requireAuth: false`, so a reviewer can tell an audited exemption from a leftover (the `csrfExempt` rationale). **The mode is keyed PER BUNDLE** (`_authConf.byBundle`, read via `req.routing.bundle`) — merged mode runs every bundle in ONE process and `_authConf` is written once per `init()`, so a FLAT flag would let whichever bundle booted LAST decide the posture for all of them, silently un-gating a bundle that had opted in (a fail-OPEN; the write now merges into the existing map instead of replacing it). ⚠️ `public` is a strict-mode RESERVED WORD — under the old denylist it could not join the destructuring strip as a bare binding (a SyntaxError) and was dropped with a separate `delete cleanParam.public;`; since the Slice-3 (#SPA1) allowlist rebuild of the #B212 shared builder in `core/server.js`, the withholding is structural (no authorization key is on the roster) and that whole mechanism note is historical. Boot refuses three mode-specific contradictions: `public` + an explicit gate key (same axis, opposite answers, no safe precedence); a login route the mode would gate (it bounces to ITSELF — an infinite redirect that locks out every visitor, so a rule-name target must be marked `public` and an unresolvable absolute path refuses, while NO `loginRoute` only WARNS since a pure API/machine bundle legitimately 401s everything); and **a mode-gated route that also declares `cache`** — both render-cache serve points run BEFORE the gate and `buildKey` is `<release>:<kind>:<bundle>:<url>` with NO principal component, so a cached gated route replays the first authenticated body to every later anonymous caller (scoped to the mode, so an EXPLICITLY gated cached route is not newly refused — that pre-existing exposure is tracked separately). The five framework-injected routes (`webroot@<bundle>` — which also matches `/` under `webrootAutoredirect` — `custom-error-page`, `bundle-status`, and both upload routes) ship `param.public: true`, so flipping the mode cannot take the site root or the error renderer offline. `bundle:openapi` threads the mode into its gated predicate, so the emitted spec can never declare a newly-gated route unauthenticated. Server-side only, no dist rebuild (measured: `authorizeRequest` appears 0× in the built browser artifact against a firing `csrfExempt` control); boot config, restart to change. **#B158 — a GATED route may not also declare `cache`, however it is gated (0.5.26).** Both render-cache serve points run BEFORE the gate — the engine-agnostic read serves and RETURNS ahead of `router.route` (inside which the gate runs), and isaac's runs pre-routing, before `req.routing` exists — and `buildKey` composes `<release>:<kind>:<bundle>:<url>` with NO principal component, so a cached gated route replayed the first authenticated caller's rendered body verbatim to every later anonymous one (reproduced live on an isolated boot: a Bearer-authenticated render, same body and same render nonce, served to an unauthenticated caller, while the identical route WITHOUT `cache` correctly 401'd). The boot lint now REFUSES the pairing in both modes (`auth.requireAuthByDefault` had covered only mode-gated routes; the explicitly annotated case it left open was the pre-existing hole) — **ACTION REQUIRED: such a bundle will not start** until `cache` is dropped or the route opened. Second layer: `lib/authz-gate` exports `isRouteGated(req)` (the one runtime answer — explicit key, else `param.public`, else the per-bundle mode) and all three render delegates consult it inside their `writeCache` guard, so a config that never passed the lint (hand-built, tampered, mutated at runtime) still fails CLOSED. **#B159 — an authorization key on a `method: "ws"` route REFUSES the boot (0.5.26).** A ws route is registered on the engine's extended-CONNECT handler and never reaches `handle()`/`router.route`, so `requireAuth`/`roles`/`policy` on one are unenforceable BY CONSTRUCTION — and it previously linted clean, booted, AND was counted in the `Registered N authorization-gated route(s)` line, so the framework actively CONFIRMED an illusion. Refused now (the truthy-string-`requireAuth` precedent: a structurally-OFF gate must refuse exactly like a silently-OFF one), keyed on `/^ws$/i` — byte-identical to the ws registrar's own predicate so the two can never disagree — placed after the policy block so one check covers all three keys. Remedy: authenticate in the `wsHandler`, which receives the full request. The MODE half is a WARN, not a refusal (an explicit key is an author asserting something false; `requireAuthByDefault` sweeps up every un-annotated route, so refusing would break a working ws bundle the moment the mode is switched on): ws routes are excluded from the deny-by-default tally and NAMED at boot instead. Live-verified both arms (refuses with the key, boots without). Tests: `authz-gate.test.js §17` (8 — replica + case-insensitivity + the `websocket`-is-not-`ws` boundary + source pins) and `§15` (20 — per-bundle isolation, structural precedence, a mode-OFF subtract-control, and a boot-lint replica; all 11 source pins + the headline behaviour red-armed against pre-fix source).
|
|
945
946
|
|
|
946
947
|
252. **Audit trail — `settings.json > audit` + `self.audit()` + authz auto-events (#COMPLY2 slices 1-2, 2026-07-17, unreleased).** A user-attributed append-only record of "who did what to which record when", DISTINCT from `lib/logger` (its own store; never the log sinks). Opt in with `audit.enabled: true` (STRICTLY boolean — a truthy `"true"` refuses to boot, the quietly-OFF class: a compliance control that silently does nothing is worse than one that is absent) and call `self.audit(action, data[, cb])`. **Record schema v1:** `{ id, ts, requestId, actor:{key,roles}, action, resource?, meta?, ip, rule, method, bundle, env }`. **The correlation key is the SHARED request id, not a second one:** slice 1 hoisted the existing `request._ginaReqId` stamp OUT of the `_reqCtxLogging` (JSON-log-mode) gate in `server.js` `onInstance` — always-on, both engines, **plus a first-seer `if (!request._ginaReqId)` guard** the gated stamp never had (it sits in the isaac double-dispatch spot ⇒ a re-entered dispatch REGENERATED the uuid; the hoist closes that latent hazard by construction). The `_reqALS` `.run()` stays JSON-gated, so the deferred always-on-ALS throughput question is untouched. One id, two consumers: audit records and JSON log lines correlate for free, and `_resolveRequestId`'s inbound `X-Request-Id` sanitization is reused — **so the id is client-influenceable BY DESIGN: it is correlation, never attribution** (attribution is the actor fields). **`rule` carries the re-keyed `<rule>@<bundle>` form** (`homepage@web`, `secure@web` — measured live), because `config.js` re-keys every rule at normalisation; a consumer grepping the trail must expect the suffix, not the bare routing.json key (same read as the #OBS1 metrics route label). `ip` is the socket remoteAddress, `::ffff:`-normalized — **`X-Forwarded-For` is NEVER read** (attacker-writable, the #OBS1 rule). `actor` is a SNAPSHOT of `session.user[audit.actorKey]` (default `"id"`) + a COPY of `user.roles`, never the whole user object (PII). **Default backend = append-only JSONL file**, path DERIVED as `<projectPath>/logs/audit-<bundle>-<env>.jsonl` — NOT read from `logsPath`, which is a **measured DEAD placeholder**: `env.json` declares it at the file ROOT, outside the `${bundle}`/`${env}` subtree config.js copies, nothing ever assigns it, so `${logsPath}` resolves to the literal string `"undefined"` and has zero template consumers (`projectPath` is inside the propagated subtree and provably resolves — it is what the live `bundlesPath` is built from). **Per-ENV filename is load-bearing:** two envs of one bundle running concurrently must never interleave writers into one file (harmless for JSONL, FATAL for the slice-3 hash chain). A relative `audit.file` resolves against the project root, never the process cwd (which depends on how the bundle was launched). **Writes are SERIALIZED** (FIFO, one in-flight `fs.write`): parallel writes on one fd complete in threadpool ORDER, and the chain needs on-disk order == emit order, so the queue ships in the MVP rather than with slice 3. `audit.store` (a connectors.json entry, resolved through the `lib/audit-store` dispatcher — the 3rd instance of the job-store mould) is wired from day 1 but **NO connector ships an implementation yet ⇒ configuring it is a clean boot REFUSAL**, never a silent fallback; `store`+`file` together is refused too (mutual exclusion, no silent precedence). **Framework auto-events are ON when audit is on:** the authz-gate deny sites emit `authz.denied` carrying `meta.outcome` (`401`|`login-bounce`|`403-roles`|`403-policy`|`401-machine` — the last added by #MS3's machine deny writer, whose records also carry the machine actor) — the real shape is **3 physical writers / 6 call sites / 5 outcomes** (`denyUnauthenticated`'s two exits + the SHARED `denyForbidden` with its explicit `outcome` arg + #MS3's `denyMachineUnauthenticated`), wired INSIDE the module by deep-path `require('../../audit/src/main')` because the deny helpers are unexported (the job→uuid precedent; one-way dep, no cycle). Opt out with `audit.events.authz: false`. The emit is fully contained (try/catch + enabled-gated), so **an audit failure can never change an authorization outcome** (locked by a subtract asserting identical thrown statuses with audit on vs off). Makes PCI 10.2.4 real with zero app code; the unauth-401 flood caveat is a throttling concern, not an audit one. **Two shapes worth copying:** (a) the boot lint `throw`s INSIDE `init()`'s try and INHERITS the #B57 emerg+`fs.writeSync(2,…)`+`exit(1)` from its enclosing catch — hand-rolling it would double-log and drop the `ServerEngine <stack>` prefix; (b) the boot destination line is `console.info`, NOT `.debug` — the shipped default `log_level: "info"` FILTERS debug, so a `.debug` would silently defeat the "path logged at boot" contract. `start()` is adopt-once (the `lib/job` precedent). **`self.audit()` on a released response emits a DEGRADED record** (req-derived fields null) instead of dropping — every req read in the record builder is null-safe by construction, so NO #B35 early-return: present-but-degraded beats absent for a compliance trail (the opposite of the render-path guards, deliberately). Server-side only — no dist rebuild. Live-verified daemonless: disabled = true no-op with an action calling `self.audit()`; enabled = full-schema record (real uuid requestId, socket ip, `<rule>@<bundle>`) + exactly one `authz.denied` on an unauth 401; `enabled:"true"` and an implementation-less `store` each refuse the boot with the exact emerg and never listen. **Slice 3 (2026-07-26, unreleased) adds the TAMPER-EVIDENCE hash chain + `gina audit:verify`.** Opt in with `audit.chain.enabled: true` + a signing key (`audit.chain.secret` — a literal or `${secret:VAR}`, or the `GINA_AUDIT_SECRET` env var read through the framework env reader, the #MS3 shape); every record then gains a `hash` = **`HMAC-SHA256(secret, prevHash + ':' + canonicalV1(record))`, hex, 64-zero genesis**. `canonicalV1` (key-sorted, JSON-value-space) is the canonical form insertion-order `JSON.stringify` never was — the chain store PROJECTS each record through a `JSON.parse(JSON.stringify())` round-trip BEFORE digesting, or a live `Date` in `meta` would digest as `{}` while the disk holds its ISO string. **`createChainStore` wraps the unchanged append seam** (the store stays dumb): one record in flight, the hash computed at DEQUEUE so hash order == append order == emit order, and **`prevHash` advances ONLY on a successful inner append** — a dropped record (disk full, serialize failure) never forks the on-disk chain; the next record chains from the last that landed. Consumer callbacks are guarded so a throwing callback cannot stall the queue. **Boot resume reads the trail TAIL in O(1)** (`readChainTail`, a bounded backwards window — never a whole-file scan); a torn tail from a crash mid-write is terminated with a `'\n'` (append-only preserved — the damaged bytes stay as their own unparseable line), a loud `console.warn` names the damage, and a chained `audit.chain.break` record is written as the first append carrying the damage report in `meta` — NOT a crash loop. **`gina audit:verify <bundle> @<project>`** (new offline CLI group — `verify.js` + `arguments.json` + `help.txt`, `'audit:'` in `bin/cli` allowedOffline) walks the file and reports the first break with line + reason, or the intact totals, `--format=json` for machines; exit **0** intact / **1** broken / **2** usage-or-config (`fs.writeSync` flush — a verdict must never truncate on a pipe). The verifier's rules each close a hole: hashless records are legal ONLY as a contiguous leading prefix (a pre-chain trail — anywhere later they FAIL, so inserting unhashed lines is never free); an unparseable line FAILS unless the very next record is a chained `audit.chain.break` acknowledgment (reported as a warning, never silently passed — and only ONE damaged line per ack, consecutive garbage FAILS); empty lines are skipped; a trailing unacknowledged torn tail is a WARNING (the crash-just-happened state, the next boot acknowledges); digests compared with `crypto.timingSafeEqual`. **Boot lint grows four unconditional chain-shape conjuncts** (`chain` object / `enabled` strict-boolean / `secret` non-empty-string / no unresolved `${...}`) **+ three gated guards**: no signing key → refuse (tamper-evidence without a key is silently OFF — the truthy-string class); `chain` + connector `store` → refuse (the seam carries NO ordering obligation, a chain over reordered records forks silently); and a **cross-bundle shared explicit `audit.file`** → refuse when EITHER party chains (two writers fork a linear chain; boot-detectable in both process modes by replaying the sibling's file derivation over `options.conf`). A `<32`-char key WARNS. **Threat model, stated in the docs not implied:** detects post-hoc edit, deletion, insertion and reordering by anyone WITHOUT the key (a duplicated/replayed record breaks too — its predecessor's hash no longer matches); it is explicitly NOT a defence against the WRITING PROCESS, which holds the key and can forge a whole chain — stream the JSONL trail to WORM/Object-Lock storage for that stronger adversary (the recognized PCI-DSS §10.3.3 control, already documented). **Two boundaries the chain cannot cross, pinned honestly:** truncation at the exact tail is invisible (nothing after it commits to it — read the record count), and an empty/absent trail verifies trivially. Merged-process mode stays BOOTABLE (runtime-measured single-writer) but a sibling bundle whose `audit` settings are silently ignored there is now NAMED at boot. `schema/settings.json` gains the previously-undeclared `audit.chain` block (`enabled` boolean + `secret` string, `additionalProperties:false`), so an editor now flags a mistyped `chian`/`enabeld` — which the runtime lint would otherwise catch only as a boot refusal. As change-detection it maps to **PCI-DSS v4.0.1 §10.3.4** (file-integrity/change-detection), NOT §10.3.2 (prevent-modification, which the WORM control covers). Server-side only — NO dist rebuild; enabling the chain needs a bundle restart. Live-verified daemonless (isolated home, both process modes): boots with `(tamper-evident chain: ON)`, records chain, `audit:verify` PASSES; a one-byte edit / a middle deletion / a reorder each FAIL at the right line while the untouched trail PASSES (both directions); a restart resumes the chain across the process boundary (proven by an independent HMAC re-implementation reproducing the post-restart hash from the pre-restart tail); a torn tail warns then the next boot acknowledges it with a chained break; no-key boot REFUSES; a merged pair shows the sibling-ignored warn; a cross-bundle shared `audit.file` REFUSES.
|
|
947
948
|
|
|
948
949
|
253. **The run family — `gina image:run <image>` + `gina container:ps` / `container:stop` (podman on the container host).** The verbs drive `podman` (buildah builds images but cannot run them; podman's OCI runtime is crun) on the SAME host every `image:*` verb resolves — `GINA_CONTAINER_HOST=ssh://[user@]host[:port]` env override → native → `container.host` settings — so `container:ps` always lists the host `image:build` builds onto. A build-only host (buildah present, podman absent — ssh remote rc 127 / `command not found` / native spawn ENOENT) is reported honestly: `run unavailable on this host: podman not found on <host> (buildah <ver> present — build-only host); image:run needs podman + conmon` (the buildah probe runs in the failure path only). `image:run`: detached by default, the container id ALONE on stdout with progress on stderr so `ID=$(gina image:run <image>)` captures just the id; `--format=json` → `{ host, id, name, image, ports }`; default ports = the image's own baked EXPOSE mapped same:same (read by parsing the FULL `podman image inspect` document in JS — a `--format '{{json .Config.ExposedPorts}}'` token carries a SPACE and an ssh remote shell would split it: any argv token containing a space cannot cross the ssh transport, prefer a no-token command whose output you parse in JS); `--publish=<host:ctr>[,…]` overrides, `--publish=none` disables; `--rm`; `--stream` = foreground NDJSON (`start` → `log{stream,line}` → `done{exitCode}`|`error`, fs.writeSync-flushed, the container's own exit code propagated through the remote shell + ssh). Env: `--env-var=K=V` (repeatable) + `--env-file=<local path>` compose one env file that reaches podman WITHOUT touching argv — natively a 0600 temp file; over ssh the lines ride ssh stdin into a remote `mktemp` file a shell `trap` removes (`--env-file /dev/stdin` does NOT work over ssh — sshd's stdin pipe cannot be re-opened by path, rc 125; and never `exec podman` there — exec replaces the shell so the trap never fires and the file leaks); podman lets a later duplicate key win, so the inline `--env-var` lines are composed AFTER the file's and override it; keys gated `^[A-Za-z_][A-Za-z0-9_]*$`, values newline-rejected — secrets never in argv, `${secret:KEY}` never baked. `container:ps [--all]` normalizes podman's snake_case `Ports` to `[{hostPort,containerPort,protocol}]` (null→`[]`, `hostIp` only when single-interface-bound, `range` only when the entry publishes a real range >1); `container:stop <name|id> [--time=<s>] [--force]` classifies the rung from a post-stop inspect because podman exits 0 either way — `graceful` (the container handled the signal; a gina bundle traps SIGTERM → 143), `killed` (137 = 128+SIGKILL after the grace period), `forced` (`--force` → `podman kill`); an already-exited target is a success no-op. Every user token is charset-gated BEFORE any command assembly (validate→assemble→exec, test-pinned). All stop rungs, stream, env and publish arms live-verified end-to-end on podman 5.7.0 over ssh (0.5.19).
|
|
949
950
|
|
|
950
|
-
254. **`redirect()` carries request data through the session by default; the clear-text URL form is the session-less fallback (2026-07-17, unreleased).** When a redirect's request holds params, they cross it via the session flash channel (`inheritedData` on `req.session.user || req.session`) whenever a live session exists — `router.js`'s route dispatch merges them into `req.get` on the next routed GET, then **one-shot deletes** them (a refresh does NOT replay; the URL form did — the one behaviour change). Three write sites, each gated on a live `userSession` alone: `redirect()`'s XHR branch (the `local.haltedRequestUrlResumed` conjunct that had limited it to resume-triggered XHR redirects is GONE), `redirect()`'s method-switch path (`inheritedDataIsNeeded`), and `resumeRequest()`'s GET branch. **The whole URL build AND the 2000-char cap now sit inside `if ( !userSession )`** — so a session-ful redirect never composes the string and can no longer `424` (a >2000 payload rides the session; live-measured 2600 chars intact), while a session-less bundle is byte-identical to before (`?inheritedData=<RFC5987 JSON>` in clear, capped, `424` over). `resumeRequest`'s stash sits pre-split (after the stale-`inheritedData` delete, before the popin/plain-XHR/non-XHR three-way), so **all three GET replay flavors now carry the paused data** — previously only the popin flavor did, because it alone routed through `redirect()`; plain-XHR and non-XHR silently DROPPED it. A custom `requestStorage` with no live session degrades exactly as before. Server-side only — no dist rebuild (`controller.js` is not browser-bundled). The practical security consequence (consumer-verified live): a session-ful bundle halting a credential-bearing POST no longer discloses the payload — credentials included — in clear in the URL (address bar, browser history, access logs). ⚠️ **Adjacent pre-existing crash on the same path — FIXED same day (unreleased, rides the same cut):** `getRoute(rule, params)` dereferenced `route.requirements[p]` in its extra-params loop, and a route declaring no `requirements` composes with `requirements: undefined` → `TypeError: Cannot read properties of undefined (reading '<key>')` for any param not in `route.param` — reached from `resumeRequest`'s `getRoute(rule, haltedRequest.params || dataAsParams)`, so a halted GET route with data and no `requirements` block 500'd before the stash ran (also reachable from the `url` template filter / `getUrl()` family, browser bundle included). The deref is now guarded (`route.requirements && …`, the fitsWithRequirements discipline), so extra params append to the composed URL as `?key=value` query params; the requirements-DECLARED-key skip (the path-placeholder fold) is unchanged — declaring a key in `requirements` still makes the matcher fold it in as a path placeholder, so a placeholder-less URL 404s (a `requirements` block is a matching declaration, not a sanitizer).
|
|
951
|
+
254. **`redirect()` carries request data through the session by default; the clear-text URL form is the session-less fallback (2026-07-17, unreleased).** When a redirect's request holds params, they cross it via the session flash channel (`inheritedData` on `req.session.user || req.session`) whenever a live session exists — `router.js`'s route dispatch merges them into `req.get` on the next routed GET, then **one-shot deletes** them (a refresh does NOT replay; the URL form did — the one behaviour change). Three write sites, each gated on a live `userSession` alone: `redirect()`'s XHR branch (the `local.haltedRequestUrlResumed` conjunct that had limited it to resume-triggered XHR redirects is GONE), `redirect()`'s method-switch path (`inheritedDataIsNeeded`), and `resumeRequest()`'s GET branch. **The whole URL build AND the 2000-char cap now sit inside `if ( !userSession )`** — so a session-ful redirect never composes the string and can no longer `424` (a >2000 payload rides the session; live-measured 2600 chars intact), while a session-less bundle is byte-identical to before (`?inheritedData=<RFC5987 JSON>` in clear, capped, `424` over). `resumeRequest`'s stash sits pre-split (after the stale-`inheritedData` delete, before the popin/plain-XHR/non-XHR three-way), so **all three GET replay flavors now carry the paused data** — previously only the popin flavor did, because it alone routed through `redirect()`; plain-XHR and non-XHR silently DROPPED it. A custom `requestStorage` with no live session degrades exactly as before. Server-side only — no dist rebuild (`controller.js` is not browser-bundled). The practical security consequence (consumer-verified live): a session-ful bundle halting a credential-bearing POST no longer discloses the payload — credentials included — in clear in the URL (address bar, browser history, access logs). ⚠️ **Adjacent pre-existing crash on the same path — FIXED same day (unreleased, rides the same cut):** `getRoute(rule, params)` dereferenced `route.requirements[p]` in its extra-params loop, and a route declaring no `requirements` composes with `requirements: undefined` → `TypeError: Cannot read properties of undefined (reading '<key>')` for any param not in `route.param` — reached from `resumeRequest`'s `getRoute(rule, haltedRequest.params || dataAsParams)` (since #B215 that recompose is the SESSION-LESS fallback only — a session-ful GET replay redirects to the byte-exact `haltedRequest.url` — query string included since #B219 completed the capture side: the isaac engine strips the query from `req.url` before controllers run (it is parsed into `req.get`), so `pauseRequest()` snapshots `req.originalUrl || req.url` (isaac stamps `originalUrl` byte-exact before its strip; express sets it natively and never strips), `haltedRequestUrlResumed` records that full URL (middleware comparing it to `req.url` by equality should compare against `req.originalUrl || req.url` instead), and pre-#B219 path-only snapshots replay exactly as captured — because the recompose only carries query keys captured into `req.params`, i.e. keys declared in BOTH `requirements` AND `param`: a `param`-bound-only key substitutes its `:key` placeholders at match-commit yet never reaches `req.params`, and an undeclared key never does either, so the replayed GET arrived query-less, matched anyway, and rendered literal `:key` template paths as a 500 — the `inheritedData` flash cannot heal that since router.js merges it into `req.get` AFTER matching; with no session the recompose remains, its composed query params being the halted data's only travel channel), so a halted GET route with data and no `requirements` block 500'd before the stash ran (also reachable from the `url` template filter / `getUrl()` family, browser bundle included). The deref is now guarded (`route.requirements && …`, the fitsWithRequirements discipline), so extra params append to the composed URL as `?key=value` query params; the requirements-DECLARED-key skip (the path-placeholder fold) is unchanged — declaring a key in `requirements` still makes the matcher fold it in as a path placeholder, so a placeholder-less URL 404s (a `requirements` block is a matching declaration, not a sanitizer).
|
|
951
952
|
|
|
952
953
|
255. **`redirect()` is ASYNC, and its relative-path form resolves again — an unresolvable target 404s the request instead of killing the bundle (2026-07-17, unreleased).** Three coordinated fixes. (1) `getRouteByUrl` (lib/routing) is async and awaits its internal `compareUrls` (async since the `validator::` requirements support): the historical un-awaited call read `.past` off a pending Promise, so NO rule could EVER match server-side — the documented-primary `self.redirect('/path')` relative form had been dead for years (the ignoreWebRoot form only worked because it bypasses resolution entirely). The fix also mirrors the engine loop's exact-url fast-path (`pathname == routing[name].url || isRoute.past`) — the two matchers are declared byte-parallel ("must remain identical to server.js") and any behaviour added to one must land in the other. (2) The resolution lands in a LOCAL first and is adopted onto `req.routing` only on success: a miss or resolver rejection keeps the route that dispatched the action. This no-clobber shape is load-bearing twice over — the matcher itself reads and stamps `request.routing` on the request it is handed (a `false` there silently voids the writes), and the error reporters' diagnostics deref `req.routing.param` (with the old `false` sentinel the designed 404 turned into a crash-derived 500 from inside the reporter). A double miss now throws a clean 404 (`redirect target not found`); a resolver rejection is contained as a 500 — either way one request, never the process. (3) The response-header composer guards `!request.routing` (falsy-aware; the old `typeof == 'undefined'` guard passed the boolean `false` through to a strict-mode primitive-property write that threw from inside the error path itself — a double-fault the router's catch could not contain, ending in SIGTERM). `redirect()` being async means: prefer `return self.redirect(...)` from controller actions so a rejection threads to the framework's thenable backstop; all other forms (URL, route-name, ignoreWebRoot) complete synchronously — their promise settles same-tick. `getRouteByUrl` ships in the browser bundle with zero client callers: the async declaration rides the next per-bundle re-bake with no client behaviour change expected.
|
|
953
954
|
|
|
@@ -973,7 +974,7 @@ Dev-mode query instrumentation captures every database query tied to the current
|
|
|
973
974
|
|
|
974
975
|
266. **The link plugin's HTML-callback attributes (`data-gina-link-event-on-success` / `data-gina-link-event-on-error`) were dead AND deadly — carrying either killed the link's XHR with an uncaught ReferenceError before xhr.open (#B141, fixed 2026-07-21).** Three stacked layers, all live-verified in a real browser render (real clicks; the attribute-less control link on the same page fetched normally both runs): (1) SCOPE — the only live `listenToXhrEvents` definition sat INSIDE `on()` in utils/events.js (a brace slip; the adjacent "Nothing can be added after on()" comment shows it was meant to follow on() as a top-level shim global like handleXhr), so link/main.js's call resolved nothing and threw; (2) ARITY — the call passed one arg, so a reachable definition would read `data-gina-undefined-event-on-*` and silently register nothing; (3) PLUMBING — handleXhr fired the `.hlink` events named by the link INSTANCE id and dispatched them on document, while registration binds the PER-LINK id on the anchor element — name and target could never match. Fix: the definition is a top-level global in utils/events.js (body byte-preserved incl. the function-call-shape refusal warn), link/main.js passes ($link, 'link') and drops its stale commented-out local copy, and the six .hlink triggers address $link.target + $link.id. Deliberately untouched: the plain programmatic channel (gina.link.on('success'/'error') — instance id + document on BOTH sides, always aligned) and the forms' .hform path (the form validator is self-contained and coherent). Registration is per-link idempotent (gina.events dedup by event name). Browser-bundled — consumers re-bake at pickup. Tests: link-xhr-events.test.js (18 — src+dist pins incl. the column-0 top-level scope invariant, extracted-real-definition behaviour incl. the one-arg pre-fix subtract, registration/trigger alignment replica incl. the instance-id-mismatch subtract; all discriminators red-first-validated against the pre-fix blobs).
|
|
975
976
|
|
|
976
|
-
267. **`req.files[].size` was a racy under-count — the per-file byte counter is incremented in the liner Transform one pipe hop DOWNSTREAM, but the record was built final at the SOURCE stream's 'end', when the objectMode liner (16-OBJECT high-water marks) can still hold un-counted queued chunks (#B142, fixed 2026-07-21; surfaced by a consumer verifying the #B103 pickup — measured live 16/16 uploads short by a whole-chunk amount, ~25% on a 1.5MB file, while md5 stayed perfect: bytes on disk were always intact, only the number lied).** The fix captures the pushed record in a per-part closure var and finalizes `size` in a live `liner._flush`, with probe-measured ordering guarantees: `_flush` runs after the LAST `_transform` and strictly before the write stream can emit 'finish', and the request only resumes after every write stream finished — so the patched value is final before any consumer (incl. `self.store()`, which copies `files[i].size`) reads `request.files`. On a settled pipeline `_flush` can run BEFORE the source's 'end' listener; the push then reads the already-complete count — both interleavings yield the exact byte size (both measured). The push deliberately stays at source-'end': busboy emits parts sequentially, so that preserves `request.files` part order, while flush COMPLETION order can invert across parts. Server-side only — no dist rebuild; running bundles pick it up at restart. Sibling discovered during the same verification and FIXED as #B143 (2026-07-21): the per-writeStream 'finish'/'error' listeners attached only inside `busboy.on('finish')` — after the WHOLE body was parsed — and Node never replays 'finish' for a late listener, so an early small file whose write stream finished while a later large part was still streaming lost the listener race and the request never resumed (measured live: a throttled two-file upload hung deterministically, no log line; unthrottled ~1 in 13 — fast clients exposed, rarely; unauthenticated, both engines, single-file unaffected). Both listeners now arm AT STREAM CREATION in the 'file' handler, counted by a pending counter plus a busboyDone flag set at parse end; resume fires on busboyDone && pending === 0, whichever event lands last — closing the race for every ordering (the same zero-pending branch still covers the fields-only #B93 terminal), and a mid-stream write error (missing dir, disk full) now gets a guarded 500 instead of the historical unhandled-'error' uncaughtException SIGTERM. Live post-fix: the throttled two-file was-hang 200s with exact sizes and preserved part order; fields-only, single-file, and three-file arms all green. Server-side only — no dist rebuild; restart to apply. Tests: upload-size-accuracy.test.js (11 — red-first-validated source pins, control-gated extraction of the live _transform+_flush bytes, a stalled-sink choreography replica making the race deterministic, and a frozen pre-fix subtract) + multipart-multifile-resume.test.js (15 — red-first pins incl. loop-gone negatives, a real-Writable never-replays proof with a can-fail control, the deterministic early-finisher race replica + frozen pre-fix subtract, and a REAL busboy paced two-part drive where the early sink provably finishes before parse end).
|
|
977
|
+
267. **`req.files[].size` was a racy under-count — the per-file byte counter is incremented in the liner Transform one pipe hop DOWNSTREAM, but the record was built final at the SOURCE stream's 'end', when the objectMode liner (16-OBJECT high-water marks) can still hold un-counted queued chunks (#B142, fixed 2026-07-21; surfaced by a consumer verifying the #B103 pickup — measured live 16/16 uploads short by a whole-chunk amount, ~25% on a 1.5MB file, while md5 stayed perfect: bytes on disk were always intact, only the number lied).** The fix captures the pushed record in a per-part closure var and finalizes `size` in a live `liner._flush`, with probe-measured ordering guarantees: `_flush` runs after the LAST `_transform` and strictly before the write stream can emit 'finish', and the request only resumes after every write stream finished — so the patched value is final before any consumer (incl. `self.store()`, which copies `files[i].size`) reads `request.files`. On a settled pipeline `_flush` can run BEFORE the source's 'end' listener; the push then reads the already-complete count — both interleavings yield the exact byte size (both measured). The push deliberately stays at source-'end': busboy emits parts sequentially, so that preserves `request.files` part order, while flush COMPLETION order can invert across parts. Server-side only — no dist rebuild; running bundles pick it up at restart. Sibling discovered during the same verification and FIXED as #B143 (2026-07-21): the per-writeStream 'finish'/'error' listeners attached only inside `busboy.on('finish')` — after the WHOLE body was parsed — and Node never replays 'finish' for a late listener, so an early small file whose write stream finished while a later large part was still streaming lost the listener race and the request never resumed (measured live: a throttled two-file upload hung deterministically, no log line; unthrottled ~1 in 13 — fast clients exposed, rarely; unauthenticated, both engines, single-file unaffected). Both listeners now arm AT STREAM CREATION in the 'file' handler, counted by a pending counter plus a busboyDone flag set at parse end; resume fires on busboyDone && pending === 0, whichever event lands last — closing the race for every ordering (the same zero-pending branch still covers the fields-only #B93 terminal), and a mid-stream write error (missing dir, disk full) now gets a guarded 500 instead of the historical unhandled-'error' uncaughtException SIGTERM. Live post-fix: the throttled two-file was-hang 200s with exact sizes and preserved part order; fields-only, single-file, and three-file arms all green. Server-side only — no dist rebuild; restart to apply. Tests: upload-size-accuracy.test.js (11 — red-first-validated source pins, control-gated extraction of the live _transform+_flush bytes, a stalled-sink choreography replica making the race deterministic, and a frozen pre-fix subtract) + multipart-multifile-resume.test.js (15 — red-first pins incl. loop-gone negatives, a real-Writable never-replays proof with a can-fail control, the deterministic early-finisher race replica + frozen pre-fix subtract, and a REAL busboy paced two-part drive where the early sink provably finishes before parse end). Downstream companion (#B223, fixed 2026-08-03): `Controller.store()`'s mover now streams each file to a temp sibling and publishes with an atomic rename — a reader never observes a partial file under the final name, a pre-existing destination is replaced only on success, and a FAILED move no longer destroys the staged source nor settles the callback twice — and store() propagates the REAL filesystem Error (every failure was previously masked as `No file to upload`, and a source-side stream error had no listener: an uncaughtException -> SIGTERM bundle kill; the genuinely-empty case keeps the historical message). Server-side only — no dist rebuild; restart to apply.
|
|
977
978
|
|
|
978
979
|
268. **Consumer-probeable multipart write-error — a per-upload-group `simulateWriteError` flag deterministically fires the guarded-500 write-error path, honoured OUTSIDE production scope only (#B144, unreleased — rides the next cut).** A consumer could not SAFELY re-confirm the #B143 mid-stream write-error crash-guard on their own upload surface after a pickup: every real trigger was either a global config change (an unwritable per-group `path` breaks REAL uploads to that group) or a whole-bundle destabiliser (a full disk). Fix: a group carrying `simulateWriteError: true` makes the `'file'` handler create the REAL write stream, arm the REAL #B143 terminal listeners, then synthetically `destroy()` it → the production `'error'` listener → the production `throwError(500)` with the EXACT terminal semantics of a real ENOSPC/EIO (an errored stream never emits `'finish'`, so it never decrements the pending counter and the request stays terminal at the 500). A multi-file probe destroys EVERY tagged part's stream, but throwError's `!res.headersSent` guard collapses the N errors to exactly ONE 500 (measured 2026-07-22: busboy does NOT stall on the unconsumed first part — both parts parse and both streams are destroyed; the guard is what makes it one 500). The flag is gated on `!self.isProductionScope()` (NODE_SCOPE_IS_PRODUCTION), so dev/local/beta/testing probe while production stays inert; a boot warn scans `upload.groups` for the flag either way (production: "IGNORED — remove before shipping"; else "PROBE active"). No magic group name — any consumer-named group carrying the flag; nothing ships active (the settings template carries only a COMMENTED `_probe_fail` sample), and an unconfigured group still 400s. The `group="…"` upload tag rides a Content-Disposition PARAMETER that `curl -F` / browser FormData cannot emit (the @rhinostone/busboy fork parses it into `info.dispositionParams.group`), so a faithful probe HAND-BUILDS the multipart body; `createWriteStream` opens the fd before the destroy, so a probe may leave a 0-byte tmp file (point the probe group's `path` at a tmp dir). Both engines; server-side only — no dist rebuild, restart to apply. Tests: upload-write-error-probe.test.js (source pins red-first-validated against the pre-fix blob; a REAL @rhinostone/busboy drive of a single-part and a small-then-large two-part probe body through a faithful 'file'-handler replica; a prod-scope + flag-off subtract validating the 500-counter reads both one and zero; a destroy→'error' mechanism proof). Ships alongside #B145 (the sibling `mkdirSync`-itself crash-guard in the same handler).
|
|
979
980
|
|
|
@@ -992,7 +993,7 @@ Dev-mode query instrumentation captures every database query tied to the current
|
|
|
992
993
|
275. **The `controller:` CLI group — `add` scaffolds, `remove`/`rm` reference-awarely deletes, `rename` reference-awarely renames a namespace controller (#R9, 2026-07-23).** `gina controller:add <name> <bundle> @<project> [--controls=a,b,c] [--api|--views]` — a new OFFLINE CLI group (`lib/cmd/controller/`, registered in `bin/cli` allowedOffline; a develop install serves the verb immediately, npm consumers at the next cut). Generates `controllers/controller.<name>.js` with one JSDoc'd action stub per `--controls` entry (omitted → a single `default` action — the `core/template/boilerplate/bundle_namespace` seed's own action name). Bundle FLAVOR auto-detected via `config/templates.json` presence (config.js's `hasViews` signal, offline-readable without a boot): a VIEW bundle → `self.render()` stubs + one template per action at `templates/html/<name>/<action>.html`; an API-only bundle → `self.renderJSON()` stubs, no templates. `--api`/`--views` force it (both → error). Route wiring is PRINT-ONLY: prints paste-ready rules keyed `<name>-<action>` carrying `namespace` + `param.control`, and for a VIEW rule an explicit `param.file: "<action>"` — render-swig.js:364-383 strips a `<namespace>-` prefix from a DEFAULTED `param.file` (which equals the rule name) and emits a per-request "does not respect gina naming convention" WARN, so the explicit file resolves `templates/html/<ns>/<action>.html` warn-free (the boilerplate `homepage` rule sidesteps it with a non-prefixed name); the `default` action's URL is the namespace root `/<name>`, else `/<name>/<action>`. Refuses an existing `controller.<name>.js` (no `--force` overwrite — remove it first), and validates the namespace charset `/^[a-z][a-z0-9_]*$/i` BEFORE any path build (`controller` reserved case-insensitively; the value is interpolated into file paths, a RegExp and a JS class name, so a loose value is an injection vector). Positionals come from the group's own pure `inc/args.js` parser (`opt.argv.slice(3)` minus `@project`/`--flags`) — CmdHelper's clean positional cleanup is `bundle:`-ONLY (helper.js:482), so `self.bundles` for any other topic is contaminated with the project's OTHER bundles and cannot be read for `<name> <bundle>`. Pure `inc/` helpers (`namespace` charset+className, `scaffold` parseControls/buildRules/formatRulesBlock/renderController/renderTemplate, `args` positionals) are require-by-path unit-tested behaviourally; the paste-ready report prints via flush-safe `fs.writeSync(1, …)` (the connector:infer precedent — a `console.log`/`process.stdout.write` before `process.exit` truncates on a pipe). Boot-verified end-to-end in an isolated home: the scaffolded view controller serves HTTP 200 with the rendered body and NO naming-convention warn. Tests: `test/lib/controller-add.test.js` (38). **`controller:remove <name> <bundle> @<project> [--dry-run] [--force] [--format=json]`** (alias `controller:rm` = `module.exports = require('./remove')`) is REFERENCE-AWARE: a namespace with no matching `controller.<name>.js` doesn't error — `router.js:521-525` WARNs and silently falls back to the default `controller.js` (a misdispatch), so a bare delete is unsafe. The pure `inc/reference-scan.js` (node `fs`/`path` only, no framework globals) scans the four measured reference sites — the controller file, routing.json rule-level `namespace` AND `param.namespace` (both load a controller; the body is comment-stripped with a STRING-AWARE regex so a `$schema` URL survives, and non-object entries are skipped like `config.js:2029`), and `requireController('<name>')` literals (both quote styles) across the bundle's `.js` tree — then REFUSES with a per-file blocker list unless clean (routing rules + external `requireController` calls block; a self-reference inside the file being deleted is moot). Clean → interactive readline yes/no (the `view:add` idiom; a non-TTY stdin aborts, naming `--force`/`--dry-run`) → deletes the controller file + its `templates/html/<name>/` tree. It NEVER edits routing.json. `--force` deletes the file+templates even with blockers (listing what remains to clean by hand), `--dry-run` previews, `--format=json` emits an envelope (`{controllerFile,templateDir,routingRefs,requireRefs,dynamicRefs,blocking,removable,removed}`) and deletes only with `--force`. Dynamic references a static scan cannot resolve (`param.namespace` `:variable`, non-literal `requireController(expr)`) are surfaced as an ADVISORY note, never silently cleared. The default `controller.js` can never be removed (`controller` is reserved). Deletion-plan file counts are taken BEFORE the delete. Tests: `test/lib/controller-remove.test.js` (34). **`controller:rename <old> <new> <bundle> @<project> [--dry-run] [--force] [--format=json]`** reuses the scanner + adds the pure `inc/reference-rewrite.js` (also fs/global-free): it moves `controller.<old>.js` → `controller.<new>.js`, moves `templates/html/<old>/`, and rewrites the old namespace at the two ANCHORED sites — routing.json `"namespace": "<old>"` VALUES (rule-level AND param.namespace; a QUOTED-STRING-anchored `replace`, NOT a JSON parse→stringify, so comments/ordering/whitespace survive byte-for-byte, and a substring like `checkout2` or a `:variable` value is never matched) and every `requireController('<old>')` literal (both quote styles, spacing preserved). Unlike `remove`, rename DOES edit routing.json (only those values). A full plan is shown, then an interactive readline confirm (non-TTY aborts naming `--force`); apply is ALL-OR-NOTHING — snapshot every write+move, roll back on any failure. `--force` applies without the prompt, `--dry-run` previews, `--format=json` emits an envelope and applies only with `--force`. Residuals it REPORTS-not-rewrites: `:variable`/non-literal dynamic refs and the cosmetic `<Bundle><Namespace>Controller` class name (gina loads by file path, so it is a label, not a reference). Target collision (`controller.<new>.js` or `templates/html/<new>/` exists) hard-refuses — no overwrite. Tests: `test/lib/controller-rename.test.js` (18).
|
|
993
994
|
|
|
994
995
|
276. **Worker-global proxy-context poisoning by port-less internal calls — the `getUrl`/`url` template filters, the cross-bundle redirect-by-URL target and the error-fallback redirect now prefer the emitting request's own classification; opt-in `server.proxy.requireForwardedHeaders` makes classification deterministic (#B152).** A request whose inbound Host carries no `:port` is classified reverse-proxied (the first arm of the per-request classification, both engines) and REFRESHES `process.gina.PROXY_HOST`/`PROXY_HOSTNAME` — but a port-less Host is also what an internal call addressed by service/DNS name carries (a container health probe on an app route — only `/_gina/health/check` itself, `$`-anchored, bypasses the classification block — a mesh hop, a sibling-bundle request), so ONE such call repointed every later worker-global read at the internal host: both `getUrl` filter branches (the path/bundle-override `hostname` composition AND the post-`getRoute` `proxy_hostname` override feeding `toUrl()`), the redirect-by-URL `getRoute(path).toUrl()` branch, and `throwError`'s route-object fallback. On a proxy-less fresh worker the same call CREATES proxy mode permanently (the composite latch arms once the global exists). Fixed server-side, NO dist rebuild (`getUrl` is not browser-bundled — the dist's `getUrl` hits are `getUrlProps`, a substring artifact): all four readers prefer THIS request's per-request slots (`req._ginaIsProxyHost` strict-`=== true` + `req._ginaProxyHostname`) over the worker-global, which stays the fallback for renders with no request of their own — a raw victim resolves the config host despite the latch, a proxied victim emits its own host, a slot-less caller is byte-identical; `core/router.js` fills the slots when ABSENT so the Express engine (which never set them) gets per-request truth engine-agnostically, isaac's earlier identical classification always winning. The opt-in key (settings.json + published schema; strict boolean, fail-safe false, boot-resolved once, `console.info` when armed) disables the port-less heuristic so ONLY X-Forwarded-Host classifies as proxied — the one mechanism that also protects req-less renders; enable it only behind a front proxy that always sends X-Forwarded-Host. Two mechanism corrections vs the field report that surfaced this: `PROXY_PORT` is boot-static (never request-refreshed; the boot scheme-switch forces it to 80/443, clobbering any proxy.json-hostname port), so a nonstandard port in an emitted URL comes from the victim request's OWN normalized `headers.port`/`[':port']`, never the poisoner; and moving container probes off `/_gina/health/check` onto app routes is what acquires the bug — nothing signals it. Pickup: restart (server-side only). Tests: `test/core/proxy-request-scope.test.js` (44, incl. a real `getRoute`/`toUrl` subtract reproducing the internal-host emission pre-fix).
|
|
995
|
-
277. **Render/output cache — pluggable backends, release-namespaced keys, event invalidation, flush, Cache-Status (consolidates former #234/#235/#237/#238/#242/#244; 0.5.18–0.5.22).** The framework caches rendered responses under two key namespaces — `static:` HTML + `data:` JSON — in the multi-purpose server Map (which ALSO holds `swig:` compiled templates + `http2session:` sessions: every bulk operation scopes to the output namespaces ONLY). CONFIG: bundle-wide defaults on `server.cache` (`enable` is the hard gate for writer AND reader; plus `path`/`ttl`/`sliding`/`maxAge`); `type`/`store` come from the bundle `config/settings.json` top-level `cache` block, folded into the runtime `server.cache` at config-load (env.json keys win; without the fold the documented default was inert); per-route via a routing.json rule's top-level `cache: {…}` with FILL-only inheritance — a per-route value always wins, and a route that sets `type` at all keeps it (only an OMITTED `type` inherits the bundle default). STRATEGIES: `memory` (inline content); `fs` (body file + a `.meta` sidecar carrying created/ttl/sliding/maxAge/headers/events; restart READ-BACK on an index miss with ABSOLUTE expiry preserved — a restart never extends a TTL; pure-sliding read-back counts as a fresh access, a documented imprecision); `redis` two-tier (L1 = the in-process Map written synchronously; L2 = shared redis via fire-and-forget `PSETEX` — the response never waits; `warm()` repopulates L1 with the AUTHORITATIVE remaining PTTL and re-registers the route's events so a warmed entry stays evictable; fail-open with one once-per-process warn + a 1s `commandTimeout` so a blackholed socket cannot hang the render hot path; a graceful shutdown leaves L2 INTACT — peer replicas and the restarting replica warm from it; every redis route needs a ttl or `invalidateOnEvents` — boot-enforced fail-fast, with proportionality: an `enable:false` bundle never opens a connection and would-be-fatals downgrade to loud warns). KEYS: one `buildKey` owns `[<token>:]<kind>:<bundle>:<url>` at every writer AND reader (the format was previously duplicated at 4 sites and drifted); the token (`GINA_CACHE_NAMESPACE || GINA_VERSION`, sanitized) release-namespaces every cache so a framework upgrade auto-invalidates, and the token is ALSO an fs PATH segment — a measured requirement, not cosmetics: with the token only in the key, a namespace change reused the same file path and read-back served stale cross-namespace bytes. EVENTS: routes register `cache.invalidateOnEvents`; fire from a controller via `self.cache.invalidateByEvent(event)` (a deliberately NARROW facade — it never exposes `from()`/`set()`, and reaches the OWN process only) or cross-bundle via `gina cache:clear @<project> --event=<name>` / `POST /_gina/cache/clear?event=<name>`; TTL/sliding expiry runs the SAME eviction as an explicit delete (cleanup fns fire + registrations reclaim), and fs/redis persistence re-registers events on read-back/warm. FLUSH: the CLI runs the offline fs reclaim FIRST (works with the bundle down; reclaims prior-release orphan dirs; never touches the reserved `config`/`swig` infra-cache dirs) then the in-heap endpoint (admin-gated, POST-only — a flush is a mutation a prefetch must not fire; `event` WINS over `bundle`); `--dry-run` previews the fs set + probes reachability read-only and deliberately claims NO count. READ PATH: engine-agnostic in the shared request handler (express included — previously isaac-only), so a fresh replica's FIRST GET can serve a peer replica's write; every hit carries RFC 9211 `Cache-Status:
|
|
996
|
+
277. **Render/output cache — pluggable backends, release-namespaced keys, event invalidation, flush, Cache-Status (consolidates former #234/#235/#237/#238/#242/#244; 0.5.18–0.5.22).** The framework caches rendered responses under two key namespaces — `static:` HTML + `data:` JSON — in the multi-purpose server Map (which ALSO holds `swig:` compiled templates + `http2session:` sessions: every bulk operation scopes to the output namespaces ONLY). CONFIG: bundle-wide defaults on `server.cache` (`enable` is the hard gate for writer AND reader; plus `path`/`ttl`/`sliding`/`maxAge`); `type`/`store` come from the bundle `config/settings.json` top-level `cache` block, folded into the runtime `server.cache` at config-load (env.json keys win; without the fold the documented default was inert); per-route via a routing.json rule's top-level `cache: {…}` with FILL-only inheritance — a per-route value always wins, and a route that sets `type` at all keeps it (only an OMITTED `type` inherits the bundle default). STRATEGIES: `memory` (inline content); `fs` (body file + a `.meta` sidecar carrying created/ttl/sliding/maxAge/headers/events; restart READ-BACK on an index miss with ABSOLUTE expiry preserved — a restart never extends a TTL; pure-sliding read-back counts as a fresh access, a documented imprecision); `redis` two-tier (L1 = the in-process Map written synchronously; L2 = shared redis via fire-and-forget `PSETEX` — the response never waits; `warm()` repopulates L1 with the AUTHORITATIVE remaining PTTL and re-registers the route's events so a warmed entry stays evictable; fail-open with one once-per-process warn + a 1s `commandTimeout` so a blackholed socket cannot hang the render hot path; a graceful shutdown leaves L2 INTACT — peer replicas and the restarting replica warm from it; every redis route needs a ttl or `invalidateOnEvents` — boot-enforced fail-fast, with proportionality: an `enable:false` bundle never opens a connection and would-be-fatals downgrade to loud warns). KEYS: one `buildKey` owns `[<token>:]<kind>:<bundle>:<url>` at every writer AND reader (the format was previously duplicated at 4 sites and drifted); the token (`GINA_CACHE_NAMESPACE || GINA_VERSION`, sanitized) release-namespaces every cache so a framework upgrade auto-invalidates, and the token is ALSO an fs PATH segment — a measured requirement, not cosmetics: with the token only in the key, a namespace change reused the same file path and read-back served stale cross-namespace bytes. EVENTS: routes register `cache.invalidateOnEvents`; fire from a controller via `self.cache.invalidateByEvent(event)` (a deliberately NARROW facade — it never exposes `from()`/`set()`, and reaches the OWN process only) or cross-bundle via `gina cache:clear @<project> --event=<name>` / `POST /_gina/cache/clear?event=<name>`; TTL/sliding expiry runs the SAME eviction as an explicit delete (cleanup fns fire + registrations reclaim), and fs/redis persistence re-registers events on read-back/warm. FLUSH: the CLI runs the offline fs reclaim FIRST (works with the bundle down; reclaims prior-release orphan dirs; never touches the reserved `config`/`swig` infra-cache dirs) then the in-heap endpoint (admin-gated, POST-only — a flush is a mutation a prefetch must not fire; `event` WINS over `bundle`); `--dry-run` previews the fs set + probes reachability read-only and deliberately claims NO count. READ PATH: engine-agnostic in the shared request handler (express included — previously isaac-only), so a fresh replica's FIRST GET can serve a peer replica's write; every hit carries RFC 9211 `Cache-Status: <name>; hit; ttl=N; detail=memory|redis|fs` (naming the physical tier that served the bytes) and every genuine miss `<name>; fwd=uri-miss` — `<name>` = `server.cache.name` when set to a valid token (#B238: a letter then up to 63 of `[A-Za-z0-9._-]`, a conservative RFC 8941 subset; an invalid value boot-warns and falls back), else the default `gina-cache`, resolved ONCE at boot (server.js stamps `instance._cacheName`; all mint sites on both engines read the stamp, so the identifier cannot disagree across engines or hit/miss) — and when `server.hidePoweredBy` is true + the cache enabled + no name set, boot warns that the wire still names the framework (warn-not-flip: the identifier is a documented-stable wire value — set any token e.g. `cache` to close the disclosure, or explicit `gina-cache` to silence); cached routes re-mint CSP nonces on hits (#B130 — header + body, stored entries never mutated). **A gated route is never cached** — pairing `cache` with `param.requireAuth`/`roles`/`policy` (or leaving it mode-gated) REFUSES the boot, and the delegates skip the write regardless: the key carries no principal and both serve points precede the authorization gate (#B158). Server-side only — none of this ever needs a dist rebuild. Tests: `test/lib/render-cache.test.js`, `test/core/server-render-cache-read.test.js`, `test/core/server-render-cache-boot.test.js`, `test/lib/cache-clear.test.js`; consumer guide: the docs-site caching guide.
|
|
996
997
|
|
|
997
998
|
278. **A cache key is opaque DATA — never route it through an expression/condition evaluator.** An output-cache key embeds the request URL, so a querystring's `?`/`=` parse as OPERATOR tokens in any condition-DSL lookup — the render cache's event registry originally lived in a collection whose dedup ran keys through the condition evaluator, and the SECOND cache-miss render of any cached GET route with `invalidateOnEvents` + a querystring THREW (in the swig path that rejection unwound to the function-level try, whose catch answered 500 — discarding an already-rendered page). The registry became a plain array matched with `===`. Generalizes beyond caching: never let a value carrying user-controlled characters reach a query/condition DSL as an expression — match registry rows with `===` on plain structures, and treat every composite key as an opaque string.
|
|
998
999
|
|
|
@@ -1007,7 +1008,7 @@ Dev-mode query instrumentation captures every database query tied to the current
|
|
|
1007
1008
|
|
|
1008
1009
|
284. **Couchbase session-store `touch()` — an idle-check that compared milliseconds against seconds, and why fixing only the units would have been worse (#B165, 2026-07-26, unreleased).** Both the SDK-3 and SDK-4 stores gated their `lastModified` re-stamp on `timeElapsed > ttl`, where `timeElapsed` was a millisecond difference (`Date.getTime()` minus the parsed stamp) while `ttl` was in SECONDS — so the branch fired ~1000x sooner than its wording implied (86.4s idle on the 86400s default; 3.6s against a one-hour `cookie.maxAge`). The trap is that the obvious repair — converting the units — is a REGRESSION: the `upsert` below refreshes the document's expiry on EVERY touch unconditionally, so the stamp must track every extension; gating it on `ttl` seconds of idleness means firing only after the session has already expired, freezing `lastModified` at the first `set()`. That stamp is not decorative — the browser reads it (`gina.session.lastModified`) and derives the live session countdown as `lastModified + originalTimeout - now`, so a frozen origin inflates the remaining time shown to the user. FIX: delete the idle-check outright and stamp unconditionally, keeping only the meaningful `ttl > 0` guard that `set()` already used — removing the defect by construction rather than by arithmetic. The SDK-4 store additionally stamped through a zone-less local-time mask (`yyyy-mm-dd'T'HH:MM:ss`), which the browser re-parses as BROWSER-local and so skewed the countdown by the server/browser offset; both stores now use `toISOString()`, matching the redis / sqlite / mongodb / scylladb stores. The `touchAfter` option the old comment referenced never existed anywhere in the codebase (imported wholesale from connect-mongo). Server-side only — no dist rebuild; a bundle restart picks it up. Rule worth carrying: when a stored value is the origin of a client-visible computation, throttling its writes is not an optimisation, it is a correctness bug.
|
|
1009
1010
|
|
|
1010
|
-
285. **Session-store ttl vs cookie maxAge — the implicit one-day default that shadowed the cookie (#B163, 2026-07-27, unreleased).** The redis/sqlite/mongodb/scylladb session stores defaulted the constructor ttl to `connConf.ttl || oneDay`, so `this.ttl` was truthy (86400) in every ordinary configuration and the byte-identical `set()`/`touch()` fallback `var ttl = this.ttl || ('number' === typeof maxAge ? maxAge / 1000 | 0 : oneDay)` never reached the cookie's `maxAge` — a 1-hour cookie left its record alive server-side for 24h (orphan records, and a leaked session id stayed valid long after its cookie died — the id is whispered into every rendered page), while a 7-day cookie was silently cut to 24h. Fixed by keeping nothing-configured null (`connConf.ttl || null`), converging all six stores on the couchbase constructors' `options.ttl || null`: unset ttl → the record follows the cookie's maxAge → one day only when the cookie has none; an explicit ttl (store options or the connector entry's `ttl`) still wins, and `{ttl: 0}` still means defer-to-maxAge (an explicit connector `ttl: 0` now defers too, instead of being rewritten to a day). The change moves record lifetimes in BOTH directions vs the old accidental 24h cap — that cap was an accidental default, not a security control. Tests: `test/core/session-store-ttl.test.js` executes the shipped constructor + set()/touch() expressions extracted from the source (extraction control-gated), red-first validated 17-fail against the pre-fix bytes. Consequence worth knowing: making the cookie-`maxAge` fallback reachable also made a NON-POSITIVE resolved ttl reachable (the getter decays), which activated a latent missing guard in the sqlite store's `touch()` — see #287.
|
|
1011
|
+
285. **Session-store ttl vs cookie maxAge — the implicit one-day default that shadowed the cookie (#B163, 2026-07-27, unreleased).** The redis/sqlite/mongodb/scylladb session stores defaulted the constructor ttl to `connConf.ttl || oneDay`, so `this.ttl` was truthy (86400) in every ordinary configuration and the byte-identical `set()`/`touch()` fallback `var ttl = this.ttl || ('number' === typeof maxAge ? maxAge / 1000 | 0 : oneDay)` never reached the cookie's `maxAge` — a 1-hour cookie left its record alive server-side for 24h (orphan records, and a leaked session id stayed valid long after its cookie died — the id is whispered into every rendered page), while a 7-day cookie was silently cut to 24h. Fixed by keeping nothing-configured null (`connConf.ttl || null`), converging all six stores on the couchbase constructors' `options.ttl || null`: unset ttl → the record follows the cookie's maxAge → one day only when the cookie has none; an explicit ttl (store options or the connector entry's `ttl`) still wins, and `{ttl: 0}` still means defer-to-maxAge (an explicit connector `ttl: 0` now defers too, instead of being rewritten to a day). The change moves record lifetimes in BOTH directions vs the old accidental 24h cap — that cap was an accidental default, not a security control. Tests: `test/core/session-store-ttl.test.js` executes the shipped constructor + set()/touch() expressions extracted from the source (extraction control-gated), red-first validated 17-fail against the pre-fix bytes. Consequence worth knowing: making the cookie-`maxAge` fallback reachable also made a NON-POSITIVE resolved ttl reachable (the getter decays), which activated a latent missing guard in the sqlite store's `touch()` — see #287. #B207 (2026-08-02) closes the pair: constructors now REFUSE a non-positive configured ttl at bundle init on both channels (store options + the connectors.json session entry; couchbase reads options only, stated in the schema `ttl` description), and every set() carries the touch() guard — a resolved ttl <= 0 no-ops instead of reaching the backend, where the camps split OPPOSITE ways (redis wrote an expiry-less plain SET, couchbase upserted a zero expiry, scylladb `USING TTL 0` — all IMMORTAL records for an already-expired session — while mongodb/sqlite wrote an instantly-dead row); couchbase touch() gains the guard it alone lacked, and redis's plain-SET branch is gone. A configured `ttl: 0` previously collapsed to "unset" via double truthiness on every path — it never reached any backend — so the refusal breaks nothing that ever worked.
|
|
1011
1012
|
|
|
1012
1013
|
286. **`req.logout()` destroys the session record — the gina-native logout shim's missing terminal (#B164, 2026-07-27, unreleased).** The per-request logout shim (`core/router.js`, installed inside `route()` under the Passport-HTTP2-fix block) only set `request.session.user = null` on its gina-native branch — the request de-authenticated (the authorization gate reads that key), but there was no `destroy()`, no rotation, no cookie clear: the store record, the session id (whispered into every rendered page) and every other session key (cart, flash, `inheritedData`) survived to TTL. The gina-native branch now also calls the session's own `destroy()` when it exposes one (express-session's `Session.prototype.destroy` deletes `req.session` synchronously, then destroys the store record), degrades gracefully when it does not (user still cleared, callback still fires), and takes an optional `done(err)` callback honoured on every path. The destroy is gated on the session scope, so a call after destruction (no `request.session` left) can never reach `request.destroy()` — the stream method. Install-guard arms and the Passport branch are byte-untouched: the shim never installs for a normally-initialized Passport bundle (`passport.initialize()` sets `req.logOut` + `req._passport` before `router.route()` runs), and Passport ≥ 0.6 already destroys the record via its own `regenerate()`. Cookie clearing stays the consumer's job (the cookie name is not discoverable from the request — expire it yourself after logout), and session-id rotation on privilege change is separate hardening. Tests: `test/core/router-logout.test.js` brace-match-extracts the shipped shim and drives it behaviourally (extraction control-gated), red-first validated 7-fail against the pre-fix bytes.
|
|
1013
1014
|
|
|
@@ -1027,8 +1028,9 @@ Dev-mode query instrumentation captures every database query tied to the current
|
|
|
1027
1028
|
|
|
1028
1029
|
295. **Inherited-name collisions in query-file loaders warn at boot (#B173, 0.6.1).** Six ORM connectors (mysql, postgresql, sqlite, duckdb, scylladb, mongodb) attach file-derived methods (`sql/<Entity>/<name>.sql`, `cql/…`, `pipelines/<Entity>/<name>.json`) behind `typeof entities[E].prototype[name] !== 'undefined'` — a prototype-CHAIN lookup, so a file named after an inherited member (gina's `Object.prototype` extensions `count()`/`functionCount()`, EventEmitter's `on`/`once`/`emit`, the entity base API) was silently skipped and calls fell through to the inherited member, returning a plausible value of the right type (`count.sql` → the global property counter). Since 0.6.1 the skip logs a `console.warn` through the connector's gina-logger shadow (level `warn`=4 passes the default `log_level: "info"`=6) naming the method, source file path, entity, and a rename hint (`countRows.sql`); own-property collisions still skip SILENTLY by design — user code in the entity `.js` wins over a same-named query file. The skip/attach set is byte-identical to before (the warn sits inside the already-taken branch; behavior verified unchanged live on an isolated boot with a pre-fix control). Couchbase is the deliberate exception: it attaches unconditionally, so there a colliding name SHADOWS the inherited member instead of being skipped (a file named `on.sql` would clobber EventEmitter's `on` for that entity) — and since 0.6.1 that clobber ALSO warns at boot (#B174): an own-member overwrite (a stamped entity property, a previously attached query method) or a non-`Object.prototype` inherited shadow (the EventEmitter API) logs the file path with a rename hint, while shadowing `count()`/`functionCount()` stays silent — there the query file winning is the point. Couchbase attachment behavior itself is unchanged: the warn never skips the file.
|
|
1029
1030
|
|
|
1030
|
-
296. **FormValidator submits: one XHR per send + a fail-safe lock release (#B175/#B176, 0.6.1).** `send()` used to reuse ONE module-scope `XMLHttpRequest` (created once at validator init): re-`open()`ing the completed instance synchronously replayed the PREVIOUS submit's still-assigned `onreadystatechange` at readyState 1, re-disabling that form's submit trigger and re-stamping its `data-gina-form-loading` with no release path (the lock was armed at four sites and released only inside readyState 4) — so submitting form B after form A had completed stranded A's trigger natively-disabled until something else happened to re-enable it. Since 0.6.1 every send builds its own LOCAL XHR (per-submit handler/state/lifecycle; the reuse's implicit cross-form abort is gone — each form settles its own request) and registers a `loadend` release — fires on success, error, timeout and abort alike — that removes `disabled`/`aria-disabled` + `data-gina-form-loading` and clears `$form.isSending`/`$form.sent`; `$form.isSending` now genuinely spans send→settled (it used to be cleared at the first readyState transition, i.e. almost immediately — `$form.sent` was the only flag that spanned the request); the timeout path removes `data-gina-form-loading` instead of writing the truthy string `"false"`. Same release, #B176: the live-check opt-out is honored CONSISTENTLY — two validation gates (the select-change handler and the bind-time silent-validation pass) evaluated the rules-count boolean INSIDE `/^(true)$/i.test(…)`, so an explicit `data-gina-form-live-check-enabled="false"` (a truthy string) short-circuited to the count boolean and matched, leaving those two running on a form that had opted out; the gates now test the attribute alone, then check the count. Scope worth knowing before auditing a form's pre-0.6.1 behaviour: the opt-out was NOT ignored outright — the three `registerForLiveChecking` sites (textarea / form-associated custom element / input) already tested the attribute alone, so text as-you-type never ran on an opted-out form, and the submit trigger ended enabled either way because the trigger's show branch is forced whenever live-check is off. The pre-fix behaviour was an inconsistent middle; only bind-time and select-change validation change at pickup. Browser-bundled — consumers re-bake at pickup. Caution that stays by design: `setOptions()` is a MODULE-wide default setter, not per-call — e.g. `setOptions({withRateLimit: false})` turns rate limiting off for every form for the rest of the page lifetime (per-form options are a demand-gated follow-up).
|
|
1031
|
+
296. **FormValidator submits: one XHR per send + a fail-safe lock release (#B175/#B176, 0.6.1).** `send()` used to reuse ONE module-scope `XMLHttpRequest` (created once at validator init): re-`open()`ing the completed instance synchronously replayed the PREVIOUS submit's still-assigned `onreadystatechange` at readyState 1, re-disabling that form's submit trigger and re-stamping its `data-gina-form-loading` with no release path (the lock was armed at four sites and released only inside readyState 4) — so submitting form B after form A had completed stranded A's trigger natively-disabled until something else happened to re-enable it. Since 0.6.1 every send builds its own LOCAL XHR (per-submit handler/state/lifecycle; the reuse's implicit cross-form abort is gone — each form settles its own request) and registers a `loadend` release — fires on success, error, timeout and abort alike — that removes `disabled`/`aria-disabled` + `data-gina-form-loading` and clears `$form.isSending`/`$form.sent`; `$form.isSending` now genuinely spans send→settled (it used to be cleared at the first readyState transition, i.e. almost immediately — `$form.sent` was the only flag that spanned the request); the timeout path removes `data-gina-form-loading` instead of writing the truthy string `"false"`. Same release, #B176: the live-check opt-out is honored CONSISTENTLY — two validation gates (the select-change handler and the bind-time silent-validation pass) evaluated the rules-count boolean INSIDE `/^(true)$/i.test(…)`, so an explicit `data-gina-form-live-check-enabled="false"` (a truthy string) short-circuited to the count boolean and matched, leaving those two running on a form that had opted out; the gates now test the attribute alone, then check the count. Scope worth knowing before auditing a form's pre-0.6.1 behaviour: the opt-out was NOT ignored outright — the three `registerForLiveChecking` sites (textarea / form-associated custom element / input) already tested the attribute alone, so text as-you-type never ran on an opted-out form, and the submit trigger ended enabled either way because the trigger's show branch is forced whenever live-check is off. The pre-fix behaviour was an inconsistent middle; only bind-time and select-change validation change at pickup. Browser-bundled — consumers re-bake at pickup. Caution that stays by design: `setOptions()` is a MODULE-wide default setter, not per-call — e.g. `setOptions({withRateLimit: false})` turns rate limiting off for every form for the rest of the page lifetime (per-form options are a demand-gated follow-up). **Third release in the same family, #B192 (0.6.3): a REJECTED submit now clears the `isSubmitting` latch.** The submit path arms `instance.$forms[id].isSubmitting = true` before validating — deliberately, so the live-check field listener stays quiet while a request is in flight (it hard-returns on any truthy `isSubmitting`). But the ONLY clear was the XHR settle, which a rejected submit never reaches: it renders the field errors and sends nothing. So one invalid submit attempt latched the flag true for the rest of the page's life — every later keystroke was swallowed, `updateSubmitTriggerState` never ran again, and the submit trigger kept `aria-disabled="true"` + `.gina-form-submit-disabled` (keyboard/AT users hard-blocked; mouse users saw a disabled-looking trigger that still submitted, so presentation and behaviour disagreed). Because the flag lives on the `$forms[id]` OBJECT rather than a listener closure, it also survived a full unbind/rebind — `reBind()` re-gated correctly but never restored the live check, which is the tell that distinguishes this from a listener-set bug. The internal `validate.<id>` handler's invalid branch — the terminal no-send outcome — now releases the latch before its #A11Y1 focus-first-invalid step; the valid branch is untouched, so the latch still spans send→settled and live-check still stays quiet during a real in-flight submit. Diagnostic shortcut for any similar report: read `gina.validator.$forms['<id>'].isSubmitting` — truthy IS the latched state, since that is exactly what the live-check gate tests; the other reliable discriminator is the live-check request count after a keystroke (0 when latched, 1 when healthy). ⚠️ **Do NOT use `isValidating` for this** — an earlier version of this entry did, and a consumer's A/B measured it non-discriminating (2026-08-01). `isValidating` is an in-flight flag, not a latch indicator: it inits `null` and is set true/false by EVERY validation cycle (live-check, focusin, submit), so it reads `null` only until the FIRST cycle completes and `false` at rest forever after — on any form that live-checked before the submit attempt (the normal case) it reads `false` in BOTH the latched and healthy states. The original wording held only because the fixture it was derived from happened never to live-check before submitting. Browser-bundled — consumers re-bake at pickup.
|
|
1031
1032
|
|
|
1032
1033
|
297. **Validator error labels & rendering — observable keys now translatable, announcements separator-joined, identical messages deduped (#B178 family, 0.6.1).** The key an app can OBSERVE in a field's `errors` object was not always the key the label catalog is consulted under: four rule families write the error under a generic key while consulting a SPECIFIC label key — the float coercion's NaN branch renders `errorLabels['toFloatNAN']` into `errors['toFloat']`, and the number/integer/string Length families render the `Min`/`Max` label variants into the generic `is<X>Length` errors key — so translating the observable key (bundle catalog `_validator` node or `setErrorLabels()`) was a silent no-op and a partial catalog rendered one localized message beside one English default. Fixed with an alias fill applied to every app-supplied label layer (server catalog node, client catalog whisper, per-culture registrations, `setErrorLabels()`): a supplied generic fills the specific keys the app did not supply itself; a supplied specific still wins; English defaults are untouched. Same release: numbered `is` aliases (`is1`, `is2`, …) had no per-alias default label, so a failing alias with no rule-supplied text rendered an EMPTY message — they now fall back to the shared `is` label (translate catalog key `is` once to cover every alias); and the user-validator setup loop unconditionally reset every user-defined validator's label to the English default, clobbering catalog translations — it now fills only when the app supplied none. Two render-side fixes ride along: the error container renders one `<p>` per error key and the aria-live announcement passed the container's raw `textContent`, which concatenates with NO separator — a screen reader received multiple messages as one run-on string (visually the messages stack as blocks; only the announcement channel ran them together); announcements now join the per-message texts with a sentence separator. And two rules resolving to byte-identical message text (canonically a coercion paired with its validator, both failing on the same non-numeric input) rendered the same sentence twice — each distinct text now renders once, while the dev inspector still records every error key. Browser-bundled — consumers re-bake at pickup; apps that translated only observable keys will see those messages switch from English to their language.
|
|
1033
1034
|
|
|
1034
1035
|
298. **Render/request-path deep-clone & id-mint reduction (#P39 slice 1, 0.6.x) — the filter-factory options travel by REFERENCE, locale lookups are memoized, `conf.locales` is a lazy per-request accessor, and `lib/uuid` right-sizes its entropy batch.** The CPU-profiling baseline convicted per-request copy/id overhead as ~60% of render-arm CPU, 98.7% of the clone time in three caller-attributed paths; all three are gone. (1) `controller.render-swig.js` / `controller.render-nunjucks.js` hand `local.options` to the filter factory UN-cloned, and `lib/swig-filters` / `lib/nunjucks-filters` `getInstance()` stashes that wrapper by reference — the singleton's `_options` has no writer beyond the stash itself (every filter only reads it; the `getUrl`/`getWebroot` merge fills a fresh `{}` target), the request path already shares `conf.routing`/`conf.reverseRouting`/`conf.forms` by reference, and `req`/`res` always passed by reference anyway (the cloner bails on non-plain constructors) — so reference-passing adds no new mutation-visibility class and the stash-then-await interleave window is byte-identical to the cloned era. Anything that MUTATES what it reads from the filter context must clone its own copy first (the pre-existing contract; `getRoute()` already does). (2) `setOptions` no longer builds two Collections per request over the boot-static region sets (each was a deep copy of ~500 nested records plus one 16-char id minted per record ≈ 750 webcrypto calls, paid by EVERY request in a views-bearing bundle, render or not): a module-level per-culture memo answers the language → region-set and country → row lookups; `conf.locale` is a per-request deep copy of the ONE resolved row (isolating the `.date` write and template mutations); `conf.locales` is a LAZY self-replacing accessor — the request's own deep copy materializes on first read (sole framework reader: `self.getLocales()`), so the common request pays nothing while whole-conf clones and serializers materialize it transparently, and assignment writes through. (3) `lib/uuid` sizes its `getRandomValues` batch to the REQUESTED id length (the fixed 7-byte batch was sized for the 4-char default, so `uuid(16)` — the form `lib/collection` mints per item — needed ~3 webcrypto calls; now 1; each call pays a full randomFillSync regardless of byte count, so call count is the cost). (4) The cloner's dead per-recursion `Object.keys` allocation is removed. Measured A/B on the profiling harness (fresh runs both sides, same knobs): render wall 32,186 → 7,268 ms for 3000 requests = 4.4× throughput (10.7 → 2.4 ms/render), deep-clone share 50.31% → 1.81%, GC 2,522 → 137 ms, randomFillSync out of the top-15 frames on both arms, upload-arm clone floor 253 → 11 ms — the pre-stated ≈2× ceiling was beaten because allocation-pressure knock-on (GC, locality) fell with the clones. `lib/uuid` is AMD-bundled, so the browser bundle changed: consumer pickup = restart AND per-bundle re-bake. `Collection`'s own eager per-item `_uuid` minting is UNCHANGED (its only consumers are collection-internal; `toRaw()` strips). Remaining clone sites (hot route-matching-loop clones, `getRoute`-per-`getUrl` filter call, `getParams`/`getConfig` per-call clones, cssColl/jsColl construction, async-delegate store clones) stay measure-gated for a future profile.
|
|
1036
|
+
299. **lib/math checkSum helpers - dispatch & serialization contract**: `checkSumSync(filenameOrData, algorithm, encoding)` (defaults md5/hex) accepts a string, object, or array; objects/arrays are serialized first - a plain object becomes sorted `key:value` pairs joined with commas (function properties skipped), an array becomes the JSON of a sorted copy (order-insensitive; the input array is never mutated). A string ending in an extension shape (a dot + 1-10 alphanumerics) is PROBED as a filename, and the file branch is taken only when the path resolves to an existing regular file (stat-gated): a stat miss (no such entry, name too long, not-a-directory path, NUL-carrying string) or an exists-but-directory entry falls through to hashing the input as DATA, so serialized records ending in `.com`/`.net`/`.pdf` return checksums instead of throwing ENOENT/ENAMETOOLONG (#B209; until 0.6.2 they threw, with the caller's data named as a path in the error), while an EXISTING unreadable file (EACCES) still throws. Until 0.6.2 every ARRAY input also collapsed to the checksum of the empty string - all arrays collided at one constant hash - and sort() reordered the caller's array in place (#B208, fixed alongside; previously stored array checksums were the degenerate constant and change by construction). The wide probe shape is #B210 (fixed alongside): before it only dot+3-lowercase tails fired, so paths like `file.js`, `file.json`, or `FILE.TXT` hashed as PATH STRINGS - stored sums for such paths change on upgrade (they never tracked content); an extension-LESS path (`Makefile`, `LICENSE`) still hashes as a data string, never read from disk - pass file content (or an extension-bearing path) when you mean the file. The async sibling `checkSum(filename, algorithm, encoding, isCheckingFromData, cb)` routes to the file branch on ANY dot in the input unless `isCheckingFromData` is true. `operate(expression)` evaluates arithmetic strings via a shunting-yard parser (`+ - * / %`, parentheses, decimals, unary sign) and throws on any non-arithmetic character - no eval / new Function (#SCS1).
|