gina 0.6.21 → 0.6.22
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 +6 -0
- package/README.md +13 -18
- package/ROADMAP.md +1 -0
- package/framework/v0.6.22/VERSION +1 -0
- package/framework/{v0.6.21 → v0.6.22}/core/asset/plugin/dist/vendor/gina/js/gina.js +110 -8
- package/framework/v0.6.22/core/asset/plugin/dist/vendor/gina/js/gina.min.js +655 -0
- package/framework/v0.6.22/core/asset/plugin/dist/vendor/gina/js/gina.min.js.br +0 -0
- package/framework/v0.6.22/core/asset/plugin/dist/vendor/gina/js/gina.min.js.gz +0 -0
- package/framework/{v0.6.21 → v0.6.22}/core/plugins/lib/validator/src/main.js +51 -8
- package/framework/{v0.6.21 → v0.6.22}/helpers/data/src/main.js +19 -0
- package/framework/{v0.6.21 → v0.6.22}/lib/merge/src/main.js +40 -0
- package/framework/{v0.6.21 → v0.6.22}/package.json +1 -1
- package/gna.js +4 -4
- package/llms.txt +3 -3
- package/package.json +3 -3
- package/framework/v0.6.21/VERSION +0 -1
- package/framework/v0.6.21/core/asset/plugin/dist/vendor/gina/js/gina.min.js +0 -653
- package/framework/v0.6.21/core/asset/plugin/dist/vendor/gina/js/gina.min.js.br +0 -0
- package/framework/v0.6.21/core/asset/plugin/dist/vendor/gina/js/gina.min.js.gz +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/AUTHORS +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/LICENSE +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/asset/html/nolayout.html +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/asset/html/static.html +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/asset/img/android-chrome-192x192.png +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/asset/img/android-chrome-512x512.png +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/asset/img/apple-touch-icon.png +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/asset/img/favicon-16x16.png +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/asset/img/favicon-32x32.png +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/asset/img/favicon.ico +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/asset/plugin/README.md +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/asset/plugin/dist/vendor/gina/beemaster/beemaster.css +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/asset/plugin/dist/vendor/gina/beemaster/beemaster.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/asset/plugin/dist/vendor/gina/beemaster/index.html +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/asset/plugin/dist/vendor/gina/css/gina.min.css +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/asset/plugin/dist/vendor/gina/css/gina.min.css.br +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/asset/plugin/dist/vendor/gina/css/gina.min.css.gz +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/asset/plugin/dist/vendor/gina/html/statusbar.html +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/asset/plugin/dist/vendor/gina/html/statusbar.html.br +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/asset/plugin/dist/vendor/gina/html/statusbar.html.gz +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/asset/plugin/dist/vendor/gina/inspector/have_heart_one-webfont.woff2 +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/asset/plugin/dist/vendor/gina/inspector/index.html +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/asset/plugin/dist/vendor/gina/inspector/inspector.css +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/asset/plugin/dist/vendor/gina/inspector/inspector.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/asset/plugin/dist/vendor/gina/inspector/logo.svg +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/asset/plugin/dist/vendor/gina/js/gina.onload.min.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/asset/plugin/dist/vendor/gina/js/gina.onload.min.js.br +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/asset/plugin/dist/vendor/gina/js/gina.onload.min.js.gz +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/config.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/connectors/ai/index.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/connectors/ai/lib/connector.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/connectors/couchbase/index.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/connectors/couchbase/lib/connector.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/connectors/couchbase/lib/connector.v3.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/connectors/couchbase/lib/connector.v4.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/connectors/couchbase/lib/n1ql.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/connectors/couchbase/lib/session-store.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/connectors/couchbase/lib/session-store.v3.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/connectors/couchbase/lib/session-store.v4.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/connectors/couchbase/lib/storage-store.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/connectors/duckdb/index.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/connectors/duckdb/lib/connector.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/connectors/mongodb/index.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/connectors/mongodb/lib/connector.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/connectors/mongodb/lib/job-store.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/connectors/mongodb/lib/pipeline-loader.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/connectors/mongodb/lib/session-store.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/connectors/mysql/index.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/connectors/mysql/lib/connector.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/connectors/param-redact.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/connectors/postgresql/index.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/connectors/postgresql/lib/connector.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/connectors/redis/index.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/connectors/redis/lib/connector.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/connectors/redis/lib/job-store.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/connectors/redis/lib/kv-store.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/connectors/redis/lib/render-cache-store.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/connectors/redis/lib/session-store.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/connectors/scylladb/index.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/connectors/scylladb/lib/connector.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/connectors/scylladb/lib/session-store.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/connectors/settle-once.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/connectors/sql-parser.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/connectors/sqlite/index.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/connectors/sqlite/lib/connector.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/connectors/sqlite/lib/job-store.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/connectors/sqlite/lib/kv-store.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/connectors/sqlite/lib/session-store.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/content.encoding +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/controller/controller.framework.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/controller/controller.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/controller/controller.render-json.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/controller/controller.render-nunjucks-async.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/controller/controller.render-nunjucks.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/controller/controller.render-stream.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/controller/controller.render-swig-async.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/controller/controller.render-swig.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/controller/controller.render-v1.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/controller/index.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/controller/inspector-window-emit.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/controller/release-banner.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/dev/index.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/dev/lib/class.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/dev/lib/factory.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/dev/lib/tools.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/gna.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/locales/README.md +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/locales/currency.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/locales/dist/language/en.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/locales/dist/language/fr.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/locales/dist/region/en.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/locales/dist/region/fr.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/locales/index.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/mime.types +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/model/entity.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/model/index.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/model/template/entityFactory.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/model/template/index.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/plugins/README.md +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/plugins/index.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/plugins/lib/csrf/README.md +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/plugins/lib/csrf/package.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/plugins/lib/csrf/src/main.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/plugins/lib/security-headers/README.md +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/plugins/lib/security-headers/coep/README.md +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/plugins/lib/security-headers/coep/package.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/plugins/lib/security-headers/coep/src/main.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/plugins/lib/security-headers/coop/README.md +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/plugins/lib/security-headers/coop/package.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/plugins/lib/security-headers/coop/src/main.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/plugins/lib/security-headers/corp/README.md +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/plugins/lib/security-headers/corp/package.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/plugins/lib/security-headers/corp/src/main.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/plugins/lib/security-headers/csp/README.md +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/plugins/lib/security-headers/csp/package.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/plugins/lib/security-headers/csp/src/main.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/plugins/lib/security-headers/hide-powered-by/README.md +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/plugins/lib/security-headers/hide-powered-by/package.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/plugins/lib/security-headers/hide-powered-by/src/main.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/plugins/lib/security-headers/hsts/README.md +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/plugins/lib/security-headers/hsts/package.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/plugins/lib/security-headers/hsts/src/main.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/plugins/lib/security-headers/origin-agent-cluster/README.md +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/plugins/lib/security-headers/origin-agent-cluster/package.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/plugins/lib/security-headers/origin-agent-cluster/src/main.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/plugins/lib/security-headers/package.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/plugins/lib/security-headers/referrer-policy/README.md +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/plugins/lib/security-headers/referrer-policy/package.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/plugins/lib/security-headers/referrer-policy/src/main.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/plugins/lib/security-headers/src/main.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/plugins/lib/security-headers/x-content-type-options/README.md +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/plugins/lib/security-headers/x-content-type-options/package.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/plugins/lib/security-headers/x-content-type-options/src/main.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/plugins/lib/security-headers/x-dns-prefetch-control/README.md +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/plugins/lib/security-headers/x-dns-prefetch-control/package.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/plugins/lib/security-headers/x-dns-prefetch-control/src/main.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/plugins/lib/security-headers/x-download-options/README.md +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/plugins/lib/security-headers/x-download-options/package.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/plugins/lib/security-headers/x-download-options/src/main.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/plugins/lib/security-headers/x-frame-options/README.md +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/plugins/lib/security-headers/x-frame-options/package.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/plugins/lib/security-headers/x-frame-options/src/main.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/plugins/lib/security-headers/x-permitted-cross-domain-policies/README.md +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/plugins/lib/security-headers/x-permitted-cross-domain-policies/package.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/plugins/lib/security-headers/x-permitted-cross-domain-policies/src/main.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/plugins/lib/security-headers/x-xss-protection/README.md +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/plugins/lib/security-headers/x-xss-protection/package.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/plugins/lib/security-headers/x-xss-protection/src/main.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/plugins/lib/session/README.md +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/plugins/lib/session/package.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/plugins/lib/session/src/main.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/plugins/lib/storage/README.md +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/plugins/lib/storage/build.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/plugins/lib/storage/package.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/plugins/lib/storage/src/main.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/plugins/lib/validator/README.md +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/plugins/lib/validator/build.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/plugins/lib/validator/package.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/plugins/lib/validator/src/form-validator.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/router.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/server.express.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/server.isaac.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/server.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/status.codes +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/template/_gitignore +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/template/boilerplate/bundle/config/app.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/template/boilerplate/bundle/config/connectors.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/template/boilerplate/bundle/config/routing.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/template/boilerplate/bundle/config/settings.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/template/boilerplate/bundle/config/settings.server.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/template/boilerplate/bundle/config/templates.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/template/boilerplate/bundle/config/watchers.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/template/boilerplate/bundle/controllers/controller.content.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/template/boilerplate/bundle/controllers/controller.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/template/boilerplate/bundle/controllers/setup.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/template/boilerplate/bundle/index.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/template/boilerplate/bundle/locales/en.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/template/boilerplate/bundle_namespace/controllers/controller.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/template/boilerplate/bundle_public/css/default.css +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/template/boilerplate/bundle_public/css/home.css +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/template/boilerplate/bundle_public/css/vendor/readme.md +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/template/boilerplate/bundle_public/favicon.ico +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/template/boilerplate/bundle_public/js/components/x-checklist.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/template/boilerplate/bundle_public/js/vendor/readme.md +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/template/boilerplate/bundle_public/manifest.webmanifest +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/template/boilerplate/bundle_public/readme.md +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/template/boilerplate/bundle_public/sw.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/template/boilerplate/bundle_templates/handlers/main.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/template/boilerplate/bundle_templates/html/content/homepage.html +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/template/boilerplate/bundle_templates/html/includes/error-msg-noscript.html +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/template/boilerplate/bundle_templates/html/includes/error-msg-outdated-browser.html +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/template/boilerplate/bundle_templates/html/includes/x-checklist.html +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/template/boilerplate/bundle_templates/html/layouts/main.html +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/template/command/gina.bat.tpl +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/template/command/gina.tpl +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/template/conf/env.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/template/conf/manifest.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/template/conf/package.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/template/conf/settings.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/template/conf/statics.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/template/conf/templates.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/template/error/client/json/401.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/template/error/client/json/403.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/template/error/client/json/404.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/template/error/server/html/50x.html +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/template/error/server/json/500.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/template/error/server/json/503.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/core/template/extensions/logger/config.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/helpers/console.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/helpers/context.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/helpers/data/LICENSE +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/helpers/data/README.md +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/helpers/data/package.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/helpers/dateFormat.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/helpers/index.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/helpers/json/LICENSE +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/helpers/json/README.md +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/helpers/json/package.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/helpers/json/src/main.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/helpers/path.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/helpers/plugins/README.md +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/helpers/plugins/package.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/helpers/plugins/src/api-error.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/helpers/plugins/src/main.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/helpers/prototypes.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/helpers/task.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/helpers/text.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/admin/package.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/admin/src/main.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/archiver/README.md +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/archiver/build.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/archiver/package.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/archiver/src/dep/jszip.min.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/archiver/src/main.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/async/package.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/async/src/main.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/audit/package.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/audit/src/main.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/audit-store.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/authn/package.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/authn/src/lockout.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/authn/src/main.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/authn/src/totp.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/authz-gate/package.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/authz-gate/src/main.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cache/README.md +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cache/build.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cache/package.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cache/src/main.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/aliases.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/audit/arguments.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/audit/help.txt +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/audit/verify.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/bundle/add.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/bundle/arguments.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/bundle/build.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/bundle/copy.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/bundle/cp.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/bundle/help.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/bundle/help.txt +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/bundle/inc/name-rewrite.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/bundle/list.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/bundle/man.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/bundle/mcp-start.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/bundle/mcp.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/bundle/oas.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/bundle/openapi.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/bundle/remove.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/bundle/rename.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/bundle/restart.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/bundle/rm.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/bundle/start.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/bundle/status.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/bundle/stop.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/bundle/types.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/cache/arguments.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/cache/clear.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/cache/help.txt +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/cache/stats.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/connector/add.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/connector/arguments.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/connector/help.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/connector/help.txt +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/connector/infer.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/connector/list.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/connector/migrate.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/connector/models.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/connector/remove.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/connector/rm.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/connector/test.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/container/arguments.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/container/help.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/container/help.txt +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/container/man.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/container/ps.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/container/stop.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/controller/add.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/controller/arguments.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/controller/help.txt +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/controller/inc/args.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/controller/inc/namespace.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/controller/inc/reference-rewrite.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/controller/inc/reference-scan.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/controller/inc/scaffold.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/controller/remove.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/controller/rename.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/controller/rm.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/env/add.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/env/get.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/env/help.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/env/help.txt +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/env/link-dev.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/env/list.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/env/remove.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/env/rm.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/env/set.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/env/unset.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/env/use.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/framework/add.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/framework/arguments.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/framework/build.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/framework/dot.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/framework/get.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/framework/help.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/framework/help.txt +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/framework/init.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/framework/link-node-modules.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/framework/link.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/framework/list.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/framework/man.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/framework/msg.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/framework/open.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/framework/remove.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/framework/reset.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/framework/restart.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/framework/set.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/framework/start.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/framework/status.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/framework/stop.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/framework/tail.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/framework/update.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/framework/version.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/gina-dev.1.md +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/gina-framework.1.md +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/gina.1.md +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/helper.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/i18n/add.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/i18n/arguments.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/i18n/export.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/i18n/help.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/i18n/help.txt +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/i18n/import.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/i18n/scan.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/image/_host.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/image/arguments.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/image/build.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/image/help.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/image/help.txt +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/image/list.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/image/man.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/image/rm.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/image/run.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/index.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/inspector/help.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/inspector/help.txt +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/inspector/open.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/man-render.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/minion/arguments.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/minion/help.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/minion/help.txt +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/minion/kill.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/minion/list.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/msg.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/port/help.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/port/help.txt +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/port/inc/scan.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/port/list.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/port/reset.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/port/set.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/project/add.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/project/arguments.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/project/backup.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/project/build.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/project/help.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/project/help.txt +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/project/import.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/project/list.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/project/man.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/project/move.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/project/remove.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/project/rename.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/project/restart.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/project/restore.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/project/rm.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/project/start.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/project/status.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/project/stop.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/protocol/arguments.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/protocol/help.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/protocol/help.txt +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/protocol/list.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/protocol/remove.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/protocol/set.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/scope/add.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/scope/help.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/scope/help.txt +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/scope/link-local.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/scope/link-production.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/scope/list.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/scope/remove.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/scope/rm.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/scope/use.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/secrets/arguments.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/secrets/check.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/secrets/help.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/secrets/help.txt +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/secrets/scan.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/service/help.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/service/help.txt +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/service/list.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/service/man.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/service/start.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/storage/arguments.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/storage/gc.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/storage/help.txt +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/storage/stats.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/storage/verify.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd/view/add.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd-status-format/package.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cmd-status-format/src/main.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/collection/README.md +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/collection/build.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/collection/package.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/collection/src/main.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/config.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/connector-config/package.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/connector-config/src/main.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/connector-error/package.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/connector-error/src/main.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/connector-registry/package.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/connector-registry/src/main.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cron/README.md +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cron/package.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/cron/src/main.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/domain/LICENSE +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/domain/README.md +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/domain/package.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/domain/src/main.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/dto/package.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/dto/src/main.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/dto-pipe/package.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/dto-pipe/src/main.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/dto-types/package.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/dto-types/src/main.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/generator/index.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/i18n/package.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/i18n/src/main.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/image-build/package.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/image-build/src/main.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/index.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/inherits/LICENSE +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/inherits/README.md +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/inherits/package.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/inherits/src/main.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/inspector-events/package.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/inspector-events/src/main.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/inspector-redact/package.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/inspector-redact/src/main.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/instrument/package.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/instrument/src/main.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/job/package.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/job/src/main.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/job-store.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/json-config-header/package.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/json-config-header/src/main.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/kv/package.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/kv/src/main.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/kv-store.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/loading-state/package.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/loading-state/src/main.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/logger/README.md +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/logger/package.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/logger/src/containers/default/index.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/logger/src/containers/file/index.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/logger/src/containers/file/lib/logrotator/README.md +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/logger/src/containers/file/lib/logrotator/index.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/logger/src/containers/mq/index.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/logger/src/containers/mq/listener.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/logger/src/containers/mq/speaker.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/logger/src/helper.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/logger/src/main.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/logger/src/redact.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/maintenance/package.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/maintenance/src/main.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/math/index.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/mcp-dispatch/package.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/mcp-dispatch/src/main.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/mcp-http/package.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/mcp-http/src/main.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/mcp-server/package.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/mcp-server/src/main.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/merge/README.md +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/merge/package.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/metrics/package.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/metrics/src/main.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/model.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/net-locality/package.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/net-locality/src/main.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/nunjucks-filters/README.md +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/nunjucks-filters/package.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/nunjucks-filters/src/main.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/nunjucks-resolver/package.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/nunjucks-resolver/src/main.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/proc.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/push/package.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/push/src/main.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/rate-limit/package.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/rate-limit/src/main.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/release-watch/package.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/release-watch/src/main.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/render-cache/package.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/render-cache/src/main.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/render-cache-store.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/routing/README.md +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/routing/build.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/routing/package.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/routing/src/main.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/routing/src/radix.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/routing-introspect/package.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/routing-introspect/src/main.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/secrets/package.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/secrets/src/backends/env.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/secrets/src/backends/exec.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/secrets/src/backends/file.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/secrets/src/declaration.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/secrets/src/env-file.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/secrets/src/main.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/secrets/src/sources.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/session-store.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/shell.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/sqlite-driver.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/state.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/storage/package.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/storage/src/local-cas.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/storage/src/local-stream.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/storage/src/local.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/storage/src/main.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/storage/src/meta-store.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/storage/src/s3.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/storage/src/util.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/storage-store.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/swig-filters/README.md +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/swig-filters/package.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/swig-filters/src/main.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/swig-resolver/package.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/swig-resolver/src/main.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/template-loaders/package.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/template-loaders/src/loaders/http.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/template-loaders/src/loaders/memory.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/template-loaders/src/main.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/url/README.md +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/url/index.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/url/routing.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/uuid/package.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/uuid/src/main.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/validator.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/watcher/package.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/watcher/src/main.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/ws-framing/package.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/ws-framing/src/main.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/ws-query/package.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/ws-query/src/main.js +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/ws-session/package.json +0 -0
- /package/framework/{v0.6.21 → v0.6.22}/lib/ws-session/src/main.js +0 -0
|
@@ -1781,6 +1781,17 @@ function ValidatorPlugin(rules, data, formId, culture) {
|
|
|
1781
1781
|
*/
|
|
1782
1782
|
var nestBracketNotationKey = function(obj, key, k, value) {
|
|
1783
1783
|
|
|
1784
|
+
// #B446 — client-side twin of the server guard in helpers/data. Field names come
|
|
1785
|
+
// from the form / FormData, so a segment named `__proto__`, `constructor` or
|
|
1786
|
+
// `prototype` would assign through to Object.prototype for the whole page. Drop
|
|
1787
|
+
// the path rather than throwing: these names are never legitimate field names, and
|
|
1788
|
+
// a throw here would break submission of the entire form.
|
|
1789
|
+
for (let _s = 0, _sLen = key.length; _s < _sLen; ++_s) {
|
|
1790
|
+
if ( key[_s] === '__proto__' || key[_s] === 'constructor' || key[_s] === 'prototype' ) {
|
|
1791
|
+
return obj;
|
|
1792
|
+
}
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1784
1795
|
for (let i = 0, len = key.length; i < len; i++) {
|
|
1785
1796
|
// by default
|
|
1786
1797
|
let _key = key[k];
|
|
@@ -2803,19 +2814,51 @@ function ValidatorPlugin(rules, data, formId, culture) {
|
|
|
2803
2814
|
}
|
|
2804
2815
|
|
|
2805
2816
|
|
|
2806
|
-
}
|
|
2807
|
-
//
|
|
2808
|
-
//
|
|
2809
|
-
|
|
2817
|
+
} else if ( xhr.readyState == 4 && xhr.status === 0 ) {
|
|
2818
|
+
// #B447 — TRANSPORT-layer failure: network down, connection
|
|
2819
|
+
// refused, DNS gone, the server process restarting. XHR settles
|
|
2820
|
+
// at readyState 4 with status 0, which fails BOTH `/^2/.test()`
|
|
2821
|
+
// and `status != 0` above. This arm existed but was commented
|
|
2822
|
+
// out and there was no trailing `else`, so neither `error.<id>`
|
|
2823
|
+
// nor `error.<id>.hform` was emitted — and `error.<id>.hform` is
|
|
2824
|
+
// exactly what `data-gina-form-event-on-submit-error` registers
|
|
2825
|
+
// (see listenToXhrEvents), so a consumer's declared submit-error
|
|
2826
|
+
// handler was silent for the failure it most needs to cover.
|
|
2827
|
+
// The `loadend` fail-safe still released the trigger, so the form
|
|
2828
|
+
// spun, re-enabled, and said nothing.
|
|
2829
|
+
//
|
|
2830
|
+
// Shape mirrors the sibling `xhr.ontimeout` handler below rather
|
|
2831
|
+
// than the old commented block: same 408 + `error` key (the
|
|
2832
|
+
// codebase convention for a client-side XHR failure, also used by
|
|
2833
|
+
// utils/events.js), same #B175
|
|
2834
|
+
// removeAttribute, same onUpload branch. 408 keeps range-checking
|
|
2835
|
+
// handlers (`status >= 400`) working; `transportError` is what
|
|
2836
|
+
// distinguishes this from a genuine timeout, which matters because
|
|
2837
|
+
// `ontimeout` emits a bare 408 for a different failure.
|
|
2838
|
+
//
|
|
2839
|
+
// The stash goes to eventData.ERROR, not .onerror: the three
|
|
2840
|
+
// sibling arms in this same readyState chain all write .error,
|
|
2841
|
+
// it is what the readers actually look at (utils/events.js and
|
|
2842
|
+
// the validate handler), and it is the key the pre-validation
|
|
2843
|
+
// cleanup deletes. The on* keys the ontimeout/onprogress
|
|
2844
|
+
// handlers use are written but never read.
|
|
2845
|
+
result = {
|
|
2846
|
+
'status' : 408,
|
|
2847
|
+
'transportError': true,
|
|
2848
|
+
'error' : 'Transport failure: the request did not reach the server'
|
|
2849
|
+
};
|
|
2850
|
+
|
|
2851
|
+
$form.eventData.error = result;
|
|
2852
|
+
// #B175 — remove, never setAttribute(..., false)
|
|
2853
|
+
$form.target.removeAttribute('data-gina-form-loading');
|
|
2854
|
+
|
|
2810
2855
|
XHRData = result;
|
|
2811
2856
|
// update toolbar
|
|
2812
2857
|
if ( gina && typeof(window.ginaToolbar) != 'undefined' && window.ginaToolbar && XHRData ) {
|
|
2813
2858
|
try {
|
|
2814
|
-
// don't refresh for html datas
|
|
2815
2859
|
if ( envIsDev && typeof(XHRData) != 'undefined' && /\/html/.test(contentType) ) {
|
|
2816
2860
|
window.ginaToolbar.update("data-xhr", XHRData);
|
|
2817
2861
|
}
|
|
2818
|
-
|
|
2819
2862
|
} catch (err) {
|
|
2820
2863
|
throw err
|
|
2821
2864
|
}
|
|
@@ -2823,16 +2866,16 @@ function ValidatorPlugin(rules, data, formId, culture) {
|
|
|
2823
2866
|
|
|
2824
2867
|
// intercept upload
|
|
2825
2868
|
if ( /^gina\-upload/i.test(id) ) {
|
|
2826
|
-
result.message = 'XMLHttpRequest Exception: trying to render an unknwon file.'
|
|
2827
2869
|
onUpload(gina, $target, 'error', id, result);
|
|
2828
2870
|
}
|
|
2871
|
+
|
|
2829
2872
|
triggerEvent(gina, $target, 'error.' + id, result);
|
|
2830
2873
|
|
|
2831
2874
|
if (hFormIsRequired)
|
|
2832
2875
|
triggerEvent(gina, $target, 'error.' + id + '.hform', result);
|
|
2833
2876
|
|
|
2834
2877
|
return;
|
|
2835
|
-
}
|
|
2878
|
+
}
|
|
2836
2879
|
}
|
|
2837
2880
|
|
|
2838
2881
|
};
|
|
@@ -283,6 +283,25 @@ function DataHelper(){
|
|
|
283
283
|
|
|
284
284
|
var parseLocalObj = function(obj, key, k, value) {
|
|
285
285
|
|
|
286
|
+
// #B446 — a bracket-notation field name is client-supplied and every segment is
|
|
287
|
+
// used as an assignment target below (`obj[ key[k] ] = ...`). `__proto__` is an
|
|
288
|
+
// accessor on Object.prototype, so assigning through it mutates the prototype for
|
|
289
|
+
// the whole process; `constructor`/`prototype` reach it in two hops. Measured
|
|
290
|
+
// before the fix: `__proto__[polluted]=x`, `constructor[prototype][polluted]=x`
|
|
291
|
+
// and the percent-encoded `%5F%5Fproto%5F%5F[polluted]=x` each polluted
|
|
292
|
+
// Object.prototype while the parsed body still read `{}` — i.e. silently.
|
|
293
|
+
// Drop the path rather than throwing: these names are never legitimate, and a
|
|
294
|
+
// throw on the request-parse path would turn one bad field into a 500.
|
|
295
|
+
// Kept INLINE (not a closure helper) because test/core/validator-send-formdata-
|
|
296
|
+
// nesting.test.js extracts this function's source text and evals it standalone to
|
|
297
|
+
// pin client/server parity — a helper call would be a ReferenceError there, and
|
|
298
|
+
// the client-side twin in the validator plugin is inline for the same reason.
|
|
299
|
+
for (var _s = 0, _sLen = key.length; _s < _sLen; ++_s) {
|
|
300
|
+
if ( key[_s] === '__proto__' || key[_s] === 'constructor' || key[_s] === 'prototype' ) {
|
|
301
|
+
return obj;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
286
305
|
for (let i=0,len=key.length; i<len; i++) {
|
|
287
306
|
// by default
|
|
288
307
|
let _key = key[k];
|
|
@@ -30,6 +30,31 @@
|
|
|
30
30
|
*/
|
|
31
31
|
function Merge() {
|
|
32
32
|
|
|
33
|
+
/**
|
|
34
|
+
* #B446 — keys that must never be written through by a merge. `__proto__` is an
|
|
35
|
+
* accessor on Object.prototype, so assigning it walks through to the prototype;
|
|
36
|
+
* `constructor`/`prototype` reach it in two hops. JSON.parse produces an OWN
|
|
37
|
+
* `__proto__` key, so an own-property check alone does NOT stop this — the key
|
|
38
|
+
* name has to be rejected as well. Request bodies reach merge directly
|
|
39
|
+
* (core/server.js:6657 merges JSON.parse output as the SOURCE), so this is a
|
|
40
|
+
* request-path guard, not a defensive nicety.
|
|
41
|
+
*
|
|
42
|
+
* Key rejection is the WHOLE guard, deliberately. The first cut of #B446 also
|
|
43
|
+
* skipped inherited enumerable source properties, which broke every entity in
|
|
44
|
+
* the model registry: lib/model.js setModel round-trips entity instances
|
|
45
|
+
* through merge, and all six SQL connectors attach their .sql-derived query
|
|
46
|
+
* methods to the PROTOTYPE, so an own-only copy silently dropped them
|
|
47
|
+
* (`db.xEntity.getOneById is not a function`). merge therefore copies the
|
|
48
|
+
* chain, as it always has; only these three key names are refused.
|
|
49
|
+
*
|
|
50
|
+
* @param {string} k - candidate key
|
|
51
|
+
* @returns {boolean} true when the key must be skipped
|
|
52
|
+
* @private
|
|
53
|
+
*/
|
|
54
|
+
var isUnsafeMergeKey = function(k) {
|
|
55
|
+
return k === '__proto__' || k === 'constructor' || k === 'prototype';
|
|
56
|
+
};
|
|
57
|
+
|
|
33
58
|
var newTarget = []
|
|
34
59
|
, originalValueshasBeenCached = false
|
|
35
60
|
//, keyComparison = 'id' // use for collections merging [{ id: 'val1' }, { id: 'val2' }, {id: 'val3' }, ...]
|
|
@@ -134,6 +159,9 @@ function Merge() {
|
|
|
134
159
|
} else {
|
|
135
160
|
// Merge the base object
|
|
136
161
|
for (var name in options) {
|
|
162
|
+
// #B446 — reject prototype-reaching key names.
|
|
163
|
+
if ( isUnsafeMergeKey(name) ) { continue }
|
|
164
|
+
|
|
137
165
|
if (!target) {
|
|
138
166
|
target = { name: null }
|
|
139
167
|
}
|
|
@@ -195,6 +223,9 @@ function Merge() {
|
|
|
195
223
|
clone = {};
|
|
196
224
|
// copy props
|
|
197
225
|
for (var prop in copy) {
|
|
226
|
+
// #B446
|
|
227
|
+
if ( isUnsafeMergeKey(prop) ) { continue }
|
|
228
|
+
|
|
198
229
|
clone[prop] = copy[prop]
|
|
199
230
|
}
|
|
200
231
|
}
|
|
@@ -206,6 +237,9 @@ function Merge() {
|
|
|
206
237
|
if ( !override ) {
|
|
207
238
|
// add those in copy not in clone (target)
|
|
208
239
|
for (var prop in copy) {
|
|
240
|
+
// #B446
|
|
241
|
+
if ( isUnsafeMergeKey(prop) ) { continue }
|
|
242
|
+
|
|
209
243
|
if (typeof(clone[ prop ]) == 'undefined') {
|
|
210
244
|
if ( Array.isArray(copy[ prop ]) && Array.isArray(clone[ prop ]) ) {
|
|
211
245
|
clone[ prop ] = mergeArray(copy[ prop ], clone[ prop ], override);
|
|
@@ -235,6 +269,9 @@ function Merge() {
|
|
|
235
269
|
} else {
|
|
236
270
|
|
|
237
271
|
for (var prop in copy) {
|
|
272
|
+
// #B446
|
|
273
|
+
if ( isUnsafeMergeKey(prop) ) { continue }
|
|
274
|
+
|
|
238
275
|
if ( typeof(copy[ prop ]) != 'undefined' ) {
|
|
239
276
|
//clone[prop] = copy[prop]
|
|
240
277
|
if ( Array.isArray(copy[ prop ]) && Array.isArray(clone[ prop ]) ) {
|
|
@@ -545,6 +582,9 @@ function Merge() {
|
|
|
545
582
|
// chain below already does for an object at an occupied index.
|
|
546
583
|
if (newTarget[a] !== null && typeof(newTarget[a]) == 'object') {
|
|
547
584
|
for (let k in options[a]) {
|
|
585
|
+
// #B446
|
|
586
|
+
if ( isUnsafeMergeKey(k) ) { continue }
|
|
587
|
+
|
|
548
588
|
if (!newTarget[a].hasOwnProperty(k)) {
|
|
549
589
|
newTarget[a][k] = options[a][k]
|
|
550
590
|
}
|
package/gna.js
CHANGED
|
@@ -15,14 +15,14 @@
|
|
|
15
15
|
'use strict';
|
|
16
16
|
|
|
17
17
|
// Framework core — the main gna module (lifecycle hooks, lib, etc.)
|
|
18
|
-
var _gna = require('./framework/v0.6.
|
|
18
|
+
var _gna = require('./framework/v0.6.22/core/gna');
|
|
19
19
|
|
|
20
20
|
// SuperController and EntitySuper — loaded from their source modules
|
|
21
|
-
var SuperController = require('./framework/v0.6.
|
|
22
|
-
var EntitySuper = require('./framework/v0.6.
|
|
21
|
+
var SuperController = require('./framework/v0.6.22/core/controller');
|
|
22
|
+
var EntitySuper = require('./framework/v0.6.22/core/model/entity');
|
|
23
23
|
|
|
24
24
|
// uuid — from the lib registry
|
|
25
|
-
var uuid = require('./framework/v0.6.
|
|
25
|
+
var uuid = require('./framework/v0.6.22/lib/uuid');
|
|
26
26
|
|
|
27
27
|
module.exports = {
|
|
28
28
|
|
package/llms.txt
CHANGED
|
@@ -902,7 +902,7 @@ Dev-mode query instrumentation captures every database query tied to the current
|
|
|
902
902
|
|
|
903
903
|
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). **The staged on-disk FILENAME is server-generated and opaque, never the client's (#B419, 0.6.16):** `crypto.randomBytes(16).toString('hex') + '.part'`, minted once per part and used at BOTH path-construction sites — the write stream AND `req.files[].path`, which are built independently and must never diverge. Pre-fix the destination was `<fileUploadDir>/<client basename>` with NO per-part component, so two parts sharing a name — within ONE request or across concurrent requests — opened two write streams on ONE path with independent file offsets and INTERLEAVED their bytes into a single hybrid matching neither source, while the framework answered `storeErr:false` + 200 (measured live pre-fix: 12 concurrent same-named uploads → 0/12 clean, one hybrid file; one request with two same-named parts → one 500000-byte file of {B:417353,A:82647}; post-fix 12/12 clean, each byte-exact). The parse precedes routing and all middleware, so an UNAUTHENTICATED POST to ANY url reaches it — an unrouted one answers 404 to the client and still writes the file. A CSPRNG rather than the `Math.random()` used by `movefiles` (V8's is xorshift128+, so staging paths would be predictable, and a group `path` is operator-configurable into a served root); a RANDOM name rather than `filename + <suffix>` (keeps attacker-controlled bytes off the filesystem — RTL-override display spoofing, control chars, reserved device names — and cannot overflow NAME_MAX=255 into an ENAMETOOLONG 500 for a long-but-legal client filename); no interpretable extension, so a misconfigured static-serve of the staging dir cannot hand back an executable type. This is the multer ("a random name that doesn't include any file extension") and formidable (`newFilename` hexoid) convention. `req.files[].originalFilename` still carries the client's name and is what `store()` publishes the file under (`controller.js:4061`) and what a storage driver receives as `originalName`, so the documented "keeping each file's original name" contract is UNCHANGED — only `req.files[].path`'s BASENAME differs, and that field is documented as the temporary file's path. Server-side ⇒ pickup is a bundle restart, no re-bake. **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. Since #STO1 slice 1 a group may also carry driver: '<name>', routing its self.store() step through settings.storage (entry #300; the parse/staging path above is unchanged). Server-side only. Tests: `test/core/upload-groups.test.js` + `upload-config.test.js` + `multipart-nonfile-terminal.test.js` + `multipart-field-capture.test.js` + `upload-concurrent-staging.test.js` (+ the client `send(FormData)` half: `validator-send-formdata-multipart-fields.test.js`). **Server-path integrity + terminal-state hardening (former #263/#267/#268/#270 — #B103/#B142/#B143/#B144/#B145/#B223, 0.5.22-0.5.24):** a multipart body stays RAW end-to-end — `request.isMultipart` is computed ONCE at the request prologue and gates the request-stream `setEncoding` (a multipart stream reaches busboy as raw Buffers, its documented input; the decode remains for the text-body branches), write-pipeline chunks pass through VERBATIM, and `req.files[].size` counts BYTES, never a decoded string's length (#B103 — pure-ASCII surviving both old decode layers is why text uploads always worked while every real binary corrupted on every native multipart client: a PNG's 0x89 magic landed as 0xFD). The per-file `size` is finalized in the liner Transform's `_flush` (#B142 — the source-'end' record ran one pipe hop UPSTREAM of the byte counter with 16-object high-water marks still queued, under-counting ~25% on a 1.5MB file while md5 stayed perfect: the bytes were intact, only the number lied; `_flush` runs after the last `_transform` and strictly before the write stream can emit 'finish', both interleavings measured exact), and BOTH write-stream terminal listeners arm AT STREAM CREATION in the 'file' handler (#B143 — arming inside `busboy.on('finish')` lost the race for any early-finishing small file: Node never replays 'finish' for a late listener, so a throttled two-file upload hung deterministically, ~1 in 13 unthrottled; unauthenticated, both engines). Resume fires on `busboyDone && pending === 0`, whichever event lands last (the zero-pending branch still covers the fields-only #B93 terminal), and a mid-stream write error (missing dir, disk full) gets a guarded 500 instead of the historical unhandled-'error' uncaughtException SIGTERM. Downstream, `Controller.store()`'s mover streams each file to a temp sibling and publishes with an ATOMIC rename (a reader never observes a partial file; a pre-existing destination is replaced only on success), propagates the REAL filesystem Error (previously masked as `No file to upload`), and never settles the callback twice (#B223). Since #B227 the SAME semantics govern the general-purpose byte-writer in `helpers/path.js` behind `_().cp()` / `PathObject.mv()` (which CLI copy/build/rename paths ride): temp sibling in the destination's own directory + atomic rename (a pre-existing destination is no longer unlinked BEFORE the copy — its content survives a failed copy), a source-stream error listener (was an unhandled `error` event → process kill), a settled latch (a destination-side failure previously settled twice, the second time as a success, forking `browseCopy`'s directory recursion), and a real `Error` instead of the former plain string (`Error on Path.cp(...): Not found ...`) — so caller `err.stack` prints stop logging `undefined`. The destination mkdir is crash-guarded (#B145): a group whose custom `path` has a read-only/EACCES/EROFS parent answers a guarded 500 naming group + path — a server CONFIG problem is 500 (the #B50 unconfigured-group 400 stays upstream and unchanged), and pre-fix `fs.mkdirSync` threw SYNCHRONOUSLY inside the parser callback → uncaughtException → SIGTERM, an unauthenticated single-request bundle kill (the multipart parse precedes routing and all middleware, both engines). A per-group `simulateWriteError: true` flag (#B144) lets a consumer deterministically fire the guarded-500 write-error path OUTSIDE production scope only (`!NODE_SCOPE_IS_PRODUCTION`): the 'file' handler creates the REAL write stream, arms the REAL terminal listeners, then synthetically `destroy()`s it — the exact terminal semantics of a real ENOSPC/EIO (an errored stream never emits 'finish', so the request stays terminal at the 500), N destroyed parts collapsing to exactly ONE 500 via throwError's `!res.headersSent` guard; a boot warn scans `upload.groups` for the flag in both scopes (production: "IGNORED — remove before shipping"; else "PROBE active"); nothing ships active. The `group="…"` 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. **The staged upload client layer (`data-gina-form-upload-*`, lives in the validator plugin — browser-bundled ⇒ consumers RE-BAKE at pickup; former #269/#271/#272/#274 — #R8 slices 1-2, #B146/#B147/#B148/#B149):** the staging POST body is assembled as a `Blob` with each File object embedded RAW (#B148 — the historical `FileReader`/`ab2str` DOMString concatenation UTF-8-inflated every byte >= 0x80 on the wire, ×1.49 measured, a PNG's 0x89 arriving as 0xC2 0x89; the corruption only STARTED at 0.5.22 because the pre-#B103 server's two since-removed decode layers EXACTLY reversed the inflation — two wrongs cancelling — so the #B103 server fix is what exposed the client defect). Multipart FRAMING is byte-identical (same boundary delimiters, same `name=`/`group=`/`filename=` disposition-parameter set, values percent-escaped for CR/LF/double-quote per RFC 7578 §5.1.1); a fixed client must pair with a server >= 0.5.22 (an older server's decode layers would corrupt the now-raw bytes — the #B103 corruption in reverse); files corrupted by the defect are LOSSLESSLY recoverable (the stored bytes are exactly the UTF-8 encoding of the originals: decode utf8 → re-encode latin1). **Upload progress (#R8 slice 1):** `send()` assigns `xhr.upload.onprogress` FRESH on every send (the module-scoped XHR was reused across sends — a stale handler replays the previous send's closure with the wrong id), dispatching the REGISTERED event `uploadProgress.<uploadFormId>` (the events-array registration is required — `on()` validates names against the plugin registry) with `{ status: 100, progress: <int 0-100 | null when lengthComputable is false>, loaded, total, lengthComputable, files: [names] }` — a per-REQUEST aggregate (ONE staging XHR carries every file of a selection; per-file wire progress is not separable). Consumer surfaces: `data-gina-form-upload-on-progress` (bare window identifier, the -on-success convention) + a declarative indicator `data-gina-form-upload-progress="<elId>"` defaulting to `<fieldId>-progress`, opt-in by element presence. The updater feature-detects the target: a native `<progress>` tracks value=loaded/max=total (indeterminate = the value attribute REMOVED for the native animation; error = value 0, NEVER indeterminate — that animation would read as still working); any other element gets percent textContent + `data-gina-upload-progress`/`data-gina-upload-progress-state` styling hooks (preparing|uploading|indeterminate|processing|complete|error) with NO hardcoded wording (i18n-neutral — label via CSS on the state attribute). Lifecycle: `preparing` at selection (covers the FileReader/assembly phase), `uploading` per frame, `processing` at `xhr.upload.onloadend` (the browser finished SENDING — advances the state attribute ONLY, leaving value/percent as the last frame left them so a determinate bar stays visually full), `complete`/`error` finalized in onUpload (one chokepoint covers success and every error/timeout path), reset/delete strips the indicator. The response-side download channel `progress.<id>` is byte-untouched (it also serves attachment downloads). **Drag-and-drop (#R8 slice 2):** a file input carrying `data-gina-form-upload-dropzone="<elementId>"` gets drag listeners bound on the named element at form-bind time; dropped files are assigned to the input (`input.files = dataTransfer.files`) and the input's `change` is re-fired synthetically, so the ENTIRE staging pipeline — group tagging, virtual form, staging POST, previews, hidden metadata fields, reset/delete, upload progress — runs with zero duplicated logic (the change handler reads only `currentTarget`, so a synthetic dispatch is indistinguishable from a trusted one on this path). Contract: EXPLICIT-id-only (deliberately NO `<fieldId>-dropzone` default — auto-binding a coincidentally-named element would attach drag semantics to markup that may carry its own drop handling; absent attribute = inert, missing element = console.warn + inert); the zone is stamped `data-gina-upload-dropzone` (value = owner input id — the first-wins guard: one zone serves one input) and `data-gina-upload-dropzone-state` (`idle` → `over` on a file-drag hover, DEPTH-COUNTED so child-boundary crossings never flicker → `dropped` → back to `idle` at the same onUpload chokepoint that finalizes progress, and at reset/delete) — pure CSS hooks, no hardcoded wording. Only FILE drags react (`dataTransfer.types` must carry `Files`; text/link drags fall through untouched, never preventDefault'd); a multi-file drop on a non-`multiple` input keeps the FIRST file only (console.warn) — configured groups still enforce `isMultipleAllowed` server-side; a bare file input already accepts native browser drops through the same change handler — the attribute exists to delegate a larger/styled element. **Action/preview/bind fixes (#B146/#B147/#B149):** `checkUploadUrlActions`'s default-route fallback writes the attribute ACTUALLY being checked (`setAttribute(action, …)` — it used to hardcode the staging attribute and silently repoint a staging POST at the resolved reset/delete default route, compounding to a SILENT failure when `toUrl()`'s absolute origin tripped a CORS preflight → XHR status-0 → the commented-out status-0 branch); the preview-container guard uses the typeof-null pattern (`&& previewContainer` — `getElementById` returns element|null and `typeof null === 'object'`, so a preview-element MISS used to TypeError the success handler at `.id`); an upload-only input binds QUIETLY — `-delete-action` deliberately has no framework default (it removes an already-saved file, an app-specific endpoint), so a no-default absent action is a single `console.debug` + early return (a WITH-default action that genuinely fails to resolve still errors; the delete requirement stays enforced lazily at `onUploadResetOrDelete`). Tests: `upload-binary-integrity.test.js` / `upload-size-accuracy.test.js` (11) / `multipart-multifile-resume.test.js` (15) / `upload-write-error-probe.test.js` / `upload-config.test.js §07` / `validator-upload-progress.test.js` (46) / `validator-upload-binary-wire.test.js` / `validator-upload-action-guard.test.js` — all red-first-validated with control-gated extractions of the shipped bytes. **The store() fluent form is restored (#B420, 0.6.16):** `self.store(target).onComplete(cb)` returns its `{onComplete}` handle synchronously as documented -- an accidental `async` on the declaration had wrapped the handle in a Promise on every stable from v0.6.0 to v0.6.15, so the fluent form (the upload guide entry-point example) threw `TypeError` while the 3-arg callback form worked throughout; the body was await-free, nothing anywhere awaits `store()`, and dropping the keyword is the whole fix (types now declare the fluent return shape).
|
|
904
904
|
|
|
905
|
-
211. **Request-body parsing contract — verbatim JSON, `req.rawBody`, and crash-safe decodes (consolidates former #162/#163/#164; #B28/#B30/#B64).** `application/json` bodies (POST/PUT/PATCH) parse VERBATIM first — `JSON.parse(request.body)`, with a `decodeURIComponent` fallback ONLY when that throws — so the client's exact types and string contents survive (no double-decode, no `"true"/"false"/"on"/"null"` coercion, no bracket-key expansion; urlencoded/absent content-types keep the legacy decode+coerce path; error disposition unchanged — POST/PATCH 500 only when both attempts fail, PUT warns). The framework's own server-to-server client (`self.query()`) sends raw `JSON.stringify(data)` for JSON bodies (RFC5987 value-encoding is for header values, not request bodies), and the inter-bundle HTTP/2 proxy no longer copies the INCOMING request's Content-Type onto the outbound JSON body it serialized itself — a urlencoded label re-routed raw JSON through the receiving parser's urlencoded branch and corrupted string values (the only incoming-CT→outbound copy framework-wide; kept for non-JSON bodies). `request.rawBody` snapshots the exact unparsed body (reference assignment, always-on, non-multipart only, `''` for empty) immediately BEFORE parsing mutates `request.body` — inbound-webhook HMAC verification needs the raw bytes, which middleware cannot recover after the stream drains; both engines share this single `core/server.js` pipeline. Malformed percent-escapes (`%`, `%zz`, truncated `%E0%A`) in a URL or query no longer kill the bundle (#B30 — an unauthenticated single-request DoS: any unguarded `decodeURIComponent` OR `decodeURI` on the request path threw `URIError` → uncaughtException → SIGTERM): redundant SECOND decodes on the GET/HEAD branches were DROPPED (the engine query parser + the data helper each already decode once, guarded), and every genuine first decode of attacker-controllable input routes through `safeDecodeURIComponent`/`safeDecodeURI` (try/decode/fallback-to-raw) — including the ERROR paths, where a malformed-% URL to a missing asset previously crashed FROM the error handler, turning a would-be 404 into a bundle kill. Sweep rule: `decodeURIComponent` and `decodeURI` are the same throwing family — sweep BOTH; fix the throw sites, never widen the uncaughtException net. Sibling #B64 (0.5.7): the two static-file resolvers confine the resolved filename to the matched mapping target (`path.resolve` both sides + separator-aware containment), so `../` / `%2F` / `%2e%2e` cannot escape to sibling files — an escape 404s exactly like a missing file. Second site, same guard (#B179, Security/CWE-22): the dev-mode Inspector SPA handler (`/_gina/inspector/*`) built its filename the same way — request target minus the prefix, joined onto the asset root, passed through the `_()` path helper — and `_()` calls `Path.normalize`, which **RESOLVES** `..` rather than rejecting it, so a literal `../` read any absolute path the bundle process could reach (verified live: 200 on `/etc/passwd`; encoded `%2e%2e` unaffected, since this handler never decodes — and decoding was deliberately not added). Node does not normalise the request-target (only clients do), so a raw HTTP client reaches it where a browser cannot. Unlike the static resolvers, this handler is **DUPLICATED per engine** (server.js + server.isaac.js) and `confineToBase` is scoped inside server.js's `Server()` closure — unreachable from isaac, which therefore declares an engine-local twin, pinned against drift by a test asserting both extracted copies agree. **Rule: any handler joining request-derived segments onto a base MUST confine the RESOLVED path; path normalisation is not a traversal defence, it is the mechanism that makes the escape land.** Corollary for endpoint families: check the SIBLING handlers' gating too — the Inspector endpoint carries no IP allowlist while its `/_gina/*` neighbours (`info`, `cache/stats`, `cache/clear`) all gate on `lib.admin.isClientAllowed`. **GET/HEAD query serialization no longer destroys escapes (#B407, consumer-reported):** both branches round-trip `request.query` through JSON.stringify + a re-parse, and the former textual nested-JSON unwrap ended with a blanket backslash strip over the WHOLE serialized document — so `%0A` in a value became the letter `n`, a backslash vanished, and a double quote OR a JSON-ARRAY value produced invalid JSON that silently dropped EVERY query param on the request (`parseBody` logs `[365]` and returns undefined). Both sites now serialize via `serializeQueryForReparse` (a shared inner helper in `processRequestData`): each own STRING value leaning `{`/`[` is unwrapped by PARSING it (kept verbatim when it does not parse — previously that shape also dropped the whole query), then a clean stringify with no post-processing; the whole-value `"true"/"false"/"on"/"null"` coercion is unchanged and, with escapes intact, can no longer match an escaped occurrence embedded inside a longer value. Intended behaviours preserved byte-for-byte (nested-object unwrap — now also correct for nested content carrying escapes, which the textual unwrap corrupted as well — coercion, plain values); array-valued and unparsable-`{`-leading params are delivered instead of killing the query.
|
|
905
|
+
211. **Request-body parsing contract — verbatim JSON, `req.rawBody`, and crash-safe decodes (consolidates former #162/#163/#164; #B28/#B30/#B64).** `application/json` bodies (POST/PUT/PATCH) parse VERBATIM first — `JSON.parse(request.body)`, with a `decodeURIComponent` fallback ONLY when that throws — so the client's exact types and string contents survive (no double-decode, no `"true"/"false"/"on"/"null"` coercion, no bracket-key expansion; urlencoded/absent content-types keep the legacy decode+coerce path; error disposition unchanged — POST/PATCH 500 only when both attempts fail, PUT warns). The framework's own server-to-server client (`self.query()`) sends raw `JSON.stringify(data)` for JSON bodies (RFC5987 value-encoding is for header values, not request bodies), and the inter-bundle HTTP/2 proxy no longer copies the INCOMING request's Content-Type onto the outbound JSON body it serialized itself — a urlencoded label re-routed raw JSON through the receiving parser's urlencoded branch and corrupted string values (the only incoming-CT→outbound copy framework-wide; kept for non-JSON bodies). `request.rawBody` snapshots the exact unparsed body (reference assignment, always-on, non-multipart only, `''` for empty) immediately BEFORE parsing mutates `request.body` — inbound-webhook HMAC verification needs the raw bytes, which middleware cannot recover after the stream drains; both engines share this single `core/server.js` pipeline. Malformed percent-escapes (`%`, `%zz`, truncated `%E0%A`) in a URL or query no longer kill the bundle (#B30 — an unauthenticated single-request DoS: any unguarded `decodeURIComponent` OR `decodeURI` on the request path threw `URIError` → uncaughtException → SIGTERM): redundant SECOND decodes on the GET/HEAD branches were DROPPED (the engine query parser + the data helper each already decode once, guarded), and every genuine first decode of attacker-controllable input routes through `safeDecodeURIComponent`/`safeDecodeURI` (try/decode/fallback-to-raw) — including the ERROR paths, where a malformed-% URL to a missing asset previously crashed FROM the error handler, turning a would-be 404 into a bundle kill. Sweep rule: `decodeURIComponent` and `decodeURI` are the same throwing family — sweep BOTH; fix the throw sites, never widen the uncaughtException net. Sibling #B64 (0.5.7): the two static-file resolvers confine the resolved filename to the matched mapping target (`path.resolve` both sides + separator-aware containment), so `../` / `%2F` / `%2e%2e` cannot escape to sibling files — an escape 404s exactly like a missing file. Second site, same guard (#B179, Security/CWE-22): the dev-mode Inspector SPA handler (`/_gina/inspector/*`) built its filename the same way — request target minus the prefix, joined onto the asset root, passed through the `_()` path helper — and `_()` calls `Path.normalize`, which **RESOLVES** `..` rather than rejecting it, so a literal `../` read any absolute path the bundle process could reach (verified live: 200 on `/etc/passwd`; encoded `%2e%2e` unaffected, since this handler never decodes — and decoding was deliberately not added). Node does not normalise the request-target (only clients do), so a raw HTTP client reaches it where a browser cannot. Unlike the static resolvers, this handler is **DUPLICATED per engine** (server.js + server.isaac.js) and `confineToBase` is scoped inside server.js's `Server()` closure — unreachable from isaac, which therefore declares an engine-local twin, pinned against drift by a test asserting both extracted copies agree. **Rule: any handler joining request-derived segments onto a base MUST confine the RESOLVED path; path normalisation is not a traversal defence, it is the mechanism that makes the escape land.** Corollary for endpoint families: check the SIBLING handlers' gating too — the Inspector endpoint carries no IP allowlist while its `/_gina/*` neighbours (`info`, `cache/stats`, `cache/clear`) all gate on `lib.admin.isClientAllowed`. **GET/HEAD query serialization no longer destroys escapes (#B407, consumer-reported):** both branches round-trip `request.query` through JSON.stringify + a re-parse, and the former textual nested-JSON unwrap ended with a blanket backslash strip over the WHOLE serialized document — so `%0A` in a value became the letter `n`, a backslash vanished, and a double quote OR a JSON-ARRAY value produced invalid JSON that silently dropped EVERY query param on the request (`parseBody` logs `[365]` and returns undefined). Both sites now serialize via `serializeQueryForReparse` (a shared inner helper in `processRequestData`): each own STRING value leaning `{`/`[` is unwrapped by PARSING it (kept verbatim when it does not parse — previously that shape also dropped the whole query), then a clean stringify with no post-processing; the whole-value `"true"/"false"/"on"/"null"` coercion is unchanged and, with escapes intact, can no longer match an escaped occurrence embedded inside a longer value. Intended behaviours preserved byte-for-byte (nested-object unwrap — now also correct for nested content carrying escapes, which the textual unwrap corrupted as well — coercion, plain values); array-valued and unparsable-`{`-leading params are delivered instead of killing the query. **PROTOTYPE-POLLUTION GUARD (#B446).** Every parse path that nests a bracket-notation key routes through `parseLocalObj` in `helpers/data` (aliased `nestBracketNotationKey`, and reached from `formatDataFromString`), which assigned `obj[key[k]]` from a CLIENT-SUPPLIED field name with no key filtering: `__proto__[x]=y`, `constructor[prototype][x]=y` and the percent-encoded `%5F%5Fproto%5F%5F[x]=y` each wrote to `Object.prototype` process-wide while the parsed body still read `{}` — i.e. SILENTLY. Reachable from the GET query string, urlencoded bodies, `inheritedData`, and multipart text-field names. ✅ **CONFIRMED LIVE over real HTTP on the DEFAULT `isaac` engine** (daemonless throwaway bundle, pre-fix worktree vs fixed tree, same fixture): a single unauthenticated `GET /?__proto__[polluted]=OWNED` — no body, no interaction — returned `polluted:"OWNED"` with `hasOwn:false` (the prototype-pollution signature, not an own property) and `arrayToo:"OWNED"`, and **the NEXT, innocent request inherited it at the same pid**, proving cross-request persistence. All three vectors reproduced; the fixed tree answered clean on all three with benign bracket nesting still HTTP 200. ⚠️ Isaac does NOT reference these functions at all — its own query parser is flat (`request.query[a[0]] = a[1]`); the attack survives because `server.js` re-serialises isaac's flat query (`serializeQueryForReparse`) and re-parses it through `formatDataFromString`, which restores the bracket branch. ⚠️ Instrument trap that nearly produced a FALSE NEGATIVE: `curl` treats `[...]` as a URL glob range, so every bracket request fails inside curl and never reaches the server — use `curl -g`. A second, independent source: PUT merges `JSON.parse` output as the merge SOURCE, and `JSON.parse` produces an OWN `__proto__` key, so an own-property check alone does NOT stop it — see #321. Measured gadgets before the fix: outbound-request options fully controllable (host/hostname/port/method/auth, `rejectUnauthorized` forced false, `protocol` feeding a dynamic transport require), `SAFE_HTTP_METHODS[m] === true` classifying POST as safe, and `req.routing.csrfExempt` reading true — the last two ONLY via the JSON source, since the string source yields `"true"` and is correctly rejected by the strict compare. ⚠️ The `csrfExempt` one is UNRESOLVED, not confirmed on a live route: what was measured is that a routing object LACKING an own `csrfExempt` reads the polluted value, but `core/server.js:7674` builds routes as `csrfExempt: routing[name].csrfExempt || false`, creating an OWN key that shadows the prototype — while `lib/routing/src/main.js:220` sets it only when already defined, so a route built there can lack it. Which construction path a live `req.routing` takes was NOT determined. Both sources now reject `__proto__` / `constructor` / `prototype` path segments and DROP the field rather than throwing (a throw on the parse path would turn one bad field into a 500). The guard is kept INLINE in `parseLocalObj`, not a closure helper, because `test/core/validator-send-formdata-nesting.test.js` extracts that function's source text and evals it standalone to pin client/server parity — a helper call is a ReferenceError there. `helpers/data` and the validator copy are both browser-bundled, so pickup is a bundle restart AND a re-bake. Regression suite: `test/lib/prototype-pollution-b446.test.js` (attack arms each paired with a benign control, pre/post A/B validated on extracted HEAD source).
|
|
906
906
|
|
|
907
907
|
212. **Structured (JSON) logging — `GINA_LOG_FORMAT=json` + per-request `requestId`/`durationMs` (consolidates former #152/#155; #M12a/#M12b).** The logger resolves its render format ONCE at init into `opt.format` (precedence `GINA_LOG_FORMAT=json|text` > `GINA_LOG_STDOUT` truthy ⇒ json > `text` default) BEFORE containers are cloned; a JSON line is `{ts, level, bundle, message, group, msg}` — `bundle`/`message` canonical, `group`/`msg` retained as additive back-compat aliases — and the raw `console.log` path honours the format too (otherwise JSON mode would interleave plain lines and break a collector); the `text` default keeps container logs byte-identical. Per-request `requestId` + `durationMs` ride JSON logs via a NARROW `AsyncLocalStorage` (`process.gina._reqALS`, parked on `process.gina` so it survives dev require-cache busting), gated on JSON logging ONLY (text renders no id field, so the ALS would be pure overhead — the text path stays byte-identical/zero-cost): the id resolver honours a SANITISED inbound `X-Request-Id` (`/^[\w.\-]{1,128}$/`, regenerate-on-violation kills log forging) else `crypto.randomUUID()`; the `.run({requestId, startMs})` wrap sits at `handle()` — NOT the request-entry handler — because the `request.on('end')` boundary between them loses async context while `handle()`'s awaits preserve it (the original body became `_handleDispatch`; `handle` is a thin wrapper); HTTP/2 gets per-stream scoping free via Node's per-stream compat `'request'` event, NOT `session.on('stream')`; the JSON-assembly sites read `getStore()` and add `requestId` + per-line `durationMs`, gracefully absent for CLI/boot/off-request logs. `.run()`, never `enterWith()` (enterWith bleeds sideways across siblings). **MS1 (2026-07-24, 0.5.25):** the same id is now PROPAGATED beyond the process — every `self.query()` outbound path (the file-download proxy + both the HTTP/1 and HTTP/2 inter-bundle clients) forwards it as `x-request-id` (sourced from the resolved `req._ginaReqId`, never a raw inbound header; a caller-set value always wins), and `server.js onRequest` ECHOES `X-Request-Id` back on every response — ungated / independent of `GINA_LOG_FORMAT` (the id is always-on even when the JSON-log field isn't) and guarded against an already-sent response — so one logical request stays correlatable as it fans out across bundles and a caller/LB/APM reads the id off the wire. `controller.js`/`server.js` are server-side → no dist rebuild; restart to apply. Tests: `test/core/request-id-propagation.test.js`. **The contextless `require('gina')` boundary + MQ-speaker transport resilience (moved here from the DTO entry that surfaced it; #B276/#B277 0.6.4, #B318 0.6.5, #B323 fixed post-0.6.5 — shipped 0.6.6 with the reconnect).** Importing gina outside a spawned bundle child has ALWAYS been an intended boundary (`index.mjs`'s own docblock states it; both published entries throw alike), but two defects sat on top of it. **(1) The throw was uncatchable and could HANG the process.** The logger is a load-time singleton whose default flows are `['default','mq']`, so `speaker.js` opens a TCP socket BEFORE the boot reaches its throw — and that socket kept the event loop alive (listener-CONTINGENT, measured both ways: nothing bound to the MQ port ⇒ ECONNREFUSED ⇒ exit 0, so the bug is invisible on a quiet machine and hangs with no output on one running bundles or in CI). Fixed by `client.unref()`: a logging transport must never be why a process stays alive. ⚠️ #B318: a THIRD state exists beyond connected/REFUSED — an UNREACHABLE host, where `unref()` covers the socket HANDLE but not the pending `TCPConnectWrap` REQUEST, which holds the loop by itself for the OS connect timeout (~75s on macOS); fixed with an unref'd connect deadline (2s default, `opt.mqConnectTimeout`) that `destroy(err)`s only while `client.connecting`. ⚠️ **That safety claim — "only while connecting" — is MEASURED FALSE and shipped a HIGH regression for one release (#B323):** `client.connecting` does NOT flip when the kernel completes the connect; it flips when the POLL phase runs `afterConnect` — and a timer fires in the TIMERS phase, which precedes poll. On any boot that blocks the event loop past the deadline (a bundle mount off a network filesystem — every Kubernetes-class start) the loop resumes, runs the OVERDUE deadline first, reads `connecting === true` on a connection the kernel established seconds ago, and destroys it; the bundle then logged NOWHERE for the rest of its life (the speaker dialled exactly once, ever). Measured both directions: a kernel-completed dial reads `connecting` TRUE at the timers phase and FALSE at the check phase; a black-holed one reads TRUE at both. The fix defers the verdict one phase — `setImmediate` INSIDE the deadline, so the read happens in the CHECK phase, after poll — a completed connect survives while a genuinely pending one still dies on schedule (#B318's no-hang contract intact). **Generalisable: a state flag written by an event-loop callback cannot be read from an EARLIER phase and treated as current** — `connecting`, `destroyed`, `readyState` and every `pending*` counter share the property, and a blocked loop is precisely what makes the stale read reachable; a timers-phase guard over I/O state wants a `setImmediate` (or the event itself), never a direct read. **The speaker also RECONNECTS (0.6.6):** `startMQSpeaker` returns a stable `{write}` transport FACADE (`mq/index.js` captures the return ONCE for the life of the process — without the indirection a replaced socket is unreachable); each dial builds its OWN `clientOptions` (the listener mints a fresh `sessionId` per connection and the handshake only fires while `clientOptions.sessionId` is unset — a shared object would make every reconnect skip its acknowledgement); `close` on the CURRENT socket (a `current !== client` guard keeps a superseded one from stacking a second connection) arms a capped unref'd backoff (`min(500 * 2^n, 30000)`, reset on connect). Unref'd is load-bearing BOTH ways: a short-lived CLI still exits while a long-lived bundle keeps retrying and HEALS if a listener appears later. The caller callback settles ONCE, the warn fires once per OUTAGE (not per retry), and frames are DROPPED while down — an unbounded queue behind an outage of unknown length is a memory leak, and the `default`/stdout flow carries the same lines regardless. ⚠️ The socket still CONNECTS — `unref` changes only whether it votes on process lifetime (consumer-verified against a live listener: the connected line still logs; "unref'd" is not "suppressed"). ⚠️ The workaround `GINA_LOG_STDOUT=true` does splice `mq` out of the flows but ALSO flips log format to JSON (measured) — a container logging-mode flag pressed into service as a don't-open-a-socket switch. **(2) The boundary's error now names the boundary** — the old message named an internal call (`setPath("gina.home", path): path cannot be empty`) because `getEnvVar` reads `process.gina` ONLY and is empty outside the CLI; it now routes the reader to `SuperController.createTestInstance()`, the supported way to exercise controller code without booting. ⛔ **Do NOT "fix" the boundary by extending the `getEnvVar → process.env` ladder to `gna.js`, nor by skipping the empty `setPath`** — REFUTED BY MEASUREMENT: satisfying that call merely defers the failure 8 lines to a bare-global `ReferenceError` (strictly less legible), and that `setPath` is the SOLE registration site for `gina.home` repo-wide (the SQLite connector and session store read it). Failing fast at the first detectable point is correct. Diagnostic instrument for the hang class: `process._getActiveRequests()` reports `TCPConnectWrap` while `_getActiveHandles()` shows only the unref'd Socket. Tests: `test/lib/logger-mq-speaker-resilience.test.js` (§01 deferral-ordering + reconnect-shape pins; §02 a child that blocks 3s right after the dial and must still deliver — red-first: pre-fix it lost the frame while the connection was ESTABLISHED, the defect's own signature; §03 a listener destroyed and rebound must be heard again; §04 the no-listener child still exits promptly) + `logger-mq-speaker-unref.test.js` (comment-stripped source pin — subtracting the fix left a bare existence pin GREEN against the commented-out line — + a behavioural arm with its own must-be-killed control). **Log redaction (#B433, 0.6.19):** every message is redacted BEFORE it is rendered and dispatched — inside the logger's `emit()`, the single pre-render point all 46 URL-bearing framework sites pass through (both engines, every render delegate, the CSRF filter; all levelled, none `console.log`), plus the raw `console.log` path — so stdout, the MQ speaker/`gina tail`, the file transport and the Inspector taps all receive the same masked line and a JSON line cannot be corrupted (the marker lands inside the message string). Configured per bundle in `settings.json > log.redact` `{enabled, defaults, secrets, patterns}`, ON by default: `defaults` = JWT · URL userinfo password · `Bearer`/`Basic` · named credential query keys (`token`, `access_token`, `api_key`, `secret`, `password`, `signature`, `otp`, …, key kept / value masked) · api-key headers — measured 0 false positives on 433k real log lines, ~1 µs per line, linear patterns; `secrets` = every value the secrets resolver substituted for a `${secret:KEY}` placeholder (settings, connectors, routing — one resolve point) masked verbatim, values < 8 chars skipped with a boot warn; `patterns` = consumer rules (regex source string → `[REDACTED]`, or `{pattern, flags, replacement, name}` with `$1` kept), `g` always enforced. Deliberately NOT a default: a bare long-hex path segment (a sha256 content-address key from storage and an opaque 64-hex credential are the same regex class — the consumer adds `(?<![0-9a-f])[0-9a-f]{64}(?![0-9a-f])` — anchored on the class, not `\b`, which never fires against a prefixed `key_<hex>` segment). Fail-closed: an unknown key, a non-boolean flag, an invalid or empty-matching pattern refuses the boot (a dropped rule is a leak). Installed by `core/config.js::loadBundleConfig` right after the bundle's `secrets.resolve()` — the earliest resolved point, so connector-init lines are already covered — via `console.setRedaction(block, {group, secrets: lib.secrets.getResolvedValues(conf)})`; state lives on the logger's persisted context (survives `refreshCore()`), keyed by bundle, UNION across bundles (one bundle enabling is enough in a merged process; a per-route `logUrl:false` flag was rejected by measurement — it cannot reach the second URL copy inside the #ERRREF error stack). The 404 ref line carries the URL twice in ONE record (prefix + the error's own message); both copies are masked. Restart to apply, no re-bake (server-side only). A per-route or per-site flag is NOT the extension point; the logger seam is. Error rendering (#B434, 0.6.19): an `Error` passed as a log argument renders via `util.inspect` — message, stack, own props, `[cause]` chain — at BOTH stdout writers (the levelled `write()`/`parse()` walk and the raw `console.log` `JSON.stringify` path each saw only ENUMERABLE own props, so a bare Error rendered `{}`), top-level and nested alike, realm-safe (`util.types.isNativeError` beside `instanceof`), and the rendered message still crosses the redaction seam (content is built in `write()`, redacted in `emit()`). Non-Error arguments render byte-identically to before.
|
|
908
908
|
|
|
@@ -1011,7 +1011,7 @@ Dev-mode query instrumentation captures every database query tied to the current
|
|
|
1011
1011
|
298. **Render/request-path deep-clone & id-mint reduction (#P39 slice 1, 0.6.2) — 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.
|
|
1012
1012
|
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).
|
|
1013
1013
|
300. **Pluggable object storage (`lib/storage`, #STO1)** — an **adapter** (where bytes live) crossed with a **strategy** (how keys are laid out), behind one callback-shaped contract: `put(stream, meta, cb)` -> `{key, size, contentType}` / `get(key, cb)` -> stream / `stat(key, cb)` -> meta|null / `release(key, cb)` / `resolve(key, cb)` -> `{kind:'path'|'inline'}` / `capabilities`. Slice 0 ships the `local` adapter + `sharded` strategy (`YYYY/MM/DD/<ulid><ext>`); the cas slice adds the `cas` strategy (its own block below) and the stream slice the `stream` strategy (**its own entry #302** — large sequential media + resumable uploads); `getRange(key, start, end, cb)` is present on ALL THREE local strategies with `capabilities.ranges` true — `end` is INCLUSIVE (matching the HTTP header and `createReadStream`), an over-long `end` CLAMPS, and only a `start` at/beyond the object size errors (the caller's 416); it answers from either tier, and under `cas` a released blob stays invisible to it. The serving half now ships too — `self.serveFromStorage(driverName, key, opts)` (its own entry) answers 206/416/304 with `Accept-Ranges`/`Content-Range` on both engines, gated on `capabilities.ranges`, so the flag is finally consumed by the framework itself. The `s3` adapter SHIPPED (its own entry #304): `capabilities.offload` has its first `true` and its first framework consumer (`serveFromStorage`'s presigned-307 path) in the same release, so consumers must BRANCH on `capabilities` rather than assume — every capability flag has now flipped at least once. **Keys are OPAQUE**: store what `put()` returns, never parse or compose one, which is what lets a later strategy change the layout without breaking stored references. Config is `settings.json > storage` = `{default, drivers:{<name>:{adapter, strategy, root, maxObjectSize, store}}}` — drivers NEST under their own key (the `upload.groups` shape) so a driver may legitimately be called `default`. Reached as `gina.storage()` (the `default` driver) or `gina.storage('<name>')`; the accessor is assigned UNCONDITIONALLY so an unconfigured bundle gets a named `[storage] not configured` error instead of `gina.storage is not a function`. **Metadata rides a connector seam** — `lib/storage-store.js` is the 5th member of the `job-store`/`audit-store`/`render-cache-store`/`session-store` dispatcher family (connectors.json entry name -> `.connector` -> `core/connectors/<c>/lib/storage-store.js`, throw-on-unresolvable = boot abort, and the dispatcher passes the DRIVER NAME through so two drivers may share one connectors entry); the default is an embedded SQLite file at `<root>/.meta.db`, documented **single-process-per-driver-root** (SQLite locking on a shared network FS is the known-broken case the seam exists for). **`couchbase` is the first connector store** (`core/connectors/couchbase/lib/storage-store.js`, SDK major 3|4 from the PROJECT's node_modules) and is the supported answer for a shared root — several bundles or replicas on one driver root: one JSON doc per row keyed `<prefix><driver>:<key>`, with `d`+`k` as the sargable discriminators every N1QL query filters on. Per-key atomicity is Couchbase's own CAS — every refcount verb is a `get` -> `mutateIn(...,{cas})` loop (bounded retries, then a coded `GINA_STORAGE_CAS_CONTENTION` error), and `removeIfZero` is a CAS-guarded `remove` whose mismatch IS the resurrection signal. **That atomic claim IS the multi-process sweep coordination** — concurrent sweepers are safe because exactly one claim per blob wins, so NO election layer ships (one would also make `storage:gc` report `collected:0` on a non-holder); the seam's residual is unchanged and is put-vs-sweep, never sweeper-vs-sweeper. The inline payload rides as **base64 inside the JSON doc, never a binary document body** — a binary value cannot be parsed, indexed or queried by Couchbase, which would make `listZeroRefs`/`stats` impossible, and refcount+payload must sit under ONE CAS (cost: +33%, so an `inlineThreshold` above ~14MB overflows the 20MB doc ceiling). `acquireRef` REFUSES to adopt an existing non-refcounted row (a `sharded` row or the `.driver` stamp) instead of silently stamping a count onto it. Two secondary indexes (`gina_storage_refs (d,refs,zeroAt)` / `gina_storage_keys (d,k)`) are probed via `system:indexes` and created when missing — a refusal is non-fatal but LOGS THE EXACT DDL, because an unindexed query ERRORS rather than merely running slow and the sweep's bare call would swallow it forever. ⚠️ **A statement alias that is a N1QL RESERVED WORD is refused outright by the server, so the whole verb fails to PARSE** — `stats` shipped aliasing `AS inline` and `INLINE` is reserved (measured on BOTH the 7.6 and current reserved-word references, so this was never version-specific), fixed by backticking that one alias (#B356); the other four aliases are not reserved and stay bare. **The class is INVISIBLE to this store's own suite by construction** — the tests drive a fake SDK that pattern-matches statement SHAPES and never parses N1QL, so no amount of coverage there can see a reserved word — which is why the guard is a LEXICAL pin over the captured statements carrying its own can-fail control (it must flag the pre-fix statement and clear the backticked one). Real-cluster verification against a four-node **8.0.2** deployment (the store was designed against 7.x, so this is the first 8.0 datapoint): the `system:indexes` probe and both index shapes work, the durability map matches the SDK's members, a stale-CAS `mutateIn` raises `CasMismatchError`, 32 concurrent `acquireRef` calls across 4 processes on one key yield ONE blob at refs 32, and concurrent sweepers claim exactly once over 5/5 trials with the winner varying — confirming the no-election-layer decision. ⚠️ One operational consequence of `listZeroRefs` being a GSI query: a just-zeroed blob can be invisible to the sweep briefly, so a test that zeroes and immediately sweeps may legitimately see nothing (the grace window covers it; the next pass collects). Optional `durability` (majority|majorityAndPersistToActive|persistToMajority) rides every mutation; default is the SDK's own level, the same honesty class as the embedded store's WAL `synchronous=NORMAL`. **Security, all boot-enforced or per-call**: a driver `root` inside ANY bundle's web-served tree is a boot FATAL — and the check reads `publicPath` **plus every `content.statics` target**, because a statics mapping can point anywhere on disk, so checking `publicPath` alone leaves a hole; the metadata DB living inside the root is safe only BECAUSE of that fatal. Per-call key handling runs three guards **in a load-bearing ORDER**: confinement FIRST (a lib-local `confineToBase` copy — the engine copies are closure-private), THEN canonical-form, THEN reserved dot-segments. Reversing the first two silently shadows the security boundary: `..` starts with a dot, so a reserved-check-first ordering answers every traversal attempt with "reserved", `confineToBase` is never reached, and a traversal test passes for the wrong reason — measured and fixed during the build. Non-canonical keys (`a/../b`) are refused because they would address one file under two metadata keys; `.tmp` / `.meta.db` are refused because they are reachable without leaving the root. **Write path** = the #B223 discipline generalised: stream to `<root>/.tmp/` then publish by `rename(2)` (atomic — a reader never sees a partial; same root ⇒ same FS ⇒ no EXDEV), error listeners armed AT STREAM CREATION on BOTH streams (#B143 — `.pipe()` returns the destination, so one chained listener covers only one, and an unlistened `'error'` takes the bundle down), a settled latch (`close` also follows `error`), the REAL error propagated (never a fabricated one), size measured from the PUBLISHED file (not an in-flight counter), and a metadata-write failure rolls the object back so a caller that got no key is never left with unreferenceable bytes. Durability is BY STRATEGY since the cas slice: `sharded` writes are NOT fsynced (disclosed in the module JSDoc the way `lib/audit` discloses its own tail), while `cas` publishes fsync by DEFAULT — the temp file HARD before the rename (a failure fails the put), the parent directory BEST-EFFORT after it (platforms that cannot fsync a directory — Windows, some network mounts — skip that half silently; macOS honours fsync as the platform defines it, F_FULLFSYNC being native-addon territory gina does not ship). The first fsync anywhere in gina — dir-fd fsync measured working on darwin under both runtimes before shipping. **Size tiering (the tiering slice)**: objects strictly UNDER a driver's `inlineThreshold` (unit-suffixed string, default `"64KB"`, `"0B"` = off per driver) are stored INLINE — one metadata-store transaction carrying the payload as a binary-safe `data` Buffer on the row, no temp file, no directories, no filesystem at all — while at-or-above objects take the file path; the crossing is decided mid-stream by buffering the head (memory bound: threshold + one chunk per in-flight put) and spilling byte-order-exact into the temp+rename path the moment the running total reaches the threshold. The key shape is IDENTICAL in both tiers (keys stay opaque); `resolve()` answers `{kind:'inline'}` (bytes via `get()`, which serves the row as a stream), `stat()` strips the payload, `release()` needs no special casing, and `capabilities.inline` is true iff the threshold is active. Reads are payload-presence-driven, NOT threshold-driven, so changing the threshold is retroactively safe — existing objects stay readable on either side, and the embedded store migrates a pre-tiering database in place (additive `data BLOB` column, idempotent). 64KB is the MEASURED knee, not folklore (2026-08-13 bench on the shipped stack, sequential puts: inline 10.8-12.9x faster at 1KB, 8-8.5x at 4KB, 5.7-6.6x at 16KB, 2.7-3.3x at 64KB, destabilizing above — on APFS/NVMe, the case MOST favourable to the file path, so slower/network filesystems widen the win). Inline durability is the same class as the un-fsynced rename path (WAL + synchronous=NORMAL: a crash can lose the last committed transaction, never a torn row). Two operational facts stated rather than implied: the metadata store becomes PAYLOAD-BEARING (losing it loses sub-threshold objects, not just their metadata — file-backed bytes survive an index loss; any root-level backup still carries `<root>/.meta.db`), and sub-threshold objects are not individually visible on disk (the `sharded` SSH-browsability rationale holds only at-or-above the threshold). A connector-backed store inherits payload rows, so binary-safe round-trip is part of the SEAM contract, not implementation detail. Direct factory callers get tiering OFF when the key is absent — defaults resolve in `start()`, the `maxObjectSize` pattern, which is what keeps the adapter's low-level tests pinned to the file path. **The `cas` strategy (the cas slice): the key IS the content address** — `blobs/<algo>/<aa>/<bb>/<hex>`, extension-less BY CONSTRUCTION (an extension would derive from the untrusted `originalName`, so identical bytes under different names would mint different keys and silently break dedup; `contentType` lives in the metadata row) — so two puts of identical bytes return the SAME key with refs=2 and `deduplicated:true` on the second result, metadata first-write-wins. `release()` DECREMENTS and never deletes bytes; a blob reaching 0 refs is stamped and reads as ABSENT through get/stat/resolve/findByDigest (the grace window is GC detail, not a caller-visible afterlife), while a re-upload inside the window resurrects it for free. The GC sweep (per-driver unref'd setInterval — the lib/job precedent, `lib/cron` stays dormant; `sweepInterval` default '15m', '0s' = off; `sweepGrace` default '1h'; both REASONED defaults, not benchmarks — they gate reclaim latency only) collects blobs at 0 past grace in a load-bearing ORDER: claim the row FIRST (`removeIfZero` — a guarded delete a concurrent `acquireRef` resurrection always beats), unlink the file SECOND. The inverse order can LOSE BYTES under a racing dedup-hit put (it would discard its temp against a row whose file just vanished); this order's crash window merely orphans an UNREFERENCED file — invisible to every verb, harmless, `storage:verify` territory (the CLI/maintenance slice below) — and a dedup hit whose blob file is missing (exactly that residue) HEALS by renaming its own temp in. In-process the sweep-vs-put race cannot interleave AT ALL (the embedded store is synchronous, so acquire→check→publish and claim→unlink each run in one uninterruptible JS turn); the multi-process residual belongs to future connector stores and is documented on the seam contract. `findByDigest(algo, hex, cb)` -> key|null is the ONLY sanctioned digest→key door (keys stay opaque even though cas keys LOOK parseable) and is a DEDUP ORACLE — "someone already uploaded this exact file" is an information leak across users — so it ships as a driver verb ONLY, no HTTP endpoint: a consumer exposing one owns auth and per-driver scoping. Algorithm agility: `hash` (default 'sha256') is per-driver config validated at boot against THIS runtime's `crypto.getHashes()` — an unavailable name is FATAL, and the capability is runtime-scoped (measured: sha256/sha512 on both supported runtimes, blake2b512 Node-only) — and since the algo is a path segment, a hash CHANGE is additive (the old namespace stays addressable and findByDigest-able under its own algo, dedup does not span namespaces, the boot notes the change once and restamps) while a STRATEGY change on populated storage is a re-key migration, which is why `start()` now stamps every driver root (a reserved `.driver` row whose stamp rides the store's binary-safe `data` slot) and warns EVERY boot on mismatch without restamping — surfaced through the new `opt.warn` hook (a callback, not a logger import; `start()`'s boolean return is unchanged). The refcount machinery is four OPTIONAL seam verbs (`acquireRef`/`releaseRef`/`listZeroRefs`/`removeIfZero`) the embedded store implements atomically-by-synchrony from plain UPDATE/SELECT/INSERT — deliberately NO `RETURNING`, which sits outside the Bun sqlite adapter's measured surface — plus additive idempotent `refs`/`zero_at` migrations; a cas driver REFUSES to build over a store lacking the verbs (boot fatal), and `refs` appears on get()/stat() output only for refcounted rows (the `data` pattern, so non-cas rows keep their exact shape — `set()` must never touch a refcounted row, REPLACE would reset the count). Config keys are PER-STRATEGY (`STRATEGY_KEYS`: `hash`/`fsync`/`sweepInterval`/`sweepGrace` are consumed by cas and ignored-with-a-warning under sharded; durations parse via `parseDuration`, unit-required exactly like the size keys — '15m' means minutes here and megabytes there, which is why neither parser guesses). The sharded write path, key shape and durability are byte-for-byte unchanged, and size tiering applies under cas exactly as under sharded — an inline cas blob is a refcounted row carrying `data`, dedup included, collected by row-deletion alone. `maxObjectSize` requires an EXPLICIT unit (`"50MB"`); a bare number warns and falls back to the default, deliberately stricter than `settings.upload` where a bare number means MB for back-compat — `upload` already reads a bare number as a count (`maxFields`) in one key and as MB (`maxFieldsSize`) in another, so there is no house meaning to inherit and guessing would be the bug. **Framework independence is the structural contract**: `lib/storage` imports no gina core, no `lib` registry, and no injected globals (`getContext`/`getConfig`/`_()`), so the web-served roots are INJECTED into `validateConfig` rather than read; `lib/sqlite-driver` is the ONE sanctioned import and an exact-count test pins it so a second exception cannot arrive as drift. `validateConfig(block, {servedRoots})` is pure and enable-agnostic (the `lib/render-cache` shape): it lints unconditionally and returns `{fatal, warnings, driverCount}` — the CALLER owns aborting, and the three semantics often misattributed to the validator (lint-always / build-only-when-enabled / fatal-downgrades-to-warn) all live in the boot block, not in it. Boot wiring sits in `gna.js` inside `server.on('started')`, i.e. **POST-listen**, so a storage fatal exits on an already-bound port exactly as the #RC4 and #AI6 blocks do; a pre-listen refusal would mean moving the lint into `core/server.js` `init()` where the audit trail does it. **Slice 1 binds the upload path (the #B224 resolution vehicle): a settings.json upload.groups.<name>.driver key routes that group's self.store() step through the named driver.** store() partitions per file on the RESOLVED group (req.files[].group — which since #B140, same arc, carries untagged instead of undefined for untagged parts; caller-synthesized lists normalize the same way): files in driver-less groups keep the historical move path byte-for-byte (same entry shapes — no new fields, same false success sentinel, same abort-on-first-error), while routed files stream from their parse-time staging path into driver.put() sequentially (abort on first failure, NO rollback of already-published objects, ENOENT-tolerant post-publish source unlink — the mover's exact semantics) and their result entries carry {file, group, driver, key, size, type, encoding} with NO filename: key is the opaque reference, size is the layer's own measurement of the published bytes, reads go through gina.storage(driver). target may be null ONLY when every file routes (a move-path file with no target errors naming the file + group; the target dir is only created when a move-path file exists). Boot lint: gna.js collects every bundle's group→driver bindings as NEUTRAL {owner, driver, path} tuples into validateConfig's context.groupBindings — a binding naming an undefined driver (or ANY binding when no storage/drivers block exists) is FATAL; path BESIDE driver is LEGAL (for a routed group path is only the parse-time staging dir — the multipart landing behavior is unchanged) but a path INSIDE the driver's root warns (staging into the store tree strands files no key references). Parse-time enforcement (#B50 gate, allowedExtensions, isMultipleAllowed, maxFields) is untouched; a routed file additionally meets the driver's maxObjectSize at put() time. Riding the same arc, #B224 resolved as retire+declare: the scaffold template's inert advertised keys (filePrefix/subFolder/per-group maxFieldsSize — and the inert block-level encoding, which busboy ignores in favor of its hardcoded utf8 param decoder) are retired, the three false per-group-redefinability comments corrected, and schema/settings.json now declares the REAL upload key set at both levels (additionalProperties stays true at BOTH — apps legitimately carry their own group keys, e.g. app-side composition). **The CLI/maintenance slice: `storage:stats` / `storage:gc` / `storage:verify` + the always-on admin-gated `/_gina/storage/{stats,gc,verify}` endpoint family.** The spine: who touches the store depends on who OWNS it (the embedded store is single-process-per-root), so each command sends its own request to the bundle's assigned ports (the cache:stats candidates walk — advance on ECONNREFUSED): a RESPONSE means RUNNING and the owning process did the work via the endpoint (engine-agnostic `server.js onRequest()` ONLY — the isaac listener falls through via its cb, the `/_gina/agent` precedent; `app.json admin.allowFrom` gate, ^-anchored patterns per the release-watch lesson, NO dev gate; stats/verify GET, gc POST — a gc pass is a mutation); every-port-ECONNREFUSED means DOWN and the CLI resolves the bundle's raw `settings.storage` (requireJSON — scaffolded configs carry comments) and builds the REAL driver through the boot's own exported seams (`_resolveDriverConf`/`_FACTORIES`/`_createEmbeddedMetaStore`, `sweepInterval:0`; a missing root is reported, never mkdir'd — a stats read must not mutate); a TIMEOUT or any other socket error opens NOTHING (the bundle may be alive and owning the store). Connector-backed stores have no offline path — named-and-skipped ("nothing local to open; start the bundle and re-run"). New module/driver surface: `lib.storage.list()` (built driver names, `[]` before start — the enumeration door, so callers need no isStarted() pre-check); `stats(cb)` on BOTH strategies → `{name, strategy, root, capabilities, store}` with `store` = the metadata store's OPTIONAL `stats()` aggregate `{objects, refcounted, zeroRefPending, inline, bytes}` (reserved dot-key rows excluded — the `.driver` stamp must not read as an object) or `null` = "store reports no stats"; cas `sweepNow([{dryRun}], cb)` — `_sweepOnce` PROMOTED to a documented door, the underscore alias kept for the test seam — → `{collected|collectable, drained}` where `drained` answers "anything older-than-grace left?" so gc DRAINS by looping the batch-capped pass (hard-bounded at 1000 passes) instead of guessing the cap, and dryRun lists without claiming a row; cas `verify([{fix}], cb)` — the files↔rows scan, BOTH directions age-gated past `sweepGrace` (a young file may be in-flight work; a young row sits inside put()'s acquire→rename window, where a row legitimately precedes its file): `file-without-row` (the sweep's crash residue — FIXABLE, `fix` unlinks; OFFLINE-ONLY: the CLI refuses `--fix` while the bundle runs and the endpoint never parses a fix flag, making the refusal structural) vs `row-without-file` at refs>=1 (LOSS EVIDENCE — reported, NEVER auto-fixed: deleting the row would destroy the signal; zero-ref rows are skipped — the sweep's claim + ENOENT-tolerant unlink self-heals them, so reporting would flap); the findings LIST is capped at 1000 with exact counts + a truncation flag (a silent cap would read as "that was all of them"); the rows direction rides the second OPTIONAL seam verb `listKeys(afterKey, limit, cb)` (key-ordered cursor pages, dot-keys excluded) — a store lacking it degrades verify to files-only (`rowsChecked:false`). `sharded` has no sweep and no verify in v1 — named-and-skipped, never an error; gc/verify against a root shared by several bundles are idempotent (same result from whichever bundle). CLI grammar is the cache family's: `<bundle> @<project>` or all-bundles, `--driver=`, `--format=json` (fs.writeSync flush-before-exit), `--dry-run` (gc), `--fix` (verify); `storage:` joined bin/cli's `allowedOffline` (the hard gate — a topic dir alone is unreachable, the dispatch itself is pure `lib/cmd/<topic>/<action>.js` convention).
|
|
1014
|
-
301. **FormValidator — submit pipeline, async `query` rule, loading state & error rendering (consolidates former #257/#261/#264/#296/#297; the engine/binding/a11y parent is #209).** **Rules & binding (former #257 — #B127/#B128/#B129 0.5.20, #B138 0.5.22):** numbered `is` aliases (`is0`, `is1`, … — multiple `is` conditions per field) INSTALL and enforce on client AND server (anchored `/^is\d+$/`) with a DISTINCT error key per alias on both sides (previously the per-rule typecheck `continue`d on exactly the undefined state the lazy installer required — silently skipping every `is<N>` key — and the shared server key let a later PASSING alias erase an earlier FAILING alias's error, a silent bypass). ⚠️ Enforcement-tightening: `is<N>` rules that silently skipped START ENFORCING at pickup — sweep rule JSONs for `is<N>` keys before upgrading. The `is` grammar remains ONE binary comparison (`=== !== == != < > <= >=`), no `&&` compounds. #B138: the shared `_currentValidatorAlias` handshake slot re-arms immediately before EVERY delegation (it was armed once at install while `is()` consumes-with-delete — any re-application against the same instance collapsed every numbered rule onto the bare key `is`, last-declared-wins, its message rendered twice). Injected forms (`validateFormById`) resolve their declared rule ATTRIBUTE-FIRST (`data-gina-form-rule`; the id-derived name stays the fallback) — pre-fix a form injected after boot bound `{}` rules. Live-check gate precedence: an AUTHOR-set `data-gina-form-live-check-enabled` always wins; the gate decides only when the attribute is absent (rules bound → true). The cross-field `_case_` scan coerces the CASE VALUE (declare boolean cases as JSON booleans; the strings "true"/"false" coerce consistently), and a MATCHING case application replaces that field's rule set in the bound form's rules store PERSISTENTLY for the page. There are NO standalone `maxLength`/`minLength` engine rules — length constraints are `isString`/`isNumber`/`isInteger` parameters; unknown rule keys are silently skipped. **Autocomplete interception + stale-error clear (former #261 — #B134/#B135/#B136):** the Safari-autocomplete keydown interception (autocomplete="off" fields on a live-check form) (a) bails on modifier chords — `e.metaKey`/`e.ctrlKey` returns BEFORE preventDefault so native select-all/copy/paste/cut/undo run (pre-fix Cmd+A typed the chord letter and keyboard paste was dead — `execCommand("paste")` is inert in unprivileged content); (b) gates on REAL Safari only (`/safari/i && !/chrom(e|ium)/i` — every Chromium UA carries the Safari token; WebKit-on-iOS browsers stay matched by design); and (c) the live-check whole-form pass, when it turns VALID, clears every previously-errored field's rendered error in BOTH copies instead of leaving a stale blocking paragraph beside the re-enabled trigger (the pass still never renders NEW errors on untouched fields). **The async `query` rule (former #264 — #B87 0.5.22; the #B332-#B346 cluster, 0.6.7):** the result path is BLANKET-guarded — a throw while processing the response (a detached target's null `.form` after a popin closed mid-flight, a malformed JSON body, a boolean value hitting `.toLowerCase()`) routes to `releaseQueryWaiter` (warn + release with field state as-is — FAIL-OPEN by design: the verdict is unknown and the server re-validates on submit); `xhr.onload`'s catch and the wrapped `onerror` route to the same release; the release helper is itself try-guarded and names the field. A submit can never complete while the verdict is on the wire, and ONE production POST per click at most: `getOwnedElements` dedups by node identity (an id-less submit trigger used to enter twice and stack TWO listeners — one click ran two full validate() passes, #B333), the async-arm guard is PASS-LOCAL (`armedAsyncFields` — waiters stack per pass and self-detach on first fire; the second pass used to find the first pass's waiter, zero ITS OWN counter and complete on the sync-only verdict ~200ms before the query answered, #B332), the wire dedups via a `ginaFormValidatorQueryPending` dataset marker, and every `validated.<formId>` listener consumes only its own pass's dispatch and detaches (stale listeners used to replay the submit once per leftover, #B334). #B337: the completion payload is the no-arg `d.getErrors()` — the WHOLE pass's verdict (field-scoped payloads meant other invalid fields were adjudicated but never rendered on the submit path, and the inverse scene dispatched an EMPTY error set — refusing the submit with no rendered reason). Engine contract worth knowing: `queryFromFrontend` SKIPS the wire when `self.isValid()` is already false, so whether a query fires on an invalid form depends on FIELD ORDER — the settle/skip completion payload must carry the full verdict either way. #B338: both formerly-synchronous releases (cached verdict, wire skip) defer via `queueMicrotask` so every release takes the post-pass shape a real settle has (they used to fire mid-field-loop, composing a completion missing every field declared after the query field and CLEARING their rendered errors on a re-click). #B342: a latched form's async completion dispatches UNCONDITIONALLY — the waiter's clean path used to dispatch only when the query field happened to be LAST in the rule set, so a clean submit with the query declared earlier starved the callback, no POST ever left, and the `isSubmitting` latch stranded the form until reload (masked pre-arc by the very #B332 double-pass the arc removed). #B346 closed the LAST silent door, one step UPSTREAM — the trigger gate: with live-check on, `updateSubmitTriggerState` gates the trigger whenever the form is not-yet-valid, but while a query is ON THE WIRE the form has NO verdict, and the gate misclassified verdict-pending as invalid — a click inside the round-trip window died silently (consumer-measured 0/6 inside vs 6/6 after; the window scales with query LATENCY, not typing speed). The refusal is narrowed by `isAwaitingQueryVerdictOnly`: the framework's gated mark must be the ONLY refusal arm (an authored `aria-disabled` or non-IDL `disabled` still refuses), an owned field carries the in-flight marker (checked in-form AND on `form="<id>"` reassociated controls, CSS.escape-guarded), and no SETTLED field holds a committed error (the pending field's own provisional `query` entry — staged by the same-value fast path — does not count; counting it refused every window the narrowing exists to recognize, measured). Consulted at the click proxy, the trusted-submit proxy (wrapped-label `<button type="submit"><span>` shapes; Enter with a default button is spec-synthesized into a click on it) and the loading arm; the shared fresh-validate path LATCHES `isSubmitting` when proceeding under a pending verdict (an un-latched async pass STARVES — this was also silently starving programmatic `submit()`) and releases the latch before any consumer `submit.<id>` dispatch, so a stopping custom handler cannot strand the form. **Send lifecycle (former #296 — #B175/#B176 0.6.1, #B192 0.6.3):** every `send()` builds its own LOCAL XHR (the module-scope reuse re-`open()`ed a completed instance, synchronously replaying the PREVIOUS submit's `onreadystatechange` at readyState 1 — submitting form B after form A stranded A's trigger natively-disabled with no release path) and registers a `loadend` release — success, error, timeout and abort alike — removing `disabled`/`aria-disabled` + `data-gina-form-loading` and clearing `$form.isSending`/`$form.sent`; `$form.isSending` genuinely spans send→settled; the timeout path REMOVES `data-gina-form-loading` (never writes the truthy string "false"). #B192: a REJECTED submit releases the `isSubmitting` latch (the only clear was the XHR settle, which a rejected submit never reaches — one invalid attempt latched the flag for the page's life, swallowing every later keystroke; the flag lives on the `$forms[id]` OBJECT so it even survived a full unbind/rebind). Diagnostic: read `gina.validator.$forms['<id>'].isSubmitting` — truthy IS the latched state (exactly what the live-check gate tests); the other discriminator is the live-check request count after a keystroke (0 latched, 1 healthy). ⚠️ **Do NOT use `isValidating` for this** — consumer-A/B-measured NON-discriminating (2026-08-01): it inits null and is set by EVERY validation cycle, so on any form that live-checked before the submit it reads `false` in BOTH the latched and healthy states. #B176: the live-check opt-out is honored consistently — the two gates that evaluated the rules-count boolean INSIDE `/^(true)$/i.test(…)` (an explicit `"false"` short-circuited to the count and matched) now test the attribute alone; the three `registerForLiveChecking` sites always did. Standing caution: `setOptions()` is a MODULE-wide default setter, not per-call. **Loading state (`data-gina-loading` + `lib/loading-state`, 0.6.4-0.6.5):** the FORM-scoped `data-gina-form-loading` says "a request is in flight"; the TRIGGER-scoped `data-gina-loading` marks the submit-like element the user actually operated — `"true"` while its action runs, `"false"` at ANY terminal outcome. Five release paths, two reaching no XHR at all: a submit REJECTED by validation, `send()`'s rate-limit early return, plus `loadend`, readyState 4, and never-arming when the disabled-trigger gate turns the click away. ⚠️ **The `"false"` value CONSTRAINS READERS: the attribute stays PRESENT when released — match on the VALUE, `[data-gina-loading="true"]`, never a bare `[data-gina-loading]`** (which matches a released trigger and pins the loading style on permanently); framework code uses the primitive's `isArmed()` (`=== "true"`). Arming happens at TWO sites because one is not enough: the click proxy (after the disabled-trigger gate, climbing from `event.target` to the owning button — a click on `<button type="submit"><span>Save</span></button>` targets the inner node) and the native submit proxy (where Enter, `form.submit()`, `$validator.submit()` and that same wrapped-label click actually surface). The attribute NAME is configurable — `gina.setOptions({ loadingAttribute: 'data-loading' })` (#B305: `setOptions()` used to write an orphan object nothing read — inert for every key since it shipped; it now merges into the exposed `gina.config` in place; on builds predating the fix use direct assignment `gina.config.loadingAttribute = …`), read LAZILY on every call; deliberately NO auto-detection (a DOM probe reads nothing at rest, a stylesheet probe races late sheets and throws cross-origin, and writing both names stacks spinners). The logic lives in the stateless `lib/loading-state` primitive (aliased into the browser bundle in BOTH build configs, required from `core.js`, intentionally NOT in the `lib/index.js` registry). `resolveTrigger` takes an optional third argument — a PREDICATE saying what counts as a trigger (default: submit-like controls only): the WALK belongs to the primitive, the notion of a trigger to each caller; a predicate that throws is deliberately not caught. ⚠️ Neither popin nor link passes one — both resolve by id/`closest()`, never from a raw click target. **The LINK plugin carries the same one-XHR-per-request repair (0.6.4):** each link request builds its own transport carrying a sequence taken BEFORE the supersede-abort (an `open()` on a busy object implicitly ABORTS it, an aborted request reaches readyState 4 with status 0, and `handleXhr` has no status-0 branch — the first click's completion fired NOTHING); link arms `data-gina-loading` on the anchor inside `linkRequest` (every entry funnels through it) and releases from a `loadend` listener — which fires for success, error, abort and status-0 alike; only an indefinite hang escapes it. The arm is GATED on `xhr.addEventListener` existing (the CORS branch can swap in a legacy `XDomainRequest` — never arm what cannot be released), and the release is deliberately NOT sequence-guarded (the sequence drops a stale RESPONSE; a superseded request must still RELEASE its trigger). Measured, not assumed: `abort()` fires `abort` + `loadend` SYNCHRONOUSLY, so the superseded request releases before the newer click arms. ⚠️ Still open and deliberately so: a genuine network failure remains silent and a hanging link request has no deadline (`xhr.timeout` never set) — both held back as consumer-visible changes for their own release note. **POPIN arms its trigger too (0.6.4):** `armPopinTrigger()` at both arm sites; releases at readyState 4, in `popinClose`, and in `popinUnbind` — the LAST is load-bearing because routing transitions reach `popinUnbind` WITHOUT `popinClose`. ⚠️ `data-gina-popin-loading` (the CONTAINER — "this popin is filling") and `data-gina-loading` (the TRIGGER — "this control is busy") are NOT the same signal; a rename between them is a regression. The formerly-open preload gap is CLOSED (#B285, 0.6.5): a popin opened from an in-flight preload arms via the adopted-preload arm. Maintainer cautions: `popin.test.js:778` pins `showLoadingShell($popin, $el);` at >= 2 LITERAL occurrences (absorbing the arm block into a helper reds it — grep it with `-F`, `$` and `(` are PCRE metacharacters), and `popin.test.js`'s `driveConsume` extract-and-executes the consumePreload bytes with only 7 injected symbols. **Default CSS + the `@layer gina` override contract (0.6.4-0.6.7):** `[data-gina-loading="true"]` gets a `progress` cursor + an opacity pulse gated on `prefers-reduced-motion: no-preference` (a static dim under `reduce` — a traded signal, not none). Two omissions are deliberate: NO pseudo-element spinner (stacks with a consumer's, forces `position: relative`) and NO `pointer-events: none` (suppresses the second click link deliberately supersedes). Default LOOKS (loading + the `data-gina-form-submit-gated` not-ready look) ship inside `@layer gina`, so ANY un-layered project rule overrides them regardless of specificity or load order — load-bearing because gina's stylesheet-injection order relative to project CSS is author-dependent; projects organising their own CSS in layers order themselves with `@layer gina, app;`; NEVER add `!important` inside the layer (important INVERTS layer priority). Functional rules (popin structure, scroll-lock) deliberately stay UN-layered so a generic project reset cannot break the mechanism. Browsers without `@layer` drop the block (attributes still written, project CSS still applies); `animation: none` drops just the motion; renaming via `loadingAttribute` opts out of the stylesheet entirely (keyed on the default name — the gated attribute is NOT renameable). ⚠️ Build gotcha, general to any new gina stylesheet: the CSS phase concatenates ONLY the compiled file whose basename matches its own directory (`foo/sass/bar.scss` compiles but never reaches `gina.min.css`); `inspector` is skipped entirely (served separately). **Labels & rendering (former #297 — #B178 family, 0.6.1):** the key an app can OBSERVE in a field's `errors` object is not always the key the label catalog is consulted under — four rule families write a generic key while consulting a specific label key (the float NaN branch renders `errorLabels['toFloatNAN']` into `errors['toFloat']`; the Length families render `Min`/`Max` label variants into the generic `is<X>Length` keys) — so an ALIAS FILL applies to every app-supplied label layer (server catalog node, client whisper, per-culture registrations, `setErrorLabels()`): a supplied generic fills the specific keys the app did not supply; a supplied specific still wins; English defaults untouched. Numbered `is` aliases with no rule-supplied text fall back to the shared `is` label (translate catalog key `is` once to cover every alias); the user-validator setup loop fills only when the app supplied none (it used to reset every user-defined validator's label to English, clobbering catalog translations). Render-side: the aria-live announcement joins per-message texts with a sentence separator (raw `textContent` concatenated with NO separator — a screen reader received one run-on string), and two rules resolving to byte-identical text render ONCE (the dev inspector still records every error key). All browser-bundled — consumers RE-BAKE at pickup; apps that translated only observable keys will see those messages switch from English to their language. Tests: `validator-autocomplete-interception.test.js` / `validator-livecheck-stale-error-clear.test.js` (extracted-real-bytes + red-first dist pins — Closure De-Morgans the chord bail, so minified pins target property-name shapes, never var names) + the #B332-cluster e2e matrix (incl. the field-order arm: a clean form whose query field is NOT last must send exactly once, after the settle).
|
|
1014
|
+
301. **FormValidator — submit pipeline, async `query` rule, loading state & error rendering (consolidates former #257/#261/#264/#296/#297; the engine/binding/a11y parent is #209).** **Rules & binding (former #257 — #B127/#B128/#B129 0.5.20, #B138 0.5.22):** numbered `is` aliases (`is0`, `is1`, … — multiple `is` conditions per field) INSTALL and enforce on client AND server (anchored `/^is\d+$/`) with a DISTINCT error key per alias on both sides (previously the per-rule typecheck `continue`d on exactly the undefined state the lazy installer required — silently skipping every `is<N>` key — and the shared server key let a later PASSING alias erase an earlier FAILING alias's error, a silent bypass). ⚠️ Enforcement-tightening: `is<N>` rules that silently skipped START ENFORCING at pickup — sweep rule JSONs for `is<N>` keys before upgrading. The `is` grammar remains ONE binary comparison (`=== !== == != < > <= >=`), no `&&` compounds. #B138: the shared `_currentValidatorAlias` handshake slot re-arms immediately before EVERY delegation (it was armed once at install while `is()` consumes-with-delete — any re-application against the same instance collapsed every numbered rule onto the bare key `is`, last-declared-wins, its message rendered twice). Injected forms (`validateFormById`) resolve their declared rule ATTRIBUTE-FIRST (`data-gina-form-rule`; the id-derived name stays the fallback) — pre-fix a form injected after boot bound `{}` rules. Live-check gate precedence: an AUTHOR-set `data-gina-form-live-check-enabled` always wins; the gate decides only when the attribute is absent (rules bound → true). The cross-field `_case_` scan coerces the CASE VALUE (declare boolean cases as JSON booleans; the strings "true"/"false" coerce consistently), and a MATCHING case application replaces that field's rule set in the bound form's rules store PERSISTENTLY for the page. There are NO standalone `maxLength`/`minLength` engine rules — length constraints are `isString`/`isNumber`/`isInteger` parameters; unknown rule keys are silently skipped. **Autocomplete interception + stale-error clear (former #261 — #B134/#B135/#B136):** the Safari-autocomplete keydown interception (autocomplete="off" fields on a live-check form) (a) bails on modifier chords — `e.metaKey`/`e.ctrlKey` returns BEFORE preventDefault so native select-all/copy/paste/cut/undo run (pre-fix Cmd+A typed the chord letter and keyboard paste was dead — `execCommand("paste")` is inert in unprivileged content); (b) gates on REAL Safari only (`/safari/i && !/chrom(e|ium)/i` — every Chromium UA carries the Safari token; WebKit-on-iOS browsers stay matched by design); and (c) the live-check whole-form pass, when it turns VALID, clears every previously-errored field's rendered error in BOTH copies instead of leaving a stale blocking paragraph beside the re-enabled trigger (the pass still never renders NEW errors on untouched fields). **The async `query` rule (former #264 — #B87 0.5.22; the #B332-#B346 cluster, 0.6.7):** the result path is BLANKET-guarded — a throw while processing the response (a detached target's null `.form` after a popin closed mid-flight, a malformed JSON body, a boolean value hitting `.toLowerCase()`) routes to `releaseQueryWaiter` (warn + release with field state as-is — FAIL-OPEN by design: the verdict is unknown and the server re-validates on submit); `xhr.onload`'s catch and the wrapped `onerror` route to the same release; the release helper is itself try-guarded and names the field. A submit can never complete while the verdict is on the wire, and ONE production POST per click at most: `getOwnedElements` dedups by node identity (an id-less submit trigger used to enter twice and stack TWO listeners — one click ran two full validate() passes, #B333), the async-arm guard is PASS-LOCAL (`armedAsyncFields` — waiters stack per pass and self-detach on first fire; the second pass used to find the first pass's waiter, zero ITS OWN counter and complete on the sync-only verdict ~200ms before the query answered, #B332), the wire dedups via a `ginaFormValidatorQueryPending` dataset marker, and every `validated.<formId>` listener consumes only its own pass's dispatch and detaches (stale listeners used to replay the submit once per leftover, #B334). #B337: the completion payload is the no-arg `d.getErrors()` — the WHOLE pass's verdict (field-scoped payloads meant other invalid fields were adjudicated but never rendered on the submit path, and the inverse scene dispatched an EMPTY error set — refusing the submit with no rendered reason). Engine contract worth knowing: `queryFromFrontend` SKIPS the wire when `self.isValid()` is already false, so whether a query fires on an invalid form depends on FIELD ORDER — the settle/skip completion payload must carry the full verdict either way. #B338: both formerly-synchronous releases (cached verdict, wire skip) defer via `queueMicrotask` so every release takes the post-pass shape a real settle has (they used to fire mid-field-loop, composing a completion missing every field declared after the query field and CLEARING their rendered errors on a re-click). #B342: a latched form's async completion dispatches UNCONDITIONALLY — the waiter's clean path used to dispatch only when the query field happened to be LAST in the rule set, so a clean submit with the query declared earlier starved the callback, no POST ever left, and the `isSubmitting` latch stranded the form until reload (masked pre-arc by the very #B332 double-pass the arc removed). #B346 closed the LAST silent door, one step UPSTREAM — the trigger gate: with live-check on, `updateSubmitTriggerState` gates the trigger whenever the form is not-yet-valid, but while a query is ON THE WIRE the form has NO verdict, and the gate misclassified verdict-pending as invalid — a click inside the round-trip window died silently (consumer-measured 0/6 inside vs 6/6 after; the window scales with query LATENCY, not typing speed). The refusal is narrowed by `isAwaitingQueryVerdictOnly`: the framework's gated mark must be the ONLY refusal arm (an authored `aria-disabled` or non-IDL `disabled` still refuses), an owned field carries the in-flight marker (checked in-form AND on `form="<id>"` reassociated controls, CSS.escape-guarded), and no SETTLED field holds a committed error (the pending field's own provisional `query` entry — staged by the same-value fast path — does not count; counting it refused every window the narrowing exists to recognize, measured). Consulted at the click proxy, the trusted-submit proxy (wrapped-label `<button type="submit"><span>` shapes; Enter with a default button is spec-synthesized into a click on it) and the loading arm; the shared fresh-validate path LATCHES `isSubmitting` when proceeding under a pending verdict (an un-latched async pass STARVES — this was also silently starving programmatic `submit()`) and releases the latch before any consumer `submit.<id>` dispatch, so a stopping custom handler cannot strand the form. **Send lifecycle (former #296 — #B175/#B176 0.6.1, #B192 0.6.3):** every `send()` builds its own LOCAL XHR (the module-scope reuse re-`open()`ed a completed instance, synchronously replaying the PREVIOUS submit's `onreadystatechange` at readyState 1 — submitting form B after form A stranded A's trigger natively-disabled with no release path) and registers a `loadend` release — success, error, timeout and abort alike — removing `disabled`/`aria-disabled` + `data-gina-form-loading` and clearing `$form.isSending`/`$form.sent`; `$form.isSending` genuinely spans send→settled; the timeout path REMOVES `data-gina-form-loading` (never writes the truthy string "false"). #B192: a REJECTED submit releases the `isSubmitting` latch (the only clear was the XHR settle, which a rejected submit never reaches — one invalid attempt latched the flag for the page's life, swallowing every later keystroke; the flag lives on the `$forms[id]` OBJECT so it even survived a full unbind/rebind). Diagnostic: read `gina.validator.$forms['<id>'].isSubmitting` — truthy IS the latched state (exactly what the live-check gate tests); the other discriminator is the live-check request count after a keystroke (0 latched, 1 healthy). ⚠️ **Do NOT use `isValidating` for this** — consumer-A/B-measured NON-discriminating (2026-08-01): it inits null and is set by EVERY validation cycle, so on any form that live-checked before the submit it reads `false` in BOTH the latched and healthy states. #B176: the live-check opt-out is honored consistently — the two gates that evaluated the rules-count boolean INSIDE `/^(true)$/i.test(…)` (an explicit `"false"` short-circuited to the count and matched) now test the attribute alone; the three `registerForLiveChecking` sites always did. Standing caution: `setOptions()` is a MODULE-wide default setter, not per-call. **Loading state (`data-gina-loading` + `lib/loading-state`, 0.6.4-0.6.5):** the FORM-scoped `data-gina-form-loading` says "a request is in flight"; the TRIGGER-scoped `data-gina-loading` marks the submit-like element the user actually operated — `"true"` while its action runs, `"false"` at ANY terminal outcome. Five release paths, two reaching no XHR at all: a submit REJECTED by validation, `send()`'s rate-limit early return, plus `loadend`, readyState 4, and never-arming when the disabled-trigger gate turns the click away. ⚠️ **The `"false"` value CONSTRAINS READERS: the attribute stays PRESENT when released — match on the VALUE, `[data-gina-loading="true"]`, never a bare `[data-gina-loading]`** (which matches a released trigger and pins the loading style on permanently); framework code uses the primitive's `isArmed()` (`=== "true"`). Arming happens at TWO sites because one is not enough: the click proxy (after the disabled-trigger gate, climbing from `event.target` to the owning button — a click on `<button type="submit"><span>Save</span></button>` targets the inner node) and the native submit proxy (where Enter, `form.submit()`, `$validator.submit()` and that same wrapped-label click actually surface). The attribute NAME is configurable — `gina.setOptions({ loadingAttribute: 'data-loading' })` (#B305: `setOptions()` used to write an orphan object nothing read — inert for every key since it shipped; it now merges into the exposed `gina.config` in place; on builds predating the fix use direct assignment `gina.config.loadingAttribute = …`), read LAZILY on every call; deliberately NO auto-detection (a DOM probe reads nothing at rest, a stylesheet probe races late sheets and throws cross-origin, and writing both names stacks spinners). The logic lives in the stateless `lib/loading-state` primitive (aliased into the browser bundle in BOTH build configs, required from `core.js`, intentionally NOT in the `lib/index.js` registry). `resolveTrigger` takes an optional third argument — a PREDICATE saying what counts as a trigger (default: submit-like controls only): the WALK belongs to the primitive, the notion of a trigger to each caller; a predicate that throws is deliberately not caught. ⚠️ Neither popin nor link passes one — both resolve by id/`closest()`, never from a raw click target. **The LINK plugin carries the same one-XHR-per-request repair (0.6.4):** each link request builds its own transport carrying a sequence taken BEFORE the supersede-abort (an `open()` on a busy object implicitly ABORTS it, an aborted request reaches readyState 4 with status 0, and `handleXhr` has no status-0 branch — the first click's completion fired NOTHING); link arms `data-gina-loading` on the anchor inside `linkRequest` (every entry funnels through it) and releases from a `loadend` listener — which fires for success, error, abort and status-0 alike; only an indefinite hang escapes it. The arm is GATED on `xhr.addEventListener` existing (the CORS branch can swap in a legacy `XDomainRequest` — never arm what cannot be released), and the release is deliberately NOT sequence-guarded (the sequence drops a stale RESPONSE; a superseded request must still RELEASE its trigger). Measured, not assumed: `abort()` fires `abort` + `loadend` SYNCHRONOUSLY, so the superseded request releases before the newer click arms. ⚠️ Still open and deliberately so: a genuine network failure remains silent and a hanging link request has no deadline (`xhr.timeout` never set) — both held back as consumer-visible changes for their own release note. **POPIN arms its trigger too (0.6.4):** `armPopinTrigger()` at both arm sites; releases at readyState 4, in `popinClose`, and in `popinUnbind` — the LAST is load-bearing because routing transitions reach `popinUnbind` WITHOUT `popinClose`. ⚠️ `data-gina-popin-loading` (the CONTAINER — "this popin is filling") and `data-gina-loading` (the TRIGGER — "this control is busy") are NOT the same signal; a rename between them is a regression. The formerly-open preload gap is CLOSED (#B285, 0.6.5): a popin opened from an in-flight preload arms via the adopted-preload arm. Maintainer cautions: `popin.test.js:778` pins `showLoadingShell($popin, $el);` at >= 2 LITERAL occurrences (absorbing the arm block into a helper reds it — grep it with `-F`, `$` and `(` are PCRE metacharacters), and `popin.test.js`'s `driveConsume` extract-and-executes the consumePreload bytes with only 7 injected symbols. **Default CSS + the `@layer gina` override contract (0.6.4-0.6.7):** `[data-gina-loading="true"]` gets a `progress` cursor + an opacity pulse gated on `prefers-reduced-motion: no-preference` (a static dim under `reduce` — a traded signal, not none). Two omissions are deliberate: NO pseudo-element spinner (stacks with a consumer's, forces `position: relative`) and NO `pointer-events: none` (suppresses the second click link deliberately supersedes). Default LOOKS (loading + the `data-gina-form-submit-gated` not-ready look) ship inside `@layer gina`, so ANY un-layered project rule overrides them regardless of specificity or load order — load-bearing because gina's stylesheet-injection order relative to project CSS is author-dependent; projects organising their own CSS in layers order themselves with `@layer gina, app;`; NEVER add `!important` inside the layer (important INVERTS layer priority). Functional rules (popin structure, scroll-lock) deliberately stay UN-layered so a generic project reset cannot break the mechanism. Browsers without `@layer` drop the block (attributes still written, project CSS still applies); `animation: none` drops just the motion; renaming via `loadingAttribute` opts out of the stylesheet entirely (keyed on the default name — the gated attribute is NOT renameable). ⚠️ Build gotcha, general to any new gina stylesheet: the CSS phase concatenates ONLY the compiled file whose basename matches its own directory (`foo/sass/bar.scss` compiles but never reaches `gina.min.css`); `inspector` is skipped entirely (served separately). **Labels & rendering (former #297 — #B178 family, 0.6.1):** the key an app can OBSERVE in a field's `errors` object is not always the key the label catalog is consulted under — four rule families write a generic key while consulting a specific label key (the float NaN branch renders `errorLabels['toFloatNAN']` into `errors['toFloat']`; the Length families render `Min`/`Max` label variants into the generic `is<X>Length` keys) — so an ALIAS FILL applies to every app-supplied label layer (server catalog node, client whisper, per-culture registrations, `setErrorLabels()`): a supplied generic fills the specific keys the app did not supply; a supplied specific still wins; English defaults untouched. Numbered `is` aliases with no rule-supplied text fall back to the shared `is` label (translate catalog key `is` once to cover every alias); the user-validator setup loop fills only when the app supplied none (it used to reset every user-defined validator's label to English, clobbering catalog translations). Render-side: the aria-live announcement joins per-message texts with a sentence separator (raw `textContent` concatenated with NO separator — a screen reader received one run-on string), and two rules resolving to byte-identical text render ONCE (the dev inspector still records every error key). All browser-bundled — consumers RE-BAKE at pickup; apps that translated only observable keys will see those messages switch from English to their language. **Transport-layer settle — a submit that never reaches the server now reports (#B447).** An XHR settling at `readyState` 4 with `status` 0 — network down, connection refused, DNS gone, the server process restarting — used to emit NOTHING: the success arm tests `/^2/.test(xhr.status)`, the error arm was `else if (xhr.status != 0)` which EXCLUDES 0, the arm that would have caught 0 sat fully commented out, and there was no trailing `else` — so both `error.<id>` and `error.<id>.hform` were unreachable for exactly the failure `data-gina-form-event-on-submit-error` most needs to cover. The `loadend` fail-safe still released the submit trigger, so the form spun, re-enabled, and said nothing. Both events now fire with `{ status: 408, transportError: true, error: <text> }`: 408 keeps `status >= 400` handlers working, and `transportError` is what distinguishes this from a genuine timeout (which emits a bare 408). The result stashes to `$form.eventData.error`, matching the three sibling arms in the same readyState chain and the key the readers actually consult — NOT the `on*` keys `ontimeout`/`onprogress` write, which are measured write-only (0 readers each) and are never cleared by the pre-validation `delete`. ⚠️ `xhr.timeout` is still never assigned, so the two `ontimeout` handlers remain dead code (#B448, LOW, on-demand — closing it means inventing a client-side timeout config surface that does not exist today); a real network timeout lands on THIS arm instead, which is why #B448 is NOT a coverage hole. Browser-bundled ⇒ consumers RE-BAKE at pickup. Tests: `validator-transport-error.spec.js` (arm 01 is a POSITIVE CONTROL — a 500 MUST fire the error event, without which arm 02's silence proves nothing; the `error.<id>.hform` channel is deliberately NOT asserted, because declaring the attribute flips `listenToXhrEvents` on and would change the shared fixture's scene) + `validator-autocomplete-interception.test.js` / `validator-livecheck-stale-error-clear.test.js` (extracted-real-bytes + red-first dist pins — Closure De-Morgans the chord bail, so minified pins target property-name shapes, never var names) + the #B332-cluster e2e matrix (incl. the field-order arm: a clean form whose query field is NOT last must send exactly once, after the settle).
|
|
1015
1015
|
302. **The `stream` storage strategy (`lib/storage/src/local-stream`, #STO1) — large sequential media with RESUMABLE uploads.** Keys name an ASSET, not a file: `assets/<ulid>/original<ext>` — `original` is the reserved base-rendition name and the per-asset directory reserves the namespace future renditions land in (no rendition API in this release). Keys stay OPAQUE; never parse the directory back out. What per-asset colocation buys is OPERATIONAL grouping (one `rm -rf` per asset, one `rsync` of a subtree), NOT physical locality — a directory does not place extents, the allocator does — and gina neither prevents nor CAN prevent fragmentation: Node exposes no `fallocate` binding, native addons are not shipped, and `ftruncate` yields a fully SPARSE file (measured: a 256MB truncate reserves 0 blocks, and truncating past the free space succeeds, so it delivers neither anti-fragmentation nor an early ENOSPC). Contiguity is a filesystem/volume concern (XFS extent hints and the like); `chunkSize` (default `'8MB'`, `parseSize`) is the write stream's `highWaterMark` and nothing more. **Five capability-gated verbs beyond the shared contract** (`capabilities.resumable` true — branch on the flag, never probe): `createUpload(meta, cb)` -> `{uploadId, chunkSize, expectedSize}` / `writeSegment(uploadId, offset, stream, cb)` -> `{offset, length, received}` / `statUpload(uploadId, cb)` -> `{expectedSize, contentType, originalName, createdAt, received[], missing[], complete}` / `finalize(uploadId, cb)` -> `{key, size, contentType}` (put()'s shape) / `abortUpload(uploadId, cb)` -> `{aborted}`, idempotent. `statUpload` is NOT optional — resume is impossible unless a client can learn what landed; it is the resumable twin of `stat()`, and being computed from the session's own files it stays accurate across a process restart with no in-memory session table. **`expectedSize` is REQUIRED at create**: without a declared total finalise cannot verify coverage and `statUpload` cannot say what is MISSING (only what arrived), so a caller who does not know the size uses `put()` — the same exclusion tus takes without its defer-length extension, and strictly more useful than S3 multipart's `ListParts`, which can never name a missing part because it never learns the total. The declared size also permits an early `maxObjectSize` refusal before any byte moves, **advisory only**: it is client-supplied, so the running byte counter in `writeSegment` (which refuses a segment running past the declared end) is the real enforcement. **The KEY is minted at `createUpload` and persisted in the manifest**, never invented at finalise — minting it late means a row write that fails after the bytes are published leaves them at an address nobody holds, an unreachable orphan PLUS a session that can never finalise again. **The filesystem IS the manifest**, and the seam gains ZERO new verbs: a session is `<root>/.uploads/<uploadId>/` holding `data` (assembled IN PLACE via positional writes, `flags:'r+'` + `start`, so segments may arrive out of order and no assembly pass runs at finalise — measured, a segment-per-file layout costs ~1.9 s/GB of extra concat I/O where this costs ~4ms), `meta.json` (written ONCE through temp+rename) and one zero-byte `<offset>-<length>.ok` marker per DURABLE range. `.uploads` is a dot segment, structurally uncollidable with any caller key (the key guards reject dot segments), like `.tmp` and the `.driver` stamp. The argument for that home is FATE-SHARING, not seam economy: `fsync(data)` happens-before the marker is created, which is only enforceable while both live on the same medium — a store-backed manifest (SQLite WAL, redis, couchbase) can survive a crash the bytes did not, and a manifest claiming a range the bytes lost passes coverage and publishes ZEROS. **Coverage is an INTERVAL UNION, and that is the whole safety argument.** A hole in a sparse file reads back as ZEROS rather than erroring (measured), so an unverified finalise publishes a plausible, silently corrupt object; and SUMMING marker lengths is not enough — markers `0-800` and `400-1200` sum to exactly a declared 1600 while `1200-1600` is uncovered. So finalise sorts the markers, merges overlapping AND adjacent spans, and requires exactly one span equal to `[0, expectedSize)`. A gap is a REAL error and the session is PRESERVED so the client can complete it, never a silent publish; the message names the first gap. A segment that ends early is not an error — its marker records the RECEIVED length only, so the union stays exact and re-sends may overlap freely (re-sending a covered range is idempotent). Two further finalise guards: a data file SHORTER than the declared size under a full marker set refuses the publish (marker/bytes desync — the one shape that would still publish zeros), and one LONGER is truncated back (unreachable through the API, so it means tampering). **`finalize` is IDEMPOTENT with a heal path**: if the final path already exists it skips assembly, (re)writes the row and cleans the session — so a finalise that published the bytes and then failed on the row is completed by simply retrying it. Bytes are published BEFORE the row deliberately (the inverse manufactures rows-without-files, which cas's verify classifies as loss evidence and never auto-fixes); the residue here is the benign kind — `get()` falls through to the file path when no row exists. **Durability: `fsync` defaults TRUE, and only the DATA fsync is load-bearing** — per segment the order is positional write -> `fsync(data)` -> create marker, so a surviving marker implies durable bytes; the marker's own durability is deliberately NOT guaranteed, since a lost marker beside present bytes costs a re-send, which is the safe direction. Measured: a process SIGKILLed after an un-fsynced write still yields the full bytes to a later reader (the page cache carries them), so markers stay honest across PROCESS death without fsync — only power loss or a kernel panic opens the window. The cost is a RANGE, not one flattering figure: ~12ms per 8MB segment is ~2% of network-paced ingest at 100 Mbps, ~19% at 1 Gbps, and fsync-DOMINANT at 10 Gbps, so a LAN-ingest consumer sets `fsync:false` and accepts the documented window. The parent-directory flush is best-effort (Windows and some network mounts cannot), and macOS honours fsync as the platform defines it — the `F_FULLFSYNC` caveat cas already documents. **Abandoned sessions get their OWN age-gated sweep** on two triggers — at driver BUILD (what reclaims whatever a crashed process left behind) and on an unref'd `setInterval` — because sessions legitimately live for DAYS where a crashed put()'s temp is dead immediately. A REFUSED put() (over `maxObjectSize`, or a source that errors) does not rely on that sweep at all: its cleanup waits for the write stream's `'close'` and the CALLBACK WAITS WITH IT, so a caller told the put failed can never observe residue (#B358). Do not "simplify" that back to a synchronous `existsSync`-gated unlink — `fs.createWriteStream()` opens ASYNCHRONOUSLY, so a refusal raised from the `'data'` handler runs while the `open(2)` is still in the threadpool: `existsSync` reads false, the unlink is skipped, and the worker thread creates the file a tick later, with NO event-loop turn in between for a synchronous check to catch it (measured ~30% orphan rate before the fix). **ALL THREE local strategies carry this fix** — `sharded` and `cas` build their write stream lazily inside `spill()`, so their window opens on the OTHER arm (a source error arriving just after the spill, rather than a `maxObjectSize` refusal), which is exactly how a first pass that measured only the refusal arm wrongly concluded they were immune. Fault-injecting the single false `existsSync` answer orphans a real temp on both. Two arms reach this handler — the `'data'` refusal and `stream.on('error')` — and a measurement covering one licenses no claim about the other. `sessionTtl` default `'24h'`, `sessionSweepInterval` default `'1h'`, both `parseDuration` (unit required); `'0s'` disables the interval only, and a ZERO ttl snaps back to the default rather than meaning "off", since it would reclaim in-flight uploads. Liveness is `mtime(data)`, NOT the session directory's — `data` advances on every segment write while a directory mtime only moves when entries are added or removed — with a dir-mtime fallback so a session whose data was renamed away by a half-finished finalise stays reclaimable. The same pass reclaims `<root>/.tmp` orphans from crashed `put()`s at the same cutoff (sharded has that gap and no such sweep). **No tiering, no hashing, by design**: `inlineThreshold` and `hash` are NOT in `STRATEGY_KEYS.stream`, so setting either warns as an ignored key rather than silently doing nothing, and `capabilities` reads `{offload:false, ranges:true, dedup:false, resumable:true, inline:false}`. `release()` unlinks the object and then rmdirs the asset directory, which succeeds only while it is empty — a future rendition beside `original` correctly keeps it. **Not in this release, each still deferred or capability-gated**: a rendition API; async post-hoc checksums; an `s3` adapter; and any `storage:gc`/`storage:verify`/stats extension for stream — those stay cas-only, so a stream driver has NO `sweepNow`/`verify` and is named-and-skipped by the maintenance surface rather than erroring. Multi-process is DOCUMENTED, not enforced: parallel segment writes are safe when ranges are DISJOINT (measured — two uncoordinated processes interleaving 1MB stripes into one shared file produced byte-exact output on a POSIX-coherent local filesystem), but relaxed-coherence network mounts are UNMEASURED, so route one session's segments to one process there. Shipping `stream` also EMPTIED `DEFERRED_STRATEGIES` — the list and its boot branch stay (cheap, and the next designed-before-implemented name inherits a working message) but are now exercised by nothing, which is recorded rather than hidden.
|
|
1016
1016
|
303. HTTP Range serving for stored objects — `self.serveFromStorage(driverName, key[, opts])` is the read-side companion of `self.store()`'s driver routing and the consumer `capabilities.ranges` was waiting for: a controller action serves a stored object with the whole HTTP protocol dance owned by the framework, identically on both engines (headers and status ride the response object, bytes ride `renderStream`'s two arms — no `/_gina` endpoint involved). The flow is `stat()`-gated: an unknown or released key answers 404 through `throwError` (cas hides zero-ref rows from `stat`, so a released blob 404s by construction; on `stream` a finalize-heal residue whose metadata row write failed is readable at the driver but 404s here until the idempotent finalize heals the row), and a missing/unconfigured DRIVER is an app config error — 500, never 404. Validators are minted from the layer's own invariants: `ETag: "<key>"` is deliberately STRONG because storage keys are immutable (every strategy publishes via temp+rename, cas is content-addressed, and no in-place mutation API exists), plus `Last-Modified` from the publish time; `If-None-Match` is weak-compared against that ETag (a `W/`-prefixed echo matches) and answers 304 with no driver read. Range evaluation is GET-only and capability-gated (`driver.capabilities.ranges` false ⇒ the header is ignored and no `Accept-Ranges` is advertised — the forward seam for offloading adapters): a single `bytes=a-b`/`a-`/`-n` that is satisfiable answers 206 with `Content-Range: bytes a-b/<size>` and an exact `Content-Length`; an unsatisfiable one (start at or past the size, a `-0` suffix, any range against an empty object) answers 416 with `Content-Range: bytes */<size>` and an empty body; multi-range lists, foreign units, syntactic garbage and `a-b` with a>b are IGNORED into the full 200, which RFC 9110 sanctions; `If-Range` honours the Range only on an exact validator match, so anything unevaluable degrades fail-safe to the full body rather than risking a corrupted client-side assembly. Trust model, fail-closed: the stored contentType is UPLOADER-supplied verbatim, so without `opts.contentType` the active-content types (html/xml/svg/javascript/ecmascript) downgrade to `application/octet-stream` — nosniff cannot stop a DECLARED `text/html` from rendering — and every facade response carries `X-Content-Type-Options: nosniff`; an explicit `opts.contentType` is the app's informed choice and serves verbatim. Caching defaults to `Cache-Control: private, max-age=31536000, immutable` (correct because a key's bytes can never change; `private` keeps shared caches out of the application's authorization), with `opts.cacheControl` winning verbatim; `opts.download`/`opts.filename` emit an RFC 6266 attachment disposition with control characters stripped from the uploader-supplied name (header-injection guard — Node's setHeader would otherwise THROW on a CR/LF-bearing stored originalName, inside an async callback). HEAD answers headers-only with full-size accounting and no driver read. Headers are applied only in the driver-success callback, so a read error routes through `throwError` with no Range headers leaked; a post-stat race (cas grace expiry mid-download, a vanished file) maps the read verbs' `STORAGE_NO_OBJECT` code to 404 and anything else to 500 — the machine codes (`STORAGE_NO_OBJECT`/`STORAGE_RANGE_UNSATISFIABLE`/`STORAGE_INVALID_RANGE`, minted by `util.codedError` with message wording byte-unchanged) shipped for exactly this discrimination. The enablers landed in the same release on `renderStream`: non-SSE Buffer chunks pass through byte-exact (the historical unconditional utf8 decode substituted U+FFFD into binary payloads, which made the delegate unusable for byte serving; valid-UTF-8 re-encodes identically and SSE keeps its decode), HEAD parity (headers-only, the iterable never consumed, a destroyable source destroy()ed), caller-set headers surviving the delegate defaults (the h2 frame literal used to beat the pending merge and the h1 setHeader clobbered — a serving caller's Cache-Control/Content-Range now ride), and the post-end `headersSent` bookkeeping guarded (getter-only on both real response classes under the file's strict mode — the swallowed throw had killed the Inspector Flow stream-write/total timeline entries on every streamed request). Deliberately NOT here: multipart/byteranges (multi-range ⇒ full 200), Range on statics (the pure `_parseRangeHeader(header, size)` helper is reusable when that arc comes), and Range on non-GET methods. Offload serving SHIPPED with the s3 adapter (#304): `capabilities.offload` ⇒ GET/HEAD answer 307 to a presigned URL after the local 304 check, and the provider serves Range natively. Tests: `test/core/serve-from-storage.test.js` (parser matrix + fail-closed contentType matrix as extract-and-execute of the shipped bytes; the protocol arms driven end-to-end on REAL drivers across sharded/cas/stream with byte-exact 200/206 assertions) + `test/core/render-stream.test.js` §13-§16 + `test/lib/storage-error-codes.test.js`.
|
|
1017
1017
|
304. **s3 storage adapter (#STO1 — the layer's last step)** — the second ADAPTER: `adapter:'s3'` + `bucket` (+ optional `region`/`endpoint` (https:// prepended when scheme-less; region defaults `us-east-1` ONLY beside a custom endpoint — real AWS keeps the SDK's own chain, whose missing-region message beats a mis-signed default)/`prefix` (trailing `/` normalised; the OPAQUE key EXCLUDES it, so re-pointing `prefix` orphans stored objects exactly as re-pointing a local `root` does)/`forcePathStyle` (MinIO)/`presignExpiry` ('15m')/`sweepGrace` ('1h')/`maxObjectSize`/static credentials). Dispatch became adapter × strategy by COMPOSITION — `ADAPTER_FACTORIES = {local: FACTORIES, s3: {sharded}}` — so the flat `_FACTORIES` stays the local row (the `storage:*` CLI dispatches through it offline and source-pins it; all three CLI verbs name s3 drivers remote and SKIP them BEFORE any build, ahead of the store skip so a warn-ignored `store` key cannot mislead the reason). s3 runs `sharded` ONLY — key naming; the provider owns placement — and refuses `cas`/`stream` at boot naming why (S3 ETags are not content digests; multipart is the provider's own resumable); `strategy` may be omitted (defaults sharded). STORELESS: `store=null`, no embedded `.meta.db`, no `.driver` stamp — one PutObject atomically carries bytes + ContentType (verbatim) + `x-amz-meta-gina-name` = encodeURIComponent(originalName) capped ~1KB (URI-encoded OURSELVES — AWS's RFC 2047 handling of non-ASCII metadata values is AWS-specific, plain ASCII round-trips on every compatible; AWS-doc-measured: user metadata 2KB total, keys lowercased, IMMUTABLE after upload, matching the layer's immutable keys). `stat()` = HeadObject (strongly consistent per AWS docs; ⚠️ a missing key answers 404 only WITH `s3:ListBucket`, else 403 — the guide ships the minimal policy: GetObject/PutObject/DeleteObject/ListBucket/ListBucketMultipartUploads/AbortMultipartUpload, bucket/prefix-scoped). `release()` = DeleteObject (idempotent acknowledgement — S3 reports no prior existence, so `existed:true` is DOCUMENTED as ack-only). `put()` streams through lib-storage's `Upload` (unknown length ⇒ multipart under the hood) behind a counting transform enforcing `maxObjectSize` MID-STREAM (breach ⇒ best-effort `Upload.abort()` + the REAL cap error; reported size = the meter's count — the bytes the provider acknowledged). The crashed-put analog is an INCOMPLETE MULTIPART UPLOAD (bills storage until aborted, no expiry — AWS-doc-measured): a build-time age-gated sweep (ListMultipartUploads → AbortMultipartUpload past `sweepGrace`, one bounded page per boot, best-effort never-throw — the sharded temp-orphan shape) reclaims them; a bucket `AbortIncompleteMultipartUpload` lifecycle rule stays recommended defense-in-depth. `resolve(key[, opts], fn)` answers the reserved `{kind:'url', url, expiresAt}` — a presigned GetObject; presigning is LOCAL SigV4 computation, so existence is deliberately NOT verified (the serving facade is stat-gated; a direct caller's URL to a vanished object answers the provider's own 404 — documented, not hidden). The NEW optional middle `opts` ({contentType, download, filename, cacheControl}) rides the SIGNED `response-content-type`/`-content-disposition`/`-cache-control` overrides (signed-request-only per AWS — a presigned URL qualifies) and landed on the CONTRACT for all strategies: locals accept and IGNORE it (zero prior resolve() callers, measured), so one facade call site serves every driver. `capabilities` = `{offload:true` (the layer's FIRST true — and its first reader ships beside it), `ranges:true` (get/getRange are REAL GetObject(+Range) proxies: provider InvalidRange/416 → STORAGE_RANGE_UNSATISFIABLE, NoSuchKey/NotFound → STORAGE_NO_OBJECT, anything else RAW-forwarded — the typed class is the signal), `dedup:false, resumable:false, inline:false}`. `serveFromStorage()` offload path: after the stat gate AND the local If-None-Match 304 (key ETag — no presign spent), GET/HEAD → **307** (method-preserving — HEAD stays HEAD) + Location + `Cache-Control: private, no-store` on the REDIRECT (it must never outlive its signature) while the PAYLOAD's policy (default `private, max-age=31536000, immutable`; `opts.cacheControl` wins) rides `response-cache-control`; the facade runs its fail-closed contentType DOWNGRADE BEFORE presigning and hands resolve() the SAFE type — the stored-XSS guard holds when the provider serves the bytes; `opts.offload:false` forces the in-process proxy path; an offload-claiming driver whose resolve answers no url is a LOUD 500 (driver contract violation), never a silent degrade; non-GET/HEAD methods always proxy. SDK = 3 PROJECT-side packages (`@aws-sdk/client-s3` + `@aws-sdk/lib-storage` + `@aws-sdk/s3-request-presigner`; v3 required — a module without S3Client is refused by name) resolved LAZILY through `start()`'s injected `requireProjectModule` (built in gna.js from `getPath('project')` — lib/storage is framework-independent, test-enforced, and cannot reach the globals; tests inject a fake SDK via the factory's 4th `deps` arg); a missing SDK on a CONFIGURED driver is boot-FATAL with the npm hint (the no-safe-degraded-mode rationale). `${secret:KEY}` works for the static credential pair TODAY (the whole-tree config walk); omit both credentials and the SDK default provider chain runs (env / shared config / IMDS / IRSA — the container-native path, first-class). S3-compat honesty: strong read-after-write consistency (HEAD + LIST included) and the metadata semantics are AWS-DOCUMENTED behaviour; a compatible provider owns its own — stated in the guide, and the adapter uses only the core compat surface (Put/Get/Head/Delete/ListObjectsV2/ListMultipartUploads/Abort + SigV4 presigning). Tests: `test/lib/storage-s3.test.js` (REAL adapter + fake SDK that THROWS on unimplemented commands, 60) + `test/core/serve-from-storage-offload.test.js` (17 — incl. the signed-downgrade security pin and 304-before-presign).
|
|
@@ -1039,4 +1039,4 @@ Dev-mode query instrumentation captures every database query tied to the current
|
|
|
1039
1039
|
318. **Validator AJAX bodies are JSON — the Content-Type must say so, on every sender (#FORMCT; query path fixed in #B425).** The browser validator serializes its request bodies with `JSON.stringify`, but its shared `xhrOptions` defaults hardcode `Content-Type: application/x-www-form-urlencoded; charset=UTF-8` (that literal appears exactly once in the whole shipped bundle). When a JSON body goes out under that label the gina server correctly honours it and runs `body.replace(/\+/g, ' ')` plus `decodeURIComponent` BEFORE `JSON.parse` — so a value such as an email `+alias` arrives with the `+` turned into a space. **The failure is silent by construction:** the mangled body is still well-formed JSON, so it parses, no error is raised, and the endpoint answers `200` with a wrong answer. The server half is NOT the bug — for a genuine urlencoded body `+` really does mean space and `%2B` survives the decode — so every fix belongs on the client, at the labelling. **Rule for any new sender: if the body is JSON, resolve the Content-Type as `explicit author declaration, else application/json` — never let the urlencoded default ride a JSON body.** The senders and their state: the form-submit non-binary path and the file-removal POST resolve it correctly (they read an explicit form `enctype` first, else `application/json`); the multipart/binary path sets its own boundary header; a bodiless branch sets a header but sends nothing, so nothing can be corrupted; and the `query` (live-check) request in `core/plugins/lib/validator/src/form-validator.js` was the last one still inheriting the default — fixed by mirroring the same resolution. **Implementation gotcha specific to the query path:** the rule author can override the header per rule (`"headers": { "Content-Type": "application/json" }` inside a rule's `query` block, a supported seam), and that override must keep winning — but the options merge folds the rule block and the defaults into one object, after which a rule-declared `urlencoded` and the inherited default are the same string in the same slot. Provenance is unrecoverable post-merge, so the rule-declared value must be captured BEFORE the merge and consulted afterwards. **Behaviour note at pickup:** live-check bodies now route to the server's parse-verbatim JSON branch, so `+` survives — and `%XX` sequences are no longer URL-decoded either. The client stringifies raw field values without percent-encoding, so no correct consumer depends on that decode, but a bundle that leaned on it will see the literal. **Merge semantics this relies on (measured, and undocumented in the merge JSDoc):** `lib/merge` is variadic — it reads `override` off the LAST argument only when that argument is a boolean, and treats every other argument as an additional source, with EARLIER sources winning and later ones filling gaps. That is why a per-rule header beats the default, and why passing a third object is a merge rather than an override flag.
|
|
1040
1040
|
319. **Controller render data — the dotted-path `set()` contract, and why a first write must never be walked (#P39 slice 2).** Inside `core/controller/controller.js`, every render is preceded by ~60 calls to a local `set('a.b.c', value)` that builds `local.userData` (`page.view.*`, `page.environment.*`, `page.forms`, `gina.csrfToken`, …). Its contract: missing intermediates are created as plain objects and an intermediate that is not a plain object (`null`, an array, a primitive) is REPLACED; at the leaf, **first write wins** (a later `set()` to an existing leaf is ignored — the `override` argument is accepted but inert), an object written onto an existing object leaf **deep-fills** it (missing keys added, existing kept) through `lib/merge`, and **a first-write value is stored by reference and is never walked, copied or mutated**. That last clause is the load-bearing one: `set('page.forms', conf.content.forms)` stores the shared per-process forms catalog, and the previous implementation — a JSON-string path builder + `JSON.parse` + a whole-subtree `merge` — recursed into every grafted object, re-merged each array inside it with itself, and wrote the copies back into the caller's object: 0.4–1.5 ms of CPU per request for a real catalog, and the catalog's arrays silently swapped for equal copies on every render. (That lib-level rewrite of a grafted source — array identity churn at every depth plus SILENT PRIMITIVE DEDUPE — was #B428, fixed in 0.6.19: `lib/merge` now skips a subtree that is the SAME object on both sides — merging a value into itself is identity — so a grafted source keeps its arrays and duplicates and the result references it; object/array references only, equal primitives keep their path; the root-absent `createMode` shallow-copy path is deliberately unchanged. Measured on a 2000-case seeded corpus: identical results and untouched sources for every non-shared merge. Two by-catches were staked out of it: #B436 — FIXED in 0.6.20: staked as 'nested arrays at depth ≥ 2 merge TWICE' (`[1,2,3,4,3,4]`), the MEASURED root was `mergeArray`'s number top-up deciding 'push again' by INDEX rather than by COUNT — any number the target already held at a shifted index was pushed again at EVERY depth (`merge([9,1],[1])` → `[9,1,1]`; `{ports:[8080,8124]}`+`{ports:[8124]}` → `[8080,8124,8124]`), and the depth-≥2 double pass (`browse()` merged arrays in its prop loop and again in the recursion) merely manufactured the shift; the top-up now compares counts (`[25]`+`[25,25]` still `[25,25]`, a present value is never pushed again, merging the same source twice is a no-op) and the prop-loop array merge runs only on the createMode path, so a nested array is merged once and an array SHARED by both sides at depth ≥ 2 keeps its identity (a #B428 residual); #B437 — FIXED in 0.6.20: a target array repeating a primitive (`['v2','v2']`) merged with an array of objects threw `TypeError: Cannot create property 'id' on string`, because `mergeArray`'s object-fill branch tested for a free index on the DEDUPED rebuild of the target and then wrote into the real target, whose index still held the string (or `null`); an occupied index is now left alone, as it already was for an object landing on one.) The rewrite assigns a first write directly and routes only genuine collisions through `merge` at the leaf, so the observable structure is identical (11,550 states differential-tested against the frozen old implementation) while the catalog is left alone; measured ×4.5 on the primitive writes and ×800+ on the catalog write. **Two generalisable traps from that work.** (1) Until 0.6.19 `lib/merge` MUTATED A SOURCE IT GRAFTED: a source subtree grafted by reference at an existing level was walked by the recursion with `src === copy`, every array inside it re-merged with itself and the copies written back into the caller's object — contents preserved, identity churned, cost proportional to the value. #B428's identity guard closed it, and the lesson stands for any merge-like helper that walks a caller's object by reference: measure identity through it before handing it an object you must keep intact. (2) `lib/merge` is VARIADIC and its copy behaviour depends on depth and shape: a value at depth 1 of a call is shallow-copied, deeper values are referenced (and until 0.6.20 a nested array was merged twice on the same level — once in the graft loop, once in the recursion). The array contract is now in its JSDoc (#B436); still read `browse()` before reasoning about identity through merge, and prefer a measured differential test over a model of it.
|
|
1041
1041
|
320. **`route.request()` settles its caller exactly once - an outbound-request failure is an error argument, never a hang and never a bundle kill (#B442, 0.6.21).** The routing helper's outbound call (`getRoute(...).request([ignoreWebRoot,] options[, cb])`) DISCARDED the `ClientRequest` returned by `http(s).get`, so nothing could ever attach an `'error'` listener to it. Three consequences, all removed: (a) a dial failure whose code is carved out of the process-level uncaughtException handler (`ECONNREFUSED`/`ECONNRESET`/`EPIPE`/...) left the caller waiting FOREVER with nothing logged - a permanent wedge in any serialized consumer (a worker at concurrency 1, a queue processor), and the SILENCE is the defect, not the carve-out; (b) a code that is NOT carved out (`ETIMEDOUT`, `EHOSTUNREACH`, `ENETUNREACH` - i.e. a peer that stops answering or a route that goes away) escalated to `uncaughtException` and took the bundle down through the process-level `dismiss(pid, 'SIGTERM')`; (c) a response stream dying mid-body assigned its error to a local and waited for an `'end'` event that never arrives, so the assigned error was never delivered either. All four exit paths now run through ONE single-settle latch (`settled` flag + `settle()`), so the callback receives an ordinary error argument - the shape consuming code already handles - and can never be invoked twice. **`options.timeout` was INERT and looked like a consumer workaround:** it does reach `http(s).get`, but Node only EMITS `'timeout'` and never destroys the socket, and the request object was unreachable, so nobody could listen or act. It is now honoured - on expiry the request is destroyed and the callback settles with an `Error` carrying `code === 'ETIMEDOUT'`; absent/falsy `options.timeout` attaches no timeout listener, so the behaviour is opt-in. The no-callback (fire-and-forget) branch attaches its own `'error'` listener and `console.warn`s, so a call nobody awaited can no longer kill the process. Same defect class as the `store()` / `copyFile` movers (#B223/#B227): capture every request or stream you create, listen on BOTH ends, and latch the settle. **`lib/routing` IS browser-bundled** (aliased in the plugin `build.json`), so this ships in `gina.min.js` as well as server-side - pickup is a bundle restart AND a re-bake. Regression test: `test/lib/routing-request-settle.test.js` - source pins plus extract-and-execute behavioural arms driving the real shipped bytes against a fake agent; note the function region is brace-UNBALANCED (a `/^\{/` regex literal in the end handler), so the extraction is terminator-anchored on `} // EO route.request()` and a brace walker must NOT be substituted for it.
|
|
1042
|
-
321. **`merge()` array-element hardening - a null (or hole) element in an id-keyed array is an element with no key, never a crash (#B436/#B437/#B443 arc).** `typeof null == 'object'`, so `mergeArray`'s collection paths historically dereferenced null elements and threw `TypeError: Cannot read properties of null` - and because every configuration overlay and collection merge travels through `lib/merge`, any such shape was a whole-merge (often whole-boot) kill. The hardened contract, in layers: the WRITE side (0.6.20) - the index-merge branch tests for a free slot on the real target, and an occupied slot (a primitive or null left where the deduped rebuild had a hole) is skipped, never written into; number top-up is by COUNT, not index, so merges are idempotent (`[25]+[25,25]` gives `[25,25]`, `[9,1]+[1]` stays `[9,1]`). The READ side (0.6.21) - both collection ENTRY guards null-check the TARGET operand (they already checked the source), both id-ROSTER loops and both WALKS skip a null element, and the per-element collection tests null-check the SOURCE operand; a null element matches nothing, blocks nothing and contributes nothing. Semantics otherwise unchanged and pinned: a target whose slots are primitives/null keeps them (`[null,null]+[{id:1},{id:2}]` returns `[null,null]`, same as `['v2','v2']+...`), and an object BEHIND a primitive/null head is dropped by the per-element branch (pre-existing, `['x',{id:9}]+[{id:1}]` returns `['x']`). `lib/merge` is browser-bundled (aliased in the plugin `build.json`), so array-merge fixes ship in `gina.min.js` too - pickup is a bundle restart AND a re-bake. Regression suites: `test/lib/merge-b436.test.js`, `merge-b437.test.js`, `merge-b443.test.js` (behavioural arms driving the real lib + comment-stripped source pins + artifact-derived dist pins, all red-first validated).
|
|
1042
|
+
321. **`merge()` array-element hardening - a null (or hole) element in an id-keyed array is an element with no key, never a crash (#B436/#B437/#B443 arc).** `typeof null == 'object'`, so `mergeArray`'s collection paths historically dereferenced null elements and threw `TypeError: Cannot read properties of null` - and because every configuration overlay and collection merge travels through `lib/merge`, any such shape was a whole-merge (often whole-boot) kill. The hardened contract, in layers: the WRITE side (0.6.20) - the index-merge branch tests for a free slot on the real target, and an occupied slot (a primitive or null left where the deduped rebuild had a hole) is skipped, never written into; number top-up is by COUNT, not index, so merges are idempotent (`[25]+[25,25]` gives `[25,25]`, `[9,1]+[1]` stays `[9,1]`). The READ side (0.6.21) - both collection ENTRY guards null-check the TARGET operand (they already checked the source), both id-ROSTER loops and both WALKS skip a null element, and the per-element collection tests null-check the SOURCE operand; a null element matches nothing, blocks nothing and contributes nothing. Semantics otherwise unchanged and pinned: a target whose slots are primitives/null keeps them (`[null,null]+[{id:1},{id:2}]` returns `[null,null]`, same as `['v2','v2']+...`), and an object BEHIND a primitive/null head is dropped by the per-element branch (pre-existing, `['x',{id:9}]+[{id:1}]` returns `['x']`). `lib/merge` is browser-bundled (aliased in the plugin `build.json`), so array-merge fixes ship in `gina.min.js` too - pickup is a bundle restart AND a re-bake. Regression suites: `test/lib/merge-b436.test.js`, `merge-b437.test.js`, `merge-b443.test.js` (behavioural arms driving the real lib + comment-stripped source pins + artifact-derived dist pins, all red-first validated). **#B446 — the same `for...in` loops are also a prototype-pollution sink.** `merge()` enumerated SOURCE keys with bare `for...in` at five sites (`:136`, `:197`, `:208`, `:237`, `:547` pre-fix; the lone `hasOwnProperty` at `:548` guards the TARGET, not the source), so an OWN `__proto__` key — exactly what `JSON.parse` yields, and what `core/server.js:6657` feeds it on the PUT path — walked through to `Object.prototype`. ⚠️ Subtler still: merge reads `src = target[name]` THROUGH the prototype chain, so with its documented target-wins-on-conflict rule a polluted value BEAT a default the source literal owned (measured: `rejectUnauthorized` flipping true→false on the query-options defaults). All five loops now reject `__proto__`/`constructor`/`prototype` BY NAME — that name check is the whole guard, and it is REQUIRED because own-property checking alone cannot see a JSON-parsed `__proto__` (an own key satisfies an own check by construction). ⚠️ The first cut ALSO skipped non-own keys as extra hardening and that was REVERTED before release: `lib/model.js` setModel round-trips entity instances through `merge()`, and all six SQL connectors attach their `.sql`-derived query methods to the PROTOTYPE, so own-only copying silently dropped every query method (`db.<x>Entity.getOneById is not a function`) while constructor-assigned methods survived — a partial, hard-to-attribute breakage caught by a consumer on the unpublished tree, never shipped. `merge()` walks the prototype chain, as it always has; only the three key names are refused. Regression suite: `test/lib/merge-entity-prototype.test.js` (red-first validated against the own-guard build). The `src`-read is deliberately left prototype-reachable — closing it would change documented conflict semantics for objects that legitimately inherit, and is tracked separately.
|