@bongos/core 1.18.3 → 1.18.18
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/.bongos-core.json +1575 -285
- package/.claude/skills/builder-box/SKILL.md +6 -0
- package/.claude/skills/builder-sequence/SKILL.md +97 -0
- package/.claude/skills/builder-ship/SKILL.md +8 -2
- package/.claude/skills/builder-start/SKILL.md +11 -0
- package/.claude/skills/builder-sync/SKILL.md +41 -0
- package/.claude/skills/fix-task/SKILL.md +64 -0
- package/.claude/skills/goal-create/SKILL.md +158 -0
- package/.claude/skills/goal-review/SKILL.md +3 -1
- package/.claude/skills/idea-triage/SKILL.md +14 -8
- package/.claude/skills/merge-mode/SKILL.md +2 -2
- package/.claude/skills/new-project/SKILL.md +1 -0
- package/.claude/skills/planning-session/SKILL.md +45 -18
- package/.env.local.example +4 -3
- package/README.md +4 -2
- package/bin/bongos.js +1 -0
- package/clients/bongos-client/README.md +2 -2
- package/clients/bongos-client/bongos-client.global.js +82 -18
- package/clients/bongos-client/index.cjs +82 -18
- package/clients/bongos-client/index.d.ts +131 -29
- package/clients/bongos-client/index.mjs +82 -18
- package/config/branding.neutral.json +6 -1
- package/config/governance.neutral.json +21 -0
- package/docs/adr/0043-git-ssh-trust-boundary-and-rank-floor-on-permission-paths.md +24 -0
- package/docs/adr/0090-metic-task-authoring.md +1 -1
- package/docs/adr/0144-devbox-rehome-onto-cloudbongos-plane.md +41 -0
- package/docs/adr/0144-federated-single-logout-backchannel.md +42 -0
- package/docs/adr/0145-devbox-app-branding-driven-module.md +44 -0
- package/docs/adr/0145-free-hosted-project-tier-isolation-and-domain-separation.md +62 -0
- package/docs/adr/0146-configurable-reward-mode-cost-plus-only.md +53 -0
- package/docs/adr/0147-standalone-live-docs-regen.md +39 -0
- package/docs/adr/0148-task-scoped-box-source-access.md +48 -0
- package/docs/adr/0149-bongos-dev-local-launcher.md +57 -0
- package/docs/adr/0150-box-first-boot-bringup-vendored-instances.md +68 -0
- package/docs/adr/0151-governance-permissions-as-atom-ranks-as-roles.md +229 -0
- package/docs/adr/0152-landing-gate-served-by-the-core-app.md +92 -0
- package/docs/adr/0152-metic-task-abandonment.md +67 -0
- package/docs/adr/0153-sequential-chain-runner.md +51 -0
- package/docs/adr/0154-goal-owner-criterion-authoring.md +86 -0
- package/docs/adr/README.md +10 -0
- package/docs/api/openapi.json +843 -17
- package/docs/api-reference.md +23 -10
- package/docs/architecture.md +4 -2
- package/docs/branding-contract.md +3 -1
- package/docs/canonical-permissions.md +21 -8
- package/docs/file-map.md +8 -0
- package/docs/onboarding/diagrams/01-task-lifecycle.mmd +2 -2
- package/docs/onboarding/diagrams/02-rank-ladder.mmd +3 -1
- package/docs/onboarding/diagrams/05-versioning.mmd +1 -3
- package/docs/onboarding/diagrams/assertions.json +4 -7
- package/docs/onboarding/drachmae-and-karma.md +3 -3
- package/docs/onboarding/primer.md +17 -16
- package/docs/onboarding/slash-commands.md +124 -0
- package/docs/recipes/builder-box-lifecycle.md +23 -0
- package/docs/recipes/devbox-app-release.md +14 -4
- package/docs/recipes/instance-first-run.md +16 -0
- package/docs/recipes/local-dev.md +81 -0
- package/docs/recipes/ops-gotchas.md +19 -1
- package/docs/recipes/overnight-builder.md +1 -1
- package/docs/recipes/parallel-goal-waves.md +174 -0
- package/docs/recipes/ship-and-regrade.md +2 -2
- package/docs/recipes/standalone-live-docs.md +48 -0
- package/docs/recipes/work-on-a-task-from-your-dev-box.md +108 -0
- package/migrations/003_pms.sql +6 -7
- package/migrations/006_pms_v2.sql +3 -31
- package/migrations/core_193_task_needs_rebase.sql +28 -0
- package/migrations/core_194_confirmed_backlog_clawback.sql +63 -0
- package/migrations/core_195_idea_triage_provenance.sql +57 -0
- package/migrations/core_196_session_reward_topup.sql +69 -0
- package/modules/autonomy/pollers/routine-timers.js +9 -8
- package/modules/builder-settings/wandering-prefs.js +61 -18
- package/modules/dev-box/app/README.md +73 -0
- package/modules/dev-box/app/assets/tray/mac-alertTemplate.png +0 -0
- package/modules/dev-box/app/assets/tray/mac-alertTemplate@2x.png +0 -0
- package/modules/dev-box/app/assets/tray/mac-busyTemplate.png +0 -0
- package/modules/dev-box/app/assets/tray/mac-busyTemplate@2x.png +0 -0
- package/modules/dev-box/app/assets/tray/mac-offTemplate.png +0 -0
- package/modules/dev-box/app/assets/tray/mac-offTemplate@2x.png +0 -0
- package/modules/dev-box/app/assets/tray/mac-onTemplate.png +0 -0
- package/modules/dev-box/app/assets/tray/mac-onTemplate@2x.png +0 -0
- package/modules/dev-box/app/assets/tray/win-alert.png +0 -0
- package/modules/dev-box/app/assets/tray/win-busy.png +0 -0
- package/modules/dev-box/app/assets/tray/win-off.png +0 -0
- package/modules/dev-box/app/assets/tray/win-on.png +0 -0
- package/modules/dev-box/app/build-resources/dmg-background.png +0 -0
- package/modules/dev-box/app/build-resources/dmg-background@2x.png +0 -0
- package/modules/dev-box/app/build-resources/icon.png +0 -0
- package/modules/dev-box/app/electron-builder.tmpl.yml +95 -0
- package/modules/dev-box/app/electron-builder.yml +95 -0
- package/modules/dev-box/app/package-lock.json +5291 -0
- package/modules/dev-box/app/package.json +30 -0
- package/modules/dev-box/app/renderer/app.js +319 -0
- package/modules/dev-box/app/renderer/index.html +45 -0
- package/modules/dev-box/app/renderer/styles.css +246 -0
- package/modules/dev-box/app/renderer/theme.generated.css +16 -0
- package/modules/dev-box/app/scripts/apply-dmg-backdrop.sh +82 -0
- package/modules/dev-box/app/scripts/dmgbuild-settings.py +39 -0
- package/modules/dev-box/app/scripts/gen-brand.js +138 -0
- package/modules/dev-box/app/scripts/gen-dmg-backdrop.py +323 -0
- package/modules/dev-box/app/scripts/gen-icons.js +286 -0
- package/modules/dev-box/app/scripts/vendor-client.js +23 -0
- package/modules/dev-box/app/src/box-controller.js +287 -0
- package/modules/dev-box/app/src/brand.generated.json +13 -0
- package/modules/dev-box/app/src/brand.js +16 -0
- package/modules/dev-box/app/src/config.js +19 -0
- package/modules/dev-box/app/src/gds-client.js +90 -0
- package/modules/dev-box/app/src/main.js +610 -0
- package/modules/dev-box/app/src/pairing.js +57 -0
- package/modules/dev-box/app/src/preload.js +29 -0
- package/modules/dev-box/app/src/session-store.js +58 -0
- package/modules/dev-box/app/src/ssh-setup.js +569 -0
- package/modules/dev-box/app/src/task-picker.js +70 -0
- package/modules/dev-box/app/src/tray.js +99 -0
- package/modules/dev-box/app/src/updater.js +55 -0
- package/modules/dev-box/app/src/vendor/bongos-client.cjs +698 -0
- package/modules/dev-box/app/test/unit.mjs +526 -0
- package/modules/dev-box/box-access.js +121 -4
- package/modules/dev-box/boxes.js +20 -0
- package/modules/dev-box/module.json +1 -1
- package/modules/dev-box/routes/box.js +87 -3
- package/modules/economy/credits.js +116 -25
- package/modules/economy/reward.js +8 -0
- package/modules/economy/streak.js +2 -1
- package/modules/governance/CLAUDE.md +15 -0
- package/modules/governance/catalog.js +419 -0
- package/modules/governance/config.js +174 -0
- package/modules/governance/db.js +125 -0
- package/modules/governance/migrations/governance_001_tables.sql +81 -0
- package/modules/governance/migrations/governance_002_seed_and_backfill.sql +103 -0
- package/modules/governance/migrations/governance_003_task_abandon_metic.sql +28 -0
- package/modules/governance/migrations/governance_004_criterion_create.sql +39 -0
- package/modules/governance/migrations/governance_005_resync_builder_rank_roles.sql +56 -0
- package/modules/governance/module.json +10 -0
- package/modules/governance/protected-surfaces.js +88 -0
- package/modules/governance/protected-surfaces.json +80 -0
- package/modules/governance/rank-roles.js +83 -0
- package/modules/governance/resolver.js +221 -0
- package/modules/governance/routes/governance.js +89 -0
- package/modules/governance/routes/role-sync.js +49 -0
- package/modules/governance/session-scopes.js +127 -0
- package/modules/governance/session-scopes.json +38 -0
- package/modules/hall-ui/public/atlas.css +117 -0
- package/modules/hall-ui/public/atlas.html +3 -114
- package/modules/hall-ui/public/brand-apply.js +34 -6
- package/modules/hall-ui/public/builders.js +187 -219
- package/modules/hall-ui/public/diagrams.html +2 -0
- package/modules/hall-ui/public/dom-utils.js +27 -0
- package/modules/hall-ui/public/drachmae.html +2 -0
- package/modules/hall-ui/public/drachmae.js +54 -1
- package/modules/hall-ui/public/gate.css +98 -0
- package/modules/hall-ui/public/gate.html +3 -95
- package/modules/hall-ui/public/goal-inbox.js +4 -1
- package/modules/hall-ui/public/goals-page.js +626 -114
- package/modules/hall-ui/public/goals.css +79 -1
- package/modules/hall-ui/public/goals.html +38 -11
- package/modules/hall-ui/public/hall-widgets.js +62 -52
- package/modules/hall-ui/public/harbor.css +123 -0
- package/modules/hall-ui/public/harbor.html +3 -120
- package/modules/hall-ui/public/index.html +11 -4
- package/modules/hall-ui/public/instances-panel.js +4 -1
- package/modules/hall-ui/public/modules.css +63 -0
- package/modules/hall-ui/public/modules.html +3 -60
- package/modules/hall-ui/public/new-project-wizard.js +7 -3
- package/modules/hall-ui/public/not-ready.html +2 -0
- package/modules/hall-ui/public/pair.css +73 -0
- package/modules/hall-ui/public/pair.html +3 -70
- package/modules/hall-ui/public/palette.js +302 -0
- package/modules/hall-ui/public/primer.css +13 -0
- package/modules/hall-ui/public/primer.html +2 -0
- package/modules/hall-ui/public/profile.html +2 -0
- package/modules/hall-ui/public/ranks.html +2 -0
- package/modules/hall-ui/public/roadmap.css +179 -0
- package/modules/hall-ui/public/roadmap.html +9 -172
- package/modules/hall-ui/public/roadmap.js +329 -316
- package/modules/hall-ui/public/sessions.css +16 -0
- package/modules/hall-ui/public/sessions.html +3 -13
- package/modules/hall-ui/public/settings.html +2 -0
- package/modules/hall-ui/public/settings.js +4 -4
- package/modules/hall-ui/public/shell.js +143 -18
- package/modules/hall-ui/public/style.css +231 -39
- package/modules/hall-ui/public/task-detail.js +347 -26
- package/modules/hall-ui/public/task.html +6 -1
- package/modules/hall-ui/public/watch.html +2 -0
- package/modules/hall-ui/public/work.html +8 -6
- package/modules/hall-ui/public/work.js +1 -1
- package/modules/ideas/inbox.js +61 -11
- package/modules/ideas/routes/inbox.js +23 -0
- package/modules/lifecycle/claim-eligibility.js +72 -0
- package/modules/lifecycle/db.js +519 -139
- package/modules/lifecycle/done-when.js +18 -3
- package/modules/lifecycle/github-push.js +19 -2
- package/modules/lifecycle/goal-advisory.js +114 -0
- package/modules/lifecycle/goal-authz.js +62 -0
- package/modules/lifecycle/lifecycle.js +9 -0
- package/modules/lifecycle/publish-reconciler.js +109 -6
- package/modules/lifecycle/routes/claims.js +68 -23
- package/modules/lifecycle/routes/done-when.js +110 -15
- package/modules/lifecycle/routes/goals.js +8 -38
- package/modules/lifecycle/routes/tasks.js +38 -6
- package/modules/lifecycle/ship-card.js +6 -0
- package/modules/onboarding/onboarding-state.js +1 -1
- package/modules/platform-identity/CLAUDE.md +15 -0
- package/modules/platform-identity/hub-device.js +147 -0
- package/modules/platform-identity/hub-keys.js +151 -0
- package/modules/platform-identity/migrations/platform_identity_001_tables.sql +114 -0
- package/modules/platform-identity/migrations/platform_identity_002_client_origin_index.sql +18 -0
- package/modules/platform-identity/migrations/platform_identity_003_project_memberships.sql +42 -0
- package/modules/platform-identity/migrations/platform_identity_004_projects.sql +44 -0
- package/modules/platform-identity/migrations/platform_identity_005_builder_activity.sql +42 -0
- package/modules/platform-identity/module.json +18 -0
- package/modules/platform-identity/platform-identity.js +531 -0
- package/modules/platform-identity/pollers/code-sweep.js +43 -0
- package/modules/platform-identity/routes/my-projects.js +140 -0
- package/modules/platform-identity/routes/projects.js +107 -0
- package/modules/platform-identity/routes/scouting.js +35 -0
- package/modules/platform-identity/routes/sso.js +402 -0
- package/modules/platform-identity/tests/platform-identity.mjs +388 -0
- package/modules/platform-identity/ui/.gitkeep +0 -0
- package/modules/security/routes/reports.js +10 -0
- package/modules/sessions/db.js +166 -21
- package/modules/sessions/module.json +1 -1
- package/modules/sessions/pollers/session-reward-reconcile.js +78 -0
- package/modules/sessions/routes/sessions.js +33 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/public-landing/assets/anim/idle/frame_00.webp +0 -0
- package/public-landing/assets/anim/idle/frame_01.webp +0 -0
- package/public-landing/assets/anim/idle/frame_02.webp +0 -0
- package/public-landing/assets/anim/idle/frame_03.webp +0 -0
- package/public-landing/assets/anim/idle/frame_04.webp +0 -0
- package/public-landing/assets/anim/idle/frame_05.webp +0 -0
- package/public-landing/assets/anim/idle/frame_06.webp +0 -0
- package/public-landing/assets/anim/idle/frame_07.webp +0 -0
- package/public-landing/assets/anim/idle/frame_08.webp +0 -0
- package/public-landing/assets/anim/idle/frame_09.webp +0 -0
- package/public-landing/assets/anim/idle/frame_10.webp +0 -0
- package/public-landing/assets/anim/idle/frame_11.webp +0 -0
- package/public-landing/assets/anim/idle/frame_12.webp +0 -0
- package/public-landing/assets/anim/idle/frame_13.webp +0 -0
- package/public-landing/assets/anim/idle/frame_14.webp +0 -0
- package/public-landing/assets/anim/idle/frame_15.webp +0 -0
- package/public-landing/assets/anim/idle/frame_16.webp +0 -0
- package/public-landing/assets/anim/idle/frame_17.webp +0 -0
- package/public-landing/assets/anim/idle/frame_18.webp +0 -0
- package/public-landing/assets/anim/idle/frame_19.webp +0 -0
- package/public-landing/assets/anim/idle/frame_20.webp +0 -0
- package/public-landing/assets/anim/idle/frame_21.webp +0 -0
- package/public-landing/assets/anim/idle/frame_22.webp +0 -0
- package/public-landing/assets/anim/idle/frame_23.webp +0 -0
- package/public-landing/assets/anim/idle/frame_24.webp +0 -0
- package/public-landing/assets/anim/idle/frame_25.webp +0 -0
- package/public-landing/assets/anim/idle/frame_26.webp +0 -0
- package/public-landing/assets/anim/idle/frame_27.webp +0 -0
- package/public-landing/assets/anim/idle/frame_28.webp +0 -0
- package/public-landing/assets/anim/idle/frame_29.webp +0 -0
- package/public-landing/assets/anim/idle/frame_30.webp +0 -0
- package/public-landing/assets/anim/idle/frame_31.webp +0 -0
- package/public-landing/assets/anim/idle/frame_32.webp +0 -0
- package/public-landing/assets/anim/idle/frame_33.webp +0 -0
- package/public-landing/assets/anim/idle/frame_34.webp +0 -0
- package/public-landing/assets/anim/idle/frame_35.webp +0 -0
- package/public-landing/assets/anim/idle/frame_36.webp +0 -0
- package/public-landing/assets/anim/idle/frame_37.webp +0 -0
- package/public-landing/assets/anim/idle/frame_38.webp +0 -0
- package/public-landing/assets/anim/idle/frame_39.webp +0 -0
- package/public-landing/assets/anim/idle/frame_40.webp +0 -0
- package/public-landing/assets/anim/idle/frame_41.webp +0 -0
- package/public-landing/assets/anim/idle/frame_42.webp +0 -0
- package/public-landing/assets/anim/idle/frame_43.webp +0 -0
- package/public-landing/create.html +51 -58
- package/public-landing/gate/README.md +137 -0
- package/public-landing/gate/dispmaps/bar.png +0 -0
- package/public-landing/gate/dispmaps/flat.png +0 -0
- package/public-landing/gate/dispmaps/key.png +0 -0
- package/public-landing/gate/index.html +587 -0
- package/public-landing/gate/previews/cloud-bongos.jpg +0 -0
- package/public-landing/gate/previews/off-the-boats.jpg +0 -0
- package/public-landing/gate/space.css +799 -0
- package/public-landing/gate/space.js +1826 -0
- package/public-landing/gate/sprites/enter.png +0 -0
- package/public-landing/gate/sprites/search.png +0 -0
- package/public-landing/gate/sprites/terminal-blank.png +0 -0
- package/public-landing/gate/sprites/tile.png +0 -0
- package/public-landing/world/assets/clouds/cloud-island-large.png +0 -0
- package/public-landing/world/assets/clouds/cloud-puff.png +0 -0
- package/public-landing/world/assets/creatures/creature-whale-cargo.png +0 -0
- package/public-landing/world/assets/creatures/creature-whale-glide.png +0 -0
- package/public-landing/world/assets/plates/plate-clouds-near.png +0 -0
- package/public-landing/world/assets/plates/scene-establishing.png +0 -0
- package/scripts/gds/abandon.js +6 -4
- package/scripts/gds/api.js +10 -5
- package/scripts/gds/api.ps1 +26 -0
- package/scripts/gds/api.sh +6 -0
- package/scripts/gds/audit-ships.js +7 -1
- package/scripts/gds/backfill-sessions.js +78 -18
- package/scripts/gds/backup.js +23 -1
- package/scripts/gds/box-connect-lib.js +124 -31
- package/scripts/gds/box-sync.js +66 -0
- package/scripts/gds/box.js +225 -53
- package/scripts/gds/cf-tunnel.js +145 -6
- package/scripts/gds/claim.js +34 -3
- package/scripts/gds/cli-lib.js +161 -27
- package/scripts/gds/code.js +6 -2
- package/scripts/gds/deploy-divergence.js +209 -0
- package/scripts/gds/dev-lib.js +309 -0
- package/scripts/gds/dev.js +322 -0
- package/scripts/gds/doctor.js +90 -61
- package/scripts/gds/fetch-art-key.js +1 -1
- package/scripts/gds/fetch-sound-prefs.js +2 -2
- package/scripts/gds/fitness.js +228 -12
- package/scripts/gds/gds-db-backup.service +16 -1
- package/scripts/gds/gds-db-offsite.service +9 -1
- package/scripts/gds/gen-api-client.js +1 -1
- package/scripts/gds/gen-api-docs.js +36 -14
- package/scripts/gds/gen-diagrams.js +45 -28
- package/scripts/gds/gen-file-map.js +2 -1
- package/scripts/gds/gen-freshness.js +32 -0
- package/scripts/gds/gen-repo-map.js +3 -2
- package/scripts/gds/gen-session-index.js +14 -5
- package/scripts/gds/hooks/pre-push.js +2 -2
- package/scripts/gds/init.js +60 -3
- package/scripts/gds/install-git-hooks.js +1 -1
- package/scripts/gds/key-card.js +15 -6
- package/scripts/gds/key-status.js +1 -1
- package/scripts/gds/linkify-refs.js +35 -9
- package/scripts/gds/login.js +16 -1
- package/scripts/gds/paste-token.js +7 -1
- package/scripts/gds/preflight.js +126 -0
- package/scripts/gds/provision.js +123 -12
- package/scripts/gds/publish-manifest.js +7 -1
- package/scripts/gds/reauth.js +7 -3
- package/scripts/gds/regen-instance-docs.js +47 -0
- package/scripts/gds/run-unit-tests.js +4 -2
- package/scripts/gds/seed-bongos-v1-seo-tasks.js +104 -0
- package/scripts/gds/seed-bv1-profiles-tasks.js +126 -0
- package/scripts/gds/sequence.js +425 -0
- package/scripts/gds/session-cost.js +10 -1
- package/scripts/gds/setup.js +31 -5
- package/scripts/gds/shell.js +5 -3
- package/scripts/gds/ship.js +319 -29
- package/scripts/gds/smoke-gds.js +139 -8
- package/scripts/gds/start.js +171 -22
- package/scripts/gds/status.js +1 -1
- package/scripts/gds/triage.js +10 -9
- package/scripts/gds/upgrade.js +124 -2
- package/scripts/status-mirror-sync.js +114 -84
- package/src/bongos/api-errors.js +1 -0
- package/src/bongos/auth.js +447 -102
- package/src/bongos/db-kernel.js +27 -0
- package/src/bongos/db.js +39 -0
- package/src/bongos/module-scope-map.js +27 -1
- package/src/bongos/permission-path-check.js +136 -102
- package/src/bongos/repo-info.js +47 -11
- package/src/bongos/route-rank-check.js +429 -46
- package/src/bongos/routes/_helpers.js +12 -1
- package/src/bongos/routes/auth.js +219 -2
- package/src/bongos/routes/backup.js +58 -2
- package/src/bongos/routes/me.js +35 -1
- package/src/bongos/serve-internal.js +84 -15
- package/src/branding.js +32 -0
- package/src/instance-config.js +30 -0
- package/src/module-api.js +112 -1
- package/src/platform-server.js +86 -9
- package/tests/abandon_task.mjs +59 -3
- package/tests/api_cli_flags.mjs +53 -0
- package/tests/auth_page_gate.mjs +13 -0
- package/tests/backfill_sessions.mjs +127 -1
- package/tests/backup_cli.mjs +54 -0
- package/tests/bongos_box_connect.mjs +77 -0
- package/tests/box_access.mjs +130 -1
- package/tests/box_credential.mjs +125 -0
- package/tests/box_host_keys.mjs +24 -0
- package/tests/box_infra_resolve.mjs +149 -0
- package/tests/box_scope_session.mjs +6 -2
- package/tests/box_sync.mjs +55 -0
- package/tests/box_task_scope.mjs +99 -0
- package/tests/branding.mjs +12 -0
- package/tests/cf_ruleset_preflight.mjs +142 -0
- package/tests/cf_tunnel.mjs +37 -0
- package/tests/claim_gate_rebase.mjs +98 -0
- package/tests/clawback_confirmed_backlog.mjs +103 -0
- package/tests/cli_lib_formatters.mjs +20 -0
- package/tests/confirm_attribution.mjs +30 -46
- package/tests/credit_grant.mjs +198 -10
- package/tests/criterion_authoring_gate.mjs +272 -0
- package/tests/criterion_progress.mjs +33 -1
- package/tests/dev_lib.mjs +253 -0
- package/tests/devbox_artifact_gate.mjs +75 -0
- package/tests/devbox_dmg_backdrop.mjs +137 -0
- package/tests/devbox_icons.mjs +304 -0
- package/tests/devbox_installer_brand.mjs +232 -0
- package/tests/devbox_key_register.mjs +234 -0
- package/tests/docs_openapi_route.mjs +110 -0
- package/tests/doctor_hooks.mjs +141 -0
- package/tests/extract_json.mjs +140 -0
- package/tests/fitness.mjs +89 -0
- package/tests/gen_freshness.mjs +96 -0
- package/tests/goal_advisory.mjs +251 -0
- package/tests/goal_criterion_confirm_ui.mjs +269 -0
- package/tests/goal_routes.mjs +59 -4
- package/tests/goal_tier.mjs +12 -3
- package/tests/governance_catalog.mjs +129 -0
- package/tests/governance_claim_eligibility.mjs +262 -0
- package/tests/governance_config.mjs +187 -0
- package/tests/governance_ownership_scope.mjs +410 -0
- package/tests/governance_page_gates.mjs +324 -0
- package/tests/governance_parity.mjs +336 -0
- package/tests/governance_principals.mjs +232 -0
- package/tests/governance_protected_surfaces.mjs +208 -0
- package/tests/governance_require_permission.mjs +223 -0
- package/tests/governance_resolver.mjs +84 -0
- package/tests/governance_role_sync.mjs +231 -0
- package/tests/governance_seed.mjs +127 -0
- package/tests/governance_session_scope.mjs +434 -0
- package/tests/governance_vanilla_boot.mjs +203 -0
- package/tests/grade_attribution.mjs +7 -7
- package/tests/hall_audit.mjs +223 -0
- package/tests/hall_nav.mjs +191 -0
- package/tests/hall_palette.mjs +206 -0
- package/tests/hall_signout.mjs +124 -0
- package/tests/hook_user_agents.mjs +10 -0
- package/tests/idea_triage.mjs +162 -0
- package/tests/idp_device_exchange.mjs +93 -0
- package/tests/init.mjs +51 -0
- package/tests/instance_config.mjs +65 -0
- package/tests/instance_mismatch_guard.mjs +23 -6
- package/tests/instance_release_config.mjs +104 -0
- package/tests/linkify_refs.mjs +15 -2
- package/tests/module_contributions.mjs +55 -0
- package/tests/module_loader.mjs +10 -5
- package/tests/module_scope_active_claims.mjs +94 -0
- package/tests/needs_rebase.mjs +92 -0
- package/tests/one_active_claim_per_session.mjs +76 -0
- package/tests/override_attribution.mjs +10 -8
- package/tests/override_request_ownership.mjs +8 -4
- package/tests/parse_cookie_decode.mjs +42 -0
- package/tests/pay_on_land.mjs +129 -0
- package/tests/platform_boot.mjs +100 -1
- package/tests/post_login_landing.mjs +42 -0
- package/tests/preflight_checks.mjs +72 -0
- package/tests/provision.mjs +132 -7
- package/tests/publish_branch_route.mjs +53 -0
- package/tests/publish_reconciler.mjs +187 -2
- package/tests/repo_info_binding.mjs +248 -0
- package/tests/reward_gate.mjs +11 -2
- package/tests/reward_mode_cost_plus_only.mjs +116 -0
- package/tests/reward_mode_surfacing.mjs +168 -0
- package/tests/route_permission_check.mjs +301 -0
- package/tests/route_rank_check.mjs +254 -12
- package/tests/route_validation.mjs +30 -0
- package/tests/sequence.mjs +267 -0
- package/tests/session_digest.mjs +44 -0
- package/tests/session_index.mjs +18 -6
- package/tests/session_records.mjs +39 -1
- package/tests/session_reward_on_land.mjs +381 -0
- package/tests/session_reward_reconcile.mjs +229 -0
- package/tests/ship_api_client_pathspec.mjs +100 -0
- package/tests/ship_base_flag.mjs +93 -0
- package/tests/ship_ci_deploy.mjs +80 -3
- package/tests/ship_upstream_deletion.mjs +57 -0
- package/tests/smoke_target.mjs +233 -0
- package/tests/start_env_guard.mjs +87 -0
- package/tests/start_rebase_warning.mjs +103 -0
- package/tests/status_mirror_sync.mjs +122 -0
- package/tests/stop_refcheck.mjs +19 -1
- package/tests/task_goal_link.mjs +15 -6
- package/tests/upgrade.mjs +139 -0
- package/tests/web_admission_status.mjs +162 -0
- package/tests/write_validation_gaps.mjs +110 -0
|
@@ -10,10 +10,12 @@
|
|
|
10
10
|
// must classify to a KNOWN rank — instead of diffing against a frozen snapshot
|
|
11
11
|
// (the old canonical-routes.json approach, which cried false drift whenever the
|
|
12
12
|
// route set legitimately grew). Classification is STRUCTURAL: a route is gated
|
|
13
|
-
// when its middleware chain carries requireRank/requireBuilder
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
13
|
+
// when its middleware chain carries requireRank/requireBuilder/requirePermission.
|
|
14
|
+
// A `rank:` annotation is NOT a gate — it is how an intentionally-PUBLIC route
|
|
15
|
+
// SAYS so (`rank: public`). A comment claiming a privileged rank over a route with
|
|
16
|
+
// no middleware certifies nothing, and is a finding of its own (BV1.R109d). A route
|
|
17
|
+
// the parser cannot classify ('unknown') means the system can't verify it's
|
|
18
|
+
// protected, which is likewise a finding:
|
|
17
19
|
// - write method (POST/PUT/PATCH/DELETE) → 'blocker' → hard FAIL on its own.
|
|
18
20
|
// - read method (GET) → 'major' → 'concerns'; the Hacker adjudicates intent.
|
|
19
21
|
//
|
|
@@ -42,6 +44,128 @@ const KNOWN_RANKS = new Set(['public', 'any-builder', 'metic+archon', 'archon',
|
|
|
42
44
|
const WRITE_METHODS = new Set(['POST', 'PUT', 'PATCH', 'DELETE']);
|
|
43
45
|
const SPECIALTY = 'deterministic: every src/bongos/routes/ route must classify to a known rank (ADR 0016)';
|
|
44
46
|
|
|
47
|
+
// ── permission gates (BV1.R97/R109, ADR 0151) ────────────────────────────────
|
|
48
|
+
//
|
|
49
|
+
// A migrated route gates on PERMISSION ATOMS — auth.requirePermission('backup.manage')
|
|
50
|
+
// — instead of a rank. `requirePermission` must mount AFTER `requireBuilder`, so before
|
|
51
|
+
// this arm existed a migrated route fell through to the requireBuilder branch and
|
|
52
|
+
// classified 'any-builder': a downgrade that reads as legitimate. To classify it
|
|
53
|
+
// honestly the auditor needs each atom's rank FLOOR (the rank that holds it under
|
|
54
|
+
// governance's compat seed), which is declared exactly once — in
|
|
55
|
+
// modules/governance/catalog.js. There is deliberately NO second permission→rank table
|
|
56
|
+
// here; a second list is a list that drifts.
|
|
57
|
+
//
|
|
58
|
+
// READ AS DATA, NOT require()d. ADR 0083's one-way rule forbids core from importing a
|
|
59
|
+
// MODULE's code (scripts/gds/fitness.js enforces it on require edges) and this file is
|
|
60
|
+
// core, so we parse the catalog's `floor:` declarations out of its SOURCE — the same
|
|
61
|
+
// technique this file already applies to modules/*/routes/*.js, and the posture the
|
|
62
|
+
// sibling trust primitive permission-path-check.js takes with
|
|
63
|
+
// modules/governance/protected-surfaces.json. Like those, it runs with no DB, no
|
|
64
|
+
// server and no module loader, so it works inside a git hook.
|
|
65
|
+
//
|
|
66
|
+
// LAZY + FAIL-CLOSED. Loaded on first use, so an instance whose routes carry no
|
|
67
|
+
// permission gate and no permission pin never reads it. An unreadable or empty catalog
|
|
68
|
+
// THROWS rather than degrading to "every atom is unknown"; an atom the catalog does not
|
|
69
|
+
// define classifies 'unknown', which hard-fails a write route.
|
|
70
|
+
const CATALOG_PATH = path.join(MODULES_DIR, 'governance', 'catalog.js');
|
|
71
|
+
// low → high authority. Mirrors catalog.js RANK_ORDER; the parse below rejects any
|
|
72
|
+
// floor outside it, so a divergence surfaces as 'unknown' (loud) rather than as trust.
|
|
73
|
+
const PERMISSION_RANK_ORDER = ['xenos', 'thetes', 'metic', 'archon'];
|
|
74
|
+
// A permission floor → the rank LABEL this auditor speaks. The vocabulary has no
|
|
75
|
+
// 'thetes' tier (requireRank('xenos') has always collapsed to 'any-builder' too), so
|
|
76
|
+
// both sub-Metic floors read 'any-builder': the label may UNDER-report strictness,
|
|
77
|
+
// never over-report it.
|
|
78
|
+
const FLOOR_LABEL = { xenos: 'any-builder', thetes: 'any-builder', metic: 'metic+archon', archon: 'archon' };
|
|
79
|
+
// How strict each label is, for combining a permission gate with a rank gate that sits
|
|
80
|
+
// on the same route (they AND, so the stricter one is the route's real floor).
|
|
81
|
+
const LABEL_STRICTNESS = { public: 0, 'any-builder': 1, 'metic+archon': 2, archon: 3, 'bfg-principal': 4 };
|
|
82
|
+
// A pin value written in permission terms: 'perm:<key>[,<key>…]' (see EXPECTED_RANKS).
|
|
83
|
+
const PERM_PIN_PREFIX = 'perm:';
|
|
84
|
+
|
|
85
|
+
// key → floor ('xenos'|'thetes'|'metic'|'archon'|null) from the catalog SOURCE. One
|
|
86
|
+
// catalog entry per line is the shape catalog.js has and its own load-time invariants
|
|
87
|
+
// keep; an entry this misses simply never resolves, which fails closed.
|
|
88
|
+
function parsePermissionFloors(text) {
|
|
89
|
+
const floors = new Map();
|
|
90
|
+
for (const line of String(text).split('\n')) {
|
|
91
|
+
const k = /\bkey:\s*'([^']+)'/.exec(line);
|
|
92
|
+
if (!k) continue;
|
|
93
|
+
const f = /\bfloor:\s*(?:'([a-z]+)'|null)/.exec(line);
|
|
94
|
+
if (!f) continue;
|
|
95
|
+
floors.set(k[1], f[1] || null); // f[1] undefined ⇒ floor: null ⇒ a machine principal
|
|
96
|
+
}
|
|
97
|
+
return floors;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
let _floorsCache = null;
|
|
101
|
+
function permissionFloors(catalogPath = CATALOG_PATH) {
|
|
102
|
+
const cacheable = catalogPath === CATALOG_PATH; // an explicit path (tests) is never cached
|
|
103
|
+
if (cacheable && _floorsCache) return _floorsCache;
|
|
104
|
+
let text;
|
|
105
|
+
try {
|
|
106
|
+
text = fs.readFileSync(catalogPath, 'utf8');
|
|
107
|
+
} catch (e) {
|
|
108
|
+
throw new Error(`permission catalog unreadable at ${catalogPath}: ${e.message}. `
|
|
109
|
+
+ 'Refusing to classify permission gates with no floor data (ADR 0151).');
|
|
110
|
+
}
|
|
111
|
+
const floors = parsePermissionFloors(text);
|
|
112
|
+
if (floors.size === 0) {
|
|
113
|
+
throw new Error(`permission catalog ${catalogPath} declares no permissions. `
|
|
114
|
+
+ 'Refusing to classify permission gates against an empty catalog (ADR 0151).');
|
|
115
|
+
}
|
|
116
|
+
if (cacheable) _floorsCache = floors;
|
|
117
|
+
return floors;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// The effective rank label of a requirePermission(...) gate, or 'unknown'.
|
|
121
|
+
//
|
|
122
|
+
// AND semantics (auth.requirePermission holds EVERY named atom), so the gate's floor is
|
|
123
|
+
// the STRICTEST floor among them. Fail-closed to 'unknown' — never to a rank — for:
|
|
124
|
+
// • no atoms named (a mis-authored gate; requirePermission() 403s everyone anyway)
|
|
125
|
+
// • an atom the catalog does not define (typo, renamed, deleted): it grants nothing,
|
|
126
|
+
// so treating it as "gated" would be the exact silent hole this arm exists to close
|
|
127
|
+
// • a PRINCIPAL atom (floor null — held by no rank-role, only by an exact
|
|
128
|
+
// builders.system_role). A route wanting one must use requireBfgPrincipal.
|
|
129
|
+
function rankForPermissions(keys, floors = permissionFloors()) {
|
|
130
|
+
if (!Array.isArray(keys) || keys.length === 0) return 'unknown';
|
|
131
|
+
let strictest = -1;
|
|
132
|
+
for (const key of keys) {
|
|
133
|
+
if (!floors.has(key)) return 'unknown';
|
|
134
|
+
const idx = PERMISSION_RANK_ORDER.indexOf(floors.get(key)); // null/unknown floor → -1
|
|
135
|
+
if (idx === -1) return 'unknown';
|
|
136
|
+
if (idx > strictest) strictest = idx;
|
|
137
|
+
}
|
|
138
|
+
return FLOOR_LABEL[PERMISSION_RANK_ORDER[strictest]];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ── how a guard is REFERENCED in a route's argument list (BV1.R109d) ─────────
|
|
142
|
+
//
|
|
143
|
+
// Core routes call the guards through a namespace (`auth.requireBuilder`); a module
|
|
144
|
+
// reaches the same guards through the published doorway, and may either namespace them
|
|
145
|
+
// (`api.requireBuilder`) or DESTRUCTURE them — `const { requireBuilder } = api` — and
|
|
146
|
+
// pass BARE identifiers. Recognising only the namespaced spelling made 11 correctly
|
|
147
|
+
// gated module routes (modules/platform-identity/routes/*) invisible to the middleware
|
|
148
|
+
// arm; they survived only because their `rank:` comment happened to tell the truth.
|
|
149
|
+
//
|
|
150
|
+
// The bare form is honoured ONLY in ARGUMENT POSITION (directly after `(` or `,`), so
|
|
151
|
+
// the identifier appearing in prose — or inside a `function` handler's body, which the
|
|
152
|
+
// `=> {` truncation below does not cut away — can never be mistaken for a gate.
|
|
153
|
+
const GATE_REF = String.raw`(?:(?:auth|api)\.|[(,]\s*)`;
|
|
154
|
+
const RE_RANK_XENOS = new RegExp(`${GATE_REF}requireRank\\(\\s*['"]xenos['"]`);
|
|
155
|
+
const RE_RANK_METIC_ARCHON = new RegExp(`${GATE_REF}requireRank\\(\\s*['"]metic['"]\\s*,\\s*['"]archon['"]\\s*\\)`);
|
|
156
|
+
const RE_RANK_ARCHON = new RegExp(`${GATE_REF}requireRank\\(\\s*['"]archon['"]\\s*\\)`);
|
|
157
|
+
const RE_BFG_PRINCIPAL = new RegExp(`${GATE_REF}requireBfgPrincipal`);
|
|
158
|
+
const RE_REQUIRE_BUILDER = new RegExp(`${GATE_REF}requireBuilder`);
|
|
159
|
+
const RE_REQUIRE_PERMISSION = new RegExp(`${GATE_REF}requirePermission\\(([^)]*)\\)`);
|
|
160
|
+
|
|
161
|
+
// The permission atoms a route's middleware list names, or null when it carries no
|
|
162
|
+
// permission gate. `[]` (an empty gate) is DISTINCT from null and resolves 'unknown'.
|
|
163
|
+
function permissionKeysIn(tail) {
|
|
164
|
+
const m = RE_REQUIRE_PERMISSION.exec(tail);
|
|
165
|
+
if (!m) return null;
|
|
166
|
+
return [...m[1].matchAll(/['"]([^'"]+)['"]/g)].map((x) => x[1]);
|
|
167
|
+
}
|
|
168
|
+
|
|
45
169
|
// Expected-rank allowlist for KNOWN privileged routes (#999, red-team residual).
|
|
46
170
|
// The base invariant above only checks a route classifies to SOME known rank —
|
|
47
171
|
// so an accidental DOWNGRADE (an Archon-only route silently dropped to
|
|
@@ -59,16 +183,23 @@ const SPECIALTY = 'deterministic: every src/bongos/routes/ route must classify t
|
|
|
59
183
|
// downgrade-protection; routes not listed still get the base "must classify"
|
|
60
184
|
// check. A key present here that no longer exists in the route set is itself a
|
|
61
185
|
// finding (a privileged route was removed/renamed without updating this pin).
|
|
186
|
+
//
|
|
187
|
+
// A PIN VALUE IS EITHER A RANK LABEL OR AN AUTHORITY ATOM ('perm:<key>[,<key>]' —
|
|
188
|
+
// BV1.R109, ADR 0151). Both forms are satisfied by BOTH gate spellings while
|
|
189
|
+
// R104-R107 migrate `requireRank` call sites to `requirePermission`, so migrating a
|
|
190
|
+
// pinned route no longer forces an edit here. Prefer the permission spelling for the
|
|
191
|
+
// pins whose downgrade is a true authority breach: once governance grows CUSTOM roles
|
|
192
|
+
// the ATOM, not the rank, is the authority, and a rank pin stops describing it.
|
|
62
193
|
const EXPECTED_RANKS = {
|
|
63
194
|
// builder authority — rank + lifecycle status mutation
|
|
64
|
-
'PATCH /builders/:id/rank': '
|
|
65
|
-
'PATCH /builders/:id/status': '
|
|
195
|
+
'PATCH /builders/:id/rank': 'perm:builder.rank.set',
|
|
196
|
+
'PATCH /builders/:id/status': 'perm:builder.status.set',
|
|
66
197
|
// session revocation (kill another builder's / all sessions)
|
|
67
|
-
'POST /auth/revoke': '
|
|
68
|
-
'POST /auth/revoke-all': '
|
|
198
|
+
'POST /auth/revoke': 'perm:auth.session.revoke',
|
|
199
|
+
'POST /auth/revoke-all': 'perm:auth.session.revoke',
|
|
69
200
|
// forensic surfaces
|
|
70
|
-
'GET /audit-log': '
|
|
71
|
-
'GET /builders/roster': '
|
|
201
|
+
'GET /audit-log': 'perm:audit_log.read',
|
|
202
|
+
'GET /builders/roster': 'perm:builder.roster.read',
|
|
72
203
|
// deliberately any-builder (task 1877) — the invite-picker roster (Goals
|
|
73
204
|
// invite control, task 1762) needs every active builder's identity, not just
|
|
74
205
|
// /leaderboard's top-100-by-credits window. Pinned so an accidental
|
|
@@ -89,6 +220,10 @@ const EXPECTED_RANKS = {
|
|
|
89
220
|
'GET /security/docs': 'archon',
|
|
90
221
|
'GET /security/docs/:slug': 'archon',
|
|
91
222
|
'POST /security/docs': 'archon',
|
|
223
|
+
// The full threat model + defenses inventory (ADR 0020). Same class as the docs
|
|
224
|
+
// pins above — a silent downgrade puts the defense inventory below the trust line.
|
|
225
|
+
// Newly VISIBLE in BV1.R109b (declared multi-line, so previously unpinnable).
|
|
226
|
+
'GET /security/adr': 'metic+archon',
|
|
92
227
|
};
|
|
93
228
|
|
|
94
229
|
// Module-scoped privileged-rank pins: a privileged route that lives in a FEATURE
|
|
@@ -100,6 +235,17 @@ const EXPECTED_RANKS = {
|
|
|
100
235
|
// box-fleet routes moved here from EXPECTED_RANKS in BV1.R43 / task 1412 — the pins
|
|
101
236
|
// follow the routes; the intent is unchanged: closing/blocking another builder's
|
|
102
237
|
// paid infra, and reading the fleet/cost roster, stay archon-only.)
|
|
238
|
+
//
|
|
239
|
+
// The `ideas` / `memory` / `security` blocks were added in BV1.R109b: their routes are
|
|
240
|
+
// declared multi-line and so were invisible to this auditor until it learned that form
|
|
241
|
+
// — meaning they could never have been pinned. They are pinned by RANK, not by
|
|
242
|
+
// `perm:` atom, deliberately: BV1.R104-R107 are migrating these very files to
|
|
243
|
+
// requirePermission right now, and a rank pin is satisfied by EITHER spelling (a
|
|
244
|
+
// permission gate resolves through the catalog into the same vocabulary), whereas an
|
|
245
|
+
// atom pin would red the build if the migration picks a different atom than guessed.
|
|
246
|
+
// The catalog already declares the atoms each will gate on — `security.report.file`
|
|
247
|
+
// (metic), `security.report.adjudicate` / `idea.ratify` / `memory.read.any` (archon) —
|
|
248
|
+
// so tightening these to `perm:` pins is a clean follow-up once R104-R107 have landed.
|
|
103
249
|
const MODULE_EXPECTED_RANKS = {
|
|
104
250
|
'dev-box': {
|
|
105
251
|
'GET /boxes': 'archon',
|
|
@@ -112,10 +258,11 @@ const MODULE_EXPECTED_RANKS = {
|
|
|
112
258
|
lifecycle: {
|
|
113
259
|
// task authoring / promotion / abandonment. Create + promote re-ranked
|
|
114
260
|
// Archon→Metic+ (ADR 0090): a Metic who can triage ideas may also seed + shape
|
|
115
|
-
// tasks. Abandon (
|
|
261
|
+
// tasks. Abandon followed (ADR 0152) — a Metic who may author work may retire
|
|
262
|
+
// it; /promote carries the abandoned→backlog undo at the same rank.
|
|
116
263
|
'POST /tasks': 'metic+archon',
|
|
117
264
|
'POST /tasks/:id/promote': 'metic+archon',
|
|
118
|
-
'POST /tasks/:id/abandon': 'archon',
|
|
265
|
+
'POST /tasks/:id/abandon': 'metic+archon',
|
|
119
266
|
// override-request adjudication (lands credit + ship attribution).
|
|
120
267
|
'GET /override-requests': 'archon',
|
|
121
268
|
'POST /override-requests/:id/decide': 'archon',
|
|
@@ -185,25 +332,85 @@ const MODULE_EXPECTED_RANKS = {
|
|
|
185
332
|
'POST /goals/:id/requests/:reqId/withdraw': 'any-builder',
|
|
186
333
|
// criterion confirmation + goal auto-achieve (ADR 0086 §6) — the planner's close loop.
|
|
187
334
|
'POST /done-when/:criterionId/satisfy': 'metic+archon',
|
|
188
|
-
// runtime criterion create (ADR 0105 gap fix, task 1874) —
|
|
189
|
-
|
|
335
|
+
// runtime criterion create (ADR 0105 gap fix, task 1874) — re-ranked Archon→Metic+
|
|
336
|
+
// (ADR 0154) as the OPEN of the loop `satisfy` above closes. Not a bare widening:
|
|
337
|
+
// a non-Archon must name a goal they own or manage (authorizeCriterionCreate) on a
|
|
338
|
+
// planning/building version, so the in-handler wall is load-bearing — and an
|
|
339
|
+
// unauthorized caller is masked as bad_goal_id. Unsatisfy stays Archon.
|
|
340
|
+
'POST /versions/:id/done-when': 'metic+archon',
|
|
341
|
+
// ...and its inverse stays ARCHON. Un-flipping a CONFIRMED criterion rewrites
|
|
342
|
+
// shipped scope-truth; ADR 0154's whole safety argument is that the backward
|
|
343
|
+
// ratchet did not move with the create. Previously unpinned — pinned here so a
|
|
344
|
+
// silent downgrade reds the build (task 1002538).
|
|
345
|
+
'POST /done-when/:criterionId/unsatisfy': 'archon',
|
|
190
346
|
// gate-surface PR approval from the hall (task 1179, ADR 0058 amendment) — the
|
|
191
347
|
// approve route clears the gate-review hard floor; the list route exposes gate internals.
|
|
192
348
|
'GET /gate-approvals': 'archon',
|
|
193
349
|
'POST /gate-approvals/:pr/approve': 'archon',
|
|
194
350
|
},
|
|
351
|
+
// Ratifying a criterion proposal WRITES scope-truth (it creates a done-when
|
|
352
|
+
// criterion). Metic+ may file the proposal; only the Archon may ratify it.
|
|
353
|
+
ideas: {
|
|
354
|
+
'POST /inbox/:id/ratify': 'archon',
|
|
355
|
+
},
|
|
356
|
+
// Cross-builder memory READS — the counterpart to the BFG cross-builder WRITE the
|
|
357
|
+
// KNOWN_RANKS note above already protects. A downgrade here breaks the memory
|
|
358
|
+
// isolation invariant: any builder could read every other builder's memory.
|
|
359
|
+
memory: {
|
|
360
|
+
'GET /memory/builders/:builderId/files': 'archon',
|
|
361
|
+
'GET /memory/builders/:builderId/file': 'archon',
|
|
362
|
+
},
|
|
363
|
+
// Vulnerability-report intake + adjudication. The four adjudication routes move a
|
|
364
|
+
// PAYOUT (confirm awards drachmae, dispute clears it), and the list route exposes
|
|
365
|
+
// every builder's exploit detail — the same "attacker cookbook" argument as the
|
|
366
|
+
// /security/docs pins. Intake is pinned 'metic+archon' rather than archon because it
|
|
367
|
+
// is DELIBERATELY Metic+ (the rank gate is the bounty noise-floor, V3.R63): the pin
|
|
368
|
+
// catches an accidental widening to Xenos as loudly as it catches a gate removal.
|
|
369
|
+
security: {
|
|
370
|
+
'POST /security/reports': 'metic+archon',
|
|
371
|
+
'GET /security/reports': 'archon',
|
|
372
|
+
'POST /security/reports/:id/confirm': 'archon',
|
|
373
|
+
'POST /security/reports/:id/dispute': 'archon',
|
|
374
|
+
'POST /security/reports/:id/fix': 'archon',
|
|
375
|
+
'POST /security/reports/:id/link-fix': 'archon',
|
|
376
|
+
},
|
|
195
377
|
};
|
|
196
378
|
|
|
197
|
-
//
|
|
379
|
+
// The rank a route's RANK middleware implies, or null when it carries none.
|
|
380
|
+
// Every spelling GATE_REF above accepts counts: namespaced (`auth.`/`api.`) or the
|
|
381
|
+
// bare identifier a module gets by destructuring the doorway.
|
|
382
|
+
function rankFromRankGate(tail) {
|
|
383
|
+
if (RE_RANK_XENOS.test(tail)) return 'any-builder';
|
|
384
|
+
if (RE_RANK_METIC_ARCHON.test(tail)) return 'metic+archon';
|
|
385
|
+
if (RE_RANK_ARCHON.test(tail)) return 'archon';
|
|
386
|
+
// Matched BEFORE the requireBuilder branch in THIS classifier's if-chain (not
|
|
387
|
+
// a statement about middleware execution order — the actual route chain runs
|
|
388
|
+
// requireBuilder → requireBfgPrincipal). The BFG write route carries both;
|
|
389
|
+
// requireBuilder authenticates, requireBfgPrincipal narrows to the one service
|
|
390
|
+
// principal, and the principal gate is the load-bearing one we want recorded.
|
|
391
|
+
if (RE_BFG_PRINCIPAL.test(tail)) return 'bfg-principal';
|
|
392
|
+
if (RE_REQUIRE_BUILDER.test(tail)) return 'any-builder';
|
|
393
|
+
return null;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Classify one route's gate from the source lines and the declaration index.
|
|
397
|
+
// Returns { rank, perms, via } — `perms` is the permission atoms the route names, or
|
|
398
|
+
// null when it carries no permission gate; `via` is the PROVENANCE of the label:
|
|
399
|
+
// 'middleware' (a real gate), 'annotation' (only a `rank:` comment), or null (neither).
|
|
198
400
|
// 1. The middleware chain on the declaration line + next 20 lines (captures
|
|
199
|
-
// multi-line declarations) — requireRank/requireBuilder is
|
|
401
|
+
// multi-line declarations) — requirePermission/requireRank/requireBuilder is
|
|
402
|
+
// authoritative.
|
|
200
403
|
// 2. Otherwise a `rank:` annotation anywhere in the CONTIGUOUS comment block
|
|
201
404
|
// immediately above the declaration. Scanning the whole block (not a fixed
|
|
202
405
|
// 5-line window) is the fix for routes like GET /public/repo-info, whose
|
|
203
406
|
// annotation sits atop a 6-line comment — just out of the old window.
|
|
204
407
|
// 3. Neither → 'unknown'. An annotation whose value isn't a known rank (typo)
|
|
205
408
|
// is likewise 'unknown' — that surfaces the typo rather than trusting it.
|
|
206
|
-
|
|
409
|
+
//
|
|
410
|
+
// `via` is what keeps a COMMENT from passing as a trust decision (BV1.R109d): callers
|
|
411
|
+
// treat an 'annotation' label as a claim, not a verified gate. Only `rank: public` is
|
|
412
|
+
// a claim a comment can honestly make on its own — asserting nothing is protected.
|
|
413
|
+
function classifyGate(lines, i, { floors } = {}) {
|
|
207
414
|
const window = lines.slice(i, i + 20).join('\n');
|
|
208
415
|
// Only the route's own MIDDLEWARE LIST decides its rank — not its handler body.
|
|
209
416
|
// The middleware are the args before the handler function; the handler opens
|
|
@@ -216,21 +423,22 @@ function classifyRank(lines, i) {
|
|
|
216
423
|
// (e.g. a `function` handler), fall back to the full window — prior behaviour.
|
|
217
424
|
const handlerAt = window.search(/=>\s*\{/);
|
|
218
425
|
const tail = handlerAt === -1 ? window : window.slice(0, handlerAt);
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
426
|
+
|
|
427
|
+
const perms = permissionKeysIn(tail);
|
|
428
|
+
const byRank = rankFromRankGate(tail);
|
|
429
|
+
if (perms) {
|
|
430
|
+
const byPermission = rankForPermissions(perms, floors || permissionFloors());
|
|
431
|
+
// An unresolvable permission gate is 'unknown' EVEN THOUGH a requireBuilder sits
|
|
432
|
+
// beside it (every permission gate mounts after one). Deferring to that
|
|
433
|
+
// requireBuilder would let a typo'd atom — which grants nothing — read as
|
|
434
|
+
// 'any-builder', i.e. as gated. Fail closed and loud instead.
|
|
435
|
+
if (byPermission === 'unknown') return { rank: 'unknown', perms, via: 'middleware' };
|
|
436
|
+
// The two gate forms AND when a route carries both, so the stricter one is the
|
|
437
|
+
// route's real floor. Reporting the looser one would understate authority.
|
|
438
|
+
const stricter = byRank && LABEL_STRICTNESS[byRank] > LABEL_STRICTNESS[byPermission] ? byRank : byPermission;
|
|
439
|
+
return { rank: stricter, perms, via: 'middleware' };
|
|
440
|
+
}
|
|
441
|
+
if (byRank) return { rank: byRank, perms: null, via: 'middleware' };
|
|
234
442
|
|
|
235
443
|
for (let j = i - 1; j >= 0; j--) {
|
|
236
444
|
const line = lines[j];
|
|
@@ -243,24 +451,95 @@ function classifyRank(lines, i) {
|
|
|
243
451
|
const c = /\brank:\s*([a-z]+(?:[+-][a-z]+)*)/i.exec(line);
|
|
244
452
|
if (c) {
|
|
245
453
|
const v = c[1].toLowerCase();
|
|
246
|
-
|
|
454
|
+
// A typo is 'unknown' with NO provenance — there is nothing to trust here.
|
|
455
|
+
return KNOWN_RANKS.has(v)
|
|
456
|
+
? { rank: v, perms: null, via: 'annotation' }
|
|
457
|
+
: { rank: 'unknown', perms: null, via: null };
|
|
247
458
|
}
|
|
248
459
|
}
|
|
249
460
|
}
|
|
250
|
-
return 'unknown';
|
|
461
|
+
return { rank: 'unknown', perms: null, via: null };
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// Back-compat scalar form (gen-api-docs + several tests call this directly).
|
|
465
|
+
function classifyRank(lines, i, opts) {
|
|
466
|
+
return classifyGate(lines, i, opts).rank;
|
|
251
467
|
}
|
|
252
468
|
|
|
253
|
-
//
|
|
254
|
-
//
|
|
255
|
-
|
|
469
|
+
// ── route-declaration scanning (BV1.R109b) ───────────────────────────────────
|
|
470
|
+
//
|
|
471
|
+
// A route declaration OPENS with `router.<method>(`. The path may sit on that same
|
|
472
|
+
// line, or — for a long middleware chain — on the line below it:
|
|
473
|
+
//
|
|
474
|
+
// router.get(
|
|
475
|
+
// '/security/adr',
|
|
476
|
+
// auth.requireBuilder,
|
|
477
|
+
// auth.requireRank('metic', 'archon'),
|
|
478
|
+
//
|
|
479
|
+
// Until BV1.R109b the scanner required method AND quoted path on ONE line, so a
|
|
480
|
+
// multi-line declaration was never emitted: 10 real routes (6 of them writes,
|
|
481
|
+
// 2 reading other builders' memory) were INVISIBLE to this auditor — never checked
|
|
482
|
+
// for being ungated or downgraded, never pinned, never published by gen-api-docs.
|
|
483
|
+
//
|
|
484
|
+
// ROUTE_DECL_RE is therefore the CENSUS: every line it matches must resolve to
|
|
485
|
+
// exactly one classified route. A declaration whose path cannot be resolved
|
|
486
|
+
// statically is reported by checkRouteRanks (below) instead of being skipped —
|
|
487
|
+
// fail-closed, because a route the auditor cannot even NAME is strictly less
|
|
488
|
+
// verifiable than one whose gate it merely cannot classify.
|
|
489
|
+
const ROUTE_DECL_RE = /^\s*router\.(get|post|patch|put|delete)\s*\(/;
|
|
490
|
+
const ROUTE_DECL_SAME_LINE_RE = /^\s*router\.(get|post|patch|put|delete)\s*\(\s*['"]([^'"]+)['"]/;
|
|
491
|
+
// A continuation line carrying the quoted path, then the comma before the middleware.
|
|
492
|
+
const ROUTE_PATH_LINE_RE = /^\s*['"]([^'"]+)['"]\s*,/;
|
|
493
|
+
// How far past a multi-line opener to look for the path. It is on the very next line
|
|
494
|
+
// in every real case; the slack only tolerates an interleaved comment.
|
|
495
|
+
const PATH_LOOKAHEAD = 5;
|
|
496
|
+
|
|
497
|
+
// The literal path a declaration at line `i` names, or null when it is not statically
|
|
498
|
+
// resolvable (a template literal, a variable, a computed path).
|
|
499
|
+
function routePathAt(lines, i) {
|
|
500
|
+
const same = ROUTE_DECL_SAME_LINE_RE.exec(lines[i]);
|
|
501
|
+
if (same) return same[2];
|
|
502
|
+
// Multi-line form: nothing but whitespace/comment may follow the '(' …
|
|
503
|
+
const rest = lines[i].slice(lines[i].indexOf('(') + 1).trim();
|
|
504
|
+
if (rest !== '' && !rest.startsWith('//')) return null;
|
|
505
|
+
// … and the first content line below must open with the quoted path.
|
|
506
|
+
for (let j = i + 1; j < lines.length && j <= i + PATH_LOOKAHEAD; j++) {
|
|
507
|
+
const t = lines[j].trim();
|
|
508
|
+
if (t === '' || t.startsWith('//')) continue;
|
|
509
|
+
const m = ROUTE_PATH_LINE_RE.exec(lines[j]);
|
|
510
|
+
return m ? m[1] : null;
|
|
511
|
+
}
|
|
512
|
+
return null;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// Every `router.<method>(` declaration in a route file's source, in source order:
|
|
516
|
+
// [{ method, route, line }] — `route` is null when the path did not resolve. This is
|
|
517
|
+
// the single scanner BOTH consumers use (this auditor and gen-api-docs.js), so the
|
|
518
|
+
// published API contract and the enforced gate list can never enumerate different
|
|
519
|
+
// routes. There is deliberately no second declaration regex in gen-api-docs.
|
|
520
|
+
function scanDeclarations(text) {
|
|
256
521
|
const lines = String(text).split('\n');
|
|
257
522
|
const out = [];
|
|
258
523
|
for (let i = 0; i < lines.length; i++) {
|
|
259
|
-
const m =
|
|
524
|
+
const m = ROUTE_DECL_RE.exec(lines[i]);
|
|
260
525
|
if (!m) continue;
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
526
|
+
out.push({ method: m[1].toUpperCase(), route: routePathAt(lines, i), line: i });
|
|
527
|
+
}
|
|
528
|
+
return out;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// Parse route declarations + gates from a route file's source text. Pure (no fs
|
|
532
|
+
// beyond the lazily-loaded permission catalog) so it's trivially testable.
|
|
533
|
+
// Returns [{ method, route, key, rank, perms }].
|
|
534
|
+
function classifyText(text, opts) {
|
|
535
|
+
const lines = String(text).split('\n');
|
|
536
|
+
const out = [];
|
|
537
|
+
for (const d of scanDeclarations(text)) {
|
|
538
|
+
if (d.route === null) continue; // surfaced as a finding by checkRouteRanks, not dropped
|
|
539
|
+
// classifyGate scans FROM the declaration line, so for a multi-line declaration the
|
|
540
|
+
// middleware (which sits below the path line) is inside its 20-line window, and its
|
|
541
|
+
// comment-block `rank:` scan still starts directly above the declaration.
|
|
542
|
+
out.push({ method: d.method, route: d.route, key: `${d.method} ${d.route}`, ...classifyGate(lines, d.line, opts) });
|
|
264
543
|
}
|
|
265
544
|
return out;
|
|
266
545
|
}
|
|
@@ -290,6 +569,54 @@ function discoverRouteFiles({ routesDir = ROUTES_DIR, modulesDir = MODULES_DIR }
|
|
|
290
569
|
return out;
|
|
291
570
|
}
|
|
292
571
|
|
|
572
|
+
// A pin value → { kind: 'rank', rank } | { kind: 'permission', perms[] }.
|
|
573
|
+
// 'perm:a,b' names the AUTHORITY ATOMS the route must gate on; anything else is a
|
|
574
|
+
// rank label (the pre-governance spelling, still valid).
|
|
575
|
+
function parsePin(value) {
|
|
576
|
+
if (typeof value === 'string' && value.startsWith(PERM_PIN_PREFIX)) {
|
|
577
|
+
return {
|
|
578
|
+
kind: 'permission',
|
|
579
|
+
perms: value.slice(PERM_PIN_PREFIX.length).split(',').map((s) => s.trim()).filter(Boolean),
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
return { kind: 'rank', rank: value };
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// Why route `r` fails its pin, or null when it satisfies it.
|
|
586
|
+
//
|
|
587
|
+
// EITHER GATE FORM SATISFIES A PIN — that is what makes the R104-R107 migration
|
|
588
|
+
// window survivable. A rank pin is satisfied by whatever the route classifies to
|
|
589
|
+
// (a requirePermission gate resolves through the catalog into the same vocabulary).
|
|
590
|
+
// A permission pin is satisfied by the route naming exactly those atoms, OR — while
|
|
591
|
+
// the route is still un-migrated — by carrying the rank gate those atoms' floor
|
|
592
|
+
// resolves to. Neither direction lets a route drop below what the pin demands.
|
|
593
|
+
function pinMismatch(expected, r) {
|
|
594
|
+
const pin = parsePin(expected);
|
|
595
|
+
// BV1.R109d — a pin is NEVER satisfied by an annotation. Pins exist for routes
|
|
596
|
+
// "whose downgrade would be a real authority breach", so satisfying one with a
|
|
597
|
+
// `// rank: archon` comment over a route carrying no middleware would let the
|
|
598
|
+
// deterministic alarm certify prose. Checked before the label comparisons below
|
|
599
|
+
// because the label itself is what the comment fabricates.
|
|
600
|
+
if (r.via === 'annotation') {
|
|
601
|
+
return `is pinned to '${expected}' but carries NO auth middleware — its rank comes only from a \`rank:\` comment, which certifies nothing`;
|
|
602
|
+
}
|
|
603
|
+
if (pin.kind === 'rank') {
|
|
604
|
+
return r.rank === pin.rank ? null : `classifies as '${r.rank}' but is pinned to '${pin.rank}'`;
|
|
605
|
+
}
|
|
606
|
+
const pinnedRank = rankForPermissions(pin.perms);
|
|
607
|
+
if (pinnedRank === 'unknown') {
|
|
608
|
+
return `is pinned to permission(s) '${pin.perms.join(', ')}' the governance catalog does not define`;
|
|
609
|
+
}
|
|
610
|
+
if (r.perms) {
|
|
611
|
+
const got = [...r.perms].sort().join(',');
|
|
612
|
+
const want = [...pin.perms].sort().join(',');
|
|
613
|
+
return got === want ? null : `gates on permission(s) '${got}' but is pinned to '${want}'`;
|
|
614
|
+
}
|
|
615
|
+
return r.rank === pinnedRank
|
|
616
|
+
? null
|
|
617
|
+
: `classifies as '${r.rank}' but is pinned to permission(s) '${pin.perms.join(', ')}' (rank floor '${pinnedRank}')`;
|
|
618
|
+
}
|
|
619
|
+
|
|
293
620
|
// Walk every route file (core + modules) and assert the invariant. Returns:
|
|
294
621
|
// { ok, hardFail, routeCount, routes[], findings[] }
|
|
295
622
|
// findings[]: { severity, file, route, method, rank, summary }
|
|
@@ -299,13 +626,34 @@ function checkRouteRanks({ routesDir = ROUTES_DIR, modulesDir } = {}) {
|
|
|
299
626
|
// caller (or a hermetic test) may pass modulesDir explicitly to override.
|
|
300
627
|
if (modulesDir === undefined) modulesDir = path.resolve(routesDir, '..', '..', '..', 'modules');
|
|
301
628
|
const routes = [];
|
|
629
|
+
const unresolved = [];
|
|
302
630
|
for (const { abs, rel } of discoverRouteFiles({ routesDir, modulesDir })) {
|
|
303
|
-
|
|
304
|
-
|
|
631
|
+
const text = fs.readFileSync(abs, 'utf8');
|
|
632
|
+
for (const r of classifyText(text)) routes.push({ ...r, file: rel });
|
|
633
|
+
for (const d of scanDeclarations(text)) {
|
|
634
|
+
if (d.route === null) unresolved.push({ ...d, file: rel });
|
|
305
635
|
}
|
|
306
636
|
}
|
|
307
637
|
|
|
308
638
|
const findings = [];
|
|
639
|
+
|
|
640
|
+
// BV1.R109b — a declaration whose path this scanner cannot resolve is INVISIBLE to
|
|
641
|
+
// every check below, which is exactly the hole that hid 10 multi-line routes. Report
|
|
642
|
+
// it at the same severity as an unclassifiable gate: a write is a hard fail, a read
|
|
643
|
+
// is a concern the Hacker adjudicates.
|
|
644
|
+
for (const d of unresolved) {
|
|
645
|
+
const isWrite = WRITE_METHODS.has(d.method);
|
|
646
|
+
findings.push({
|
|
647
|
+
severity: isWrite ? 'blocker' : 'major',
|
|
648
|
+
file: d.file,
|
|
649
|
+
route: `${d.method} (unresolved ${d.file}:${d.line + 1})`,
|
|
650
|
+
method: d.method,
|
|
651
|
+
rank: '(unparsed)',
|
|
652
|
+
summary: `A router.${d.method.toLowerCase()}( declaration at ${d.file}:${d.line + 1} names a path this auditor cannot resolve statically, so the route is invisible to the rank check AND absent from the generated API contract. `
|
|
653
|
+
+ 'Declare the path as a literal string — on the declaration line, or on the line directly below it.',
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
|
|
309
657
|
for (const r of routes) {
|
|
310
658
|
if (r.rank !== 'unknown') continue;
|
|
311
659
|
const isWrite = WRITE_METHODS.has(r.method);
|
|
@@ -322,6 +670,28 @@ function checkRouteRanks({ routesDir = ROUTES_DIR, modulesDir } = {}) {
|
|
|
322
670
|
});
|
|
323
671
|
}
|
|
324
672
|
|
|
673
|
+
// BV1.R109d — a `rank:` COMMENT claiming a PRIVILEGED rank over a route that
|
|
674
|
+
// carries no auth middleware. `rank: public` is the annotation's legitimate use —
|
|
675
|
+
// it asserts nothing is protected, which a comment can honestly say. Any other
|
|
676
|
+
// value asserts a gate exists, and the annotation arm is reached ONLY when no
|
|
677
|
+
// middleware was found, so that assertion is false by construction. Left
|
|
678
|
+
// unchecked, such a route passed the ungated-write invariant AND every exact pin.
|
|
679
|
+
for (const r of routes) {
|
|
680
|
+
if (r.via !== 'annotation' || r.rank === 'public') continue;
|
|
681
|
+
const isWrite = WRITE_METHODS.has(r.method);
|
|
682
|
+
findings.push({
|
|
683
|
+
severity: isWrite ? 'blocker' : 'major',
|
|
684
|
+
file: r.file,
|
|
685
|
+
route: r.key,
|
|
686
|
+
method: r.method,
|
|
687
|
+
rank: r.rank,
|
|
688
|
+
summary: `${r.key} (${r.file}) claims \`rank: ${r.rank}\` in a comment but carries NO auth middleware — no requireRank, requirePermission, requireBuilder or principal gate. A comment cannot gate a route. `
|
|
689
|
+
+ (isWrite
|
|
690
|
+
? 'A privileged write route gated only by prose is a hard fail (ADR 0016): add the real middleware, or annotate `rank: public` if it is genuinely unauthenticated.'
|
|
691
|
+
: 'Add the real middleware, or annotate `rank: public` if it is genuinely unauthenticated.'),
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
|
|
325
695
|
// #999 — EXACT-rank pin on known privileged routes. A downgrade (e.g. an
|
|
326
696
|
// archon route silently dropped to any-builder) trips a 'blocker' here even
|
|
327
697
|
// though the route still classifies to a *known* rank, which the loop above
|
|
@@ -354,19 +724,20 @@ function checkRouteRanks({ routesDir = ROUTES_DIR, modulesDir } = {}) {
|
|
|
354
724
|
route: key,
|
|
355
725
|
method: key.split(' ')[0],
|
|
356
726
|
rank: '(missing)',
|
|
357
|
-
summary: `Privileged route ${key} is pinned to expect
|
|
727
|
+
summary: `Privileged route ${key} is pinned to expect '${expected}' but is no longer present under src/bongos/routes/. `
|
|
358
728
|
+ 'If it was intentionally removed/renamed, drop or update its entry in EXPECTED_RANKS; otherwise a protected route vanished.',
|
|
359
729
|
});
|
|
360
730
|
continue;
|
|
361
731
|
}
|
|
362
|
-
|
|
732
|
+
const mismatch = pinMismatch(expected, r);
|
|
733
|
+
if (mismatch) {
|
|
363
734
|
findings.push({
|
|
364
735
|
severity: 'blocker',
|
|
365
736
|
file: r.file,
|
|
366
737
|
route: r.key,
|
|
367
738
|
method: r.method,
|
|
368
739
|
rank: r.rank,
|
|
369
|
-
summary: `Privileged route ${r.key} (${r.file})
|
|
740
|
+
summary: `Privileged route ${r.key} (${r.file}) ${mismatch}. `
|
|
370
741
|
+ 'This is a rank DOWNGRADE/CHANGE on a route that guards authority (ADR 0016). '
|
|
371
742
|
+ 'If the change is intentional, update EXPECTED_RANKS in route-rank-check.js in the same commit; otherwise restore the gate.',
|
|
372
743
|
});
|
|
@@ -413,13 +784,25 @@ function toWorkerResult(result) {
|
|
|
413
784
|
module.exports = {
|
|
414
785
|
ROUTE_FILE_RE,
|
|
415
786
|
MODULES_DIR,
|
|
787
|
+
CATALOG_PATH,
|
|
416
788
|
KNOWN_RANKS,
|
|
417
789
|
WRITE_METHODS,
|
|
418
790
|
EXPECTED_RANKS,
|
|
419
791
|
MODULE_EXPECTED_RANKS,
|
|
792
|
+
PERM_PIN_PREFIX,
|
|
793
|
+
PERMISSION_RANK_ORDER,
|
|
794
|
+
FLOOR_LABEL,
|
|
420
795
|
SPECIALTY,
|
|
796
|
+
parsePermissionFloors,
|
|
797
|
+
permissionFloors,
|
|
798
|
+
permissionKeysIn,
|
|
799
|
+
rankForPermissions,
|
|
800
|
+
parsePin,
|
|
801
|
+
pinMismatch,
|
|
802
|
+
classifyGate,
|
|
421
803
|
classifyRank,
|
|
422
804
|
classifyText,
|
|
805
|
+
scanDeclarations,
|
|
423
806
|
discoverRouteFiles,
|
|
424
807
|
checkRouteRanks,
|
|
425
808
|
toWorkerResult,
|
|
@@ -33,7 +33,18 @@ function parseCookie(req, name) {
|
|
|
33
33
|
const cookieHeader = req.headers['cookie'] || '';
|
|
34
34
|
for (const part of cookieHeader.split(';')) {
|
|
35
35
|
const [k, v] = part.trim().split('=');
|
|
36
|
-
if (k === name && v)
|
|
36
|
+
if (k === name && v) {
|
|
37
|
+
// Cookie values are percent-ENCODED on write (Express `res.cookie` defaults
|
|
38
|
+
// to encodeURIComponent), so decode symmetrically on read. Without this, a
|
|
39
|
+
// return path like "/create" arrives as "%2Fcreate": safeReturnTo() sees a
|
|
40
|
+
// value that doesn't start with "/", tries `new URL()`, throws, and returns
|
|
41
|
+
// null — so a post-login redirect silently falls through to the default
|
|
42
|
+
// landing instead of the requested page (task 1002241; also un-breaks the
|
|
43
|
+
// #726 subdomain-return, whose full https:// URLs were encoded too). No-op
|
|
44
|
+
// for every value without a %-escape (hex state/session tokens, "1"). Guard
|
|
45
|
+
// a malformed lone "%" by falling back to the raw value.
|
|
46
|
+
try { return decodeURIComponent(v); } catch { return v; }
|
|
47
|
+
}
|
|
37
48
|
}
|
|
38
49
|
return null;
|
|
39
50
|
}
|