@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
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
// tests/governance_ownership_scope.mjs — ownership-scoped permissions
|
|
2
|
+
// ("act on OWN X") as a first-class axis (BV1.R100, ADR 0151 §3). DB-free.
|
|
3
|
+
//
|
|
4
|
+
// The thing being proved is a CONJUNCTION. `box.manage.own` is not a grant that
|
|
5
|
+
// happens to be named "own" — it is half a decision:
|
|
6
|
+
//
|
|
7
|
+
// 1. the ROLE fact — the builder holds the key (resolved from the DB, uncached);
|
|
8
|
+
// 2. the OWNER fact — the concrete row is theirs (a REQUEST-scoped fact the
|
|
9
|
+
// resolver structurally cannot know: it is handed a builder
|
|
10
|
+
// id, never a box).
|
|
11
|
+
//
|
|
12
|
+
// The security failure mode this file exists to catch is (2) quietly disappearing,
|
|
13
|
+
// leaving `.own` as an unconditional grant — i.e. `X.manage.own` silently becoming
|
|
14
|
+
// `X.manage.any`. So the load-bearing tests are: `.own` never implies `.any`, and a
|
|
15
|
+
// builder holding EVERY permission in the catalog still cannot touch a row that
|
|
16
|
+
// belongs to someone else.
|
|
17
|
+
//
|
|
18
|
+
// ORACLE DISCIPLINE. A test whose two sides read the same field cannot fail — a bad
|
|
19
|
+
// value moves both together. So the "own ≠ any" assertions are driven by `floor`
|
|
20
|
+
// (a PRE-R100 field) run through auth.rankMeetsThreshold (the REAL production rank
|
|
21
|
+
// gate). The new `scope`/`resource` fields are used only to ENUMERATE which pairs
|
|
22
|
+
// to check, never to decide whether a pair is separated.
|
|
23
|
+
import assert from 'node:assert/strict';
|
|
24
|
+
import { test } from 'node:test';
|
|
25
|
+
import { createRequire } from 'node:module';
|
|
26
|
+
import fs from 'node:fs';
|
|
27
|
+
import os from 'node:os';
|
|
28
|
+
import path from 'node:path';
|
|
29
|
+
import { fileURLToPath } from 'node:url';
|
|
30
|
+
|
|
31
|
+
const require = createRequire(import.meta.url);
|
|
32
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
33
|
+
const GOV = path.join(ROOT, 'modules', 'governance');
|
|
34
|
+
const catalog = require(path.join(GOV, 'catalog.js'));
|
|
35
|
+
const resolver = require(path.join(GOV, 'resolver.js'));
|
|
36
|
+
const db = require(path.join(GOV, 'db.js'));
|
|
37
|
+
const auth = require(path.join(ROOT, 'src', 'bongos', 'auth.js'));
|
|
38
|
+
|
|
39
|
+
const {
|
|
40
|
+
PERMISSIONS, RANK_ORDER, RANK_ROLE_SEED, RESOURCE_FAMILIES, permissionKeys, byKey,
|
|
41
|
+
scopeOf, resourceOf, isOwnershipScoped, isCrossOwner, ownershipScopedKeys, crossOwnerKeys,
|
|
42
|
+
crossOwnerKeysFor, isPrincipalKey, seedForRank,
|
|
43
|
+
} = catalog;
|
|
44
|
+
const {
|
|
45
|
+
sameOwner, authorizeOwned, authorizeOwnedOrAny, builderHasPermissions, effectivePermissions,
|
|
46
|
+
} = resolver;
|
|
47
|
+
|
|
48
|
+
const ME = 42;
|
|
49
|
+
const SOMEONE_ELSE = 99;
|
|
50
|
+
|
|
51
|
+
// Stub the ONE DB read the resolver makes, so the ownership logic is exercised
|
|
52
|
+
// end-to-end without Postgres. resolver.js looks the method up on the db module
|
|
53
|
+
// object at call time, so replacing it here bites the real code path.
|
|
54
|
+
const realGetPermissionKeys = db.getPermissionKeysForBuilder;
|
|
55
|
+
async function holding(keys, fn) {
|
|
56
|
+
db.getPermissionKeysForBuilder = async () => keys;
|
|
57
|
+
try {
|
|
58
|
+
return await fn();
|
|
59
|
+
} finally {
|
|
60
|
+
db.getPermissionKeysForBuilder = realGetPermissionKeys;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
const EVERYTHING = permissionKeys();
|
|
64
|
+
|
|
65
|
+
// Families with BOTH sides declared — the pairs "own must not imply any" is about.
|
|
66
|
+
const PAIRED = Object.entries(RESOURCE_FAMILIES).filter(([, f]) => f.own.length && f.any.length);
|
|
67
|
+
|
|
68
|
+
// ═══ A — the axis is explicit, well-formed catalog DATA ═══════════════════════
|
|
69
|
+
|
|
70
|
+
test('A1: scope and resource come together, with a valid scope and a real rank floor', () => {
|
|
71
|
+
// byKey() returns the NORMALIZED record (absent scope/resource read back as
|
|
72
|
+
// null), the same shape R98's principal axis normalizes to.
|
|
73
|
+
for (const p of permissionKeys().map(byKey)) {
|
|
74
|
+
assert.equal(p.scope === null, p.resource === null,
|
|
75
|
+
`${p.key}: an ownership scope is always scoped OVER a named resource`);
|
|
76
|
+
if (p.scope === null) continue;
|
|
77
|
+
assert.ok(['own', 'any'].includes(p.scope), `${p.key}: invalid scope "${p.scope}"`);
|
|
78
|
+
assert.equal(typeof p.resource, 'string');
|
|
79
|
+
assert.ok(p.resource.length > 0, `${p.key}: empty resource family`);
|
|
80
|
+
assert.notEqual(p.floor, null,
|
|
81
|
+
`${p.key}: the ownership axis narrows a ROLE permission — it never composes with an identity`);
|
|
82
|
+
assert.equal(isPrincipalKey(p.key), false, `${p.key}: a principal has no owner axis`);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('A2: every ".own"-suffixed key DECLARES scope own — the namespace never outruns the data', () => {
|
|
87
|
+
const suffixed = PERMISSIONS.filter((p) => p.key.endsWith('.own'));
|
|
88
|
+
assert.ok(suffixed.length >= 4, `expected the own-scoped keys, saw ${suffixed.length}`);
|
|
89
|
+
for (const p of suffixed) {
|
|
90
|
+
assert.equal(p.scope, 'own', `${p.key} looks ownership-scoped but does not declare it`);
|
|
91
|
+
}
|
|
92
|
+
// ...and the converse is deliberately NOT asserted: `me.wandering.set` is
|
|
93
|
+
// own-scoped with no suffix, and `task.claim.any` ends in `.any` while meaning
|
|
94
|
+
// "any task in the pool", not "any owner's resource". Suffix inference would get
|
|
95
|
+
// both wrong — which is the whole reason the axis is data.
|
|
96
|
+
assert.equal(scopeOf('me.wandering.set'), 'own');
|
|
97
|
+
assert.equal(scopeOf('task.claim.any'), null, 'task.claim.any is a POOL filter, not an owner scope');
|
|
98
|
+
assert.equal(isCrossOwner('task.claim.any'), false);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('A3: the modelled families are the ones the ownership sweep names', () => {
|
|
102
|
+
assert.deepEqual(Object.keys(RESOURCE_FAMILIES).sort(),
|
|
103
|
+
['box', 'builder_prefs', 'claim', 'credential', 'goal', 'memory']);
|
|
104
|
+
assert.deepEqual([...ownershipScopedKeys()].sort(),
|
|
105
|
+
['box.manage.own', 'credential.manage.own', 'goal.manage.own', 'me.wandering.set', 'memory.manage.own', 'task.act.own']);
|
|
106
|
+
// The cross-owner side is NOT derivable from spelling: box's is `box.fleet.manage`.
|
|
107
|
+
assert.deepEqual([...crossOwnerKeys()].sort(),
|
|
108
|
+
['box.fleet.manage', 'claim.release.any', 'memory.read.any']);
|
|
109
|
+
assert.deepEqual(crossOwnerKeysFor('task.act.own'), ['claim.release.any']);
|
|
110
|
+
assert.deepEqual(crossOwnerKeysFor('box.manage.own'), ['box.fleet.manage']);
|
|
111
|
+
// Some resources have NO cross-owner form by design — nobody manages another
|
|
112
|
+
// builder's credential, Archon included.
|
|
113
|
+
assert.deepEqual(crossOwnerKeysFor('credential.manage.own'), []);
|
|
114
|
+
assert.deepEqual(crossOwnerKeysFor('nope.not.a.key'), []);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test('A4: RESOURCE_FAMILIES is frozen and agrees with each row it lists', () => {
|
|
118
|
+
assert.ok(Object.isFrozen(RESOURCE_FAMILIES));
|
|
119
|
+
for (const [resource, fam] of Object.entries(RESOURCE_FAMILIES)) {
|
|
120
|
+
assert.ok(Object.isFrozen(fam) && Object.isFrozen(fam.own) && Object.isFrozen(fam.any));
|
|
121
|
+
for (const key of fam.own) {
|
|
122
|
+
assert.equal(resourceOf(key), resource);
|
|
123
|
+
assert.equal(scopeOf(key), 'own');
|
|
124
|
+
}
|
|
125
|
+
for (const key of fam.any) {
|
|
126
|
+
assert.equal(resourceOf(key), resource);
|
|
127
|
+
assert.equal(scopeOf(key), 'any');
|
|
128
|
+
}
|
|
129
|
+
assert.ok(fam.own.length > 0, `${resource}: a cross-owner key with no own counterpart is a mis-declaration`);
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// ═══ B — `.own` does NOT imply `.any` (oracle: floors × the real rank gate) ════
|
|
134
|
+
|
|
135
|
+
test('B1: for every paired family some rank reaches its OWN row but not everyone else\'s', () => {
|
|
136
|
+
assert.ok(PAIRED.length >= 3, `expected the box/claim/memory pairs, saw ${PAIRED.length}`);
|
|
137
|
+
for (const [resource, fam] of PAIRED) {
|
|
138
|
+
for (const ownKey of fam.own) {
|
|
139
|
+
for (const anyKey of fam.any) {
|
|
140
|
+
// Independent oracle: the floors (a pre-R100 field) run through the REAL
|
|
141
|
+
// production threshold function. A separation asserted this way cannot be
|
|
142
|
+
// faked by anything R100 added.
|
|
143
|
+
const separating = RANK_ORDER.filter((rank) =>
|
|
144
|
+
auth.rankMeetsThreshold(rank, [byKey(ownKey).floor]) &&
|
|
145
|
+
!auth.rankMeetsThreshold(rank, [byKey(anyKey).floor]));
|
|
146
|
+
assert.ok(separating.length > 0,
|
|
147
|
+
`${resource}: ${ownKey} and ${anyKey} are held by exactly the same ranks — "own" collapses into "any"`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test('B2: the seed grants the own key to ranks it withholds the cross-owner key from', () => {
|
|
154
|
+
for (const [resource, fam] of PAIRED) {
|
|
155
|
+
for (const ownKey of fam.own) {
|
|
156
|
+
const ownOnly = RANK_ORDER.filter((r) => RANK_ROLE_SEED[r].includes(ownKey) && !fam.any.some((a) => RANK_ROLE_SEED[r].includes(a)));
|
|
157
|
+
assert.ok(ownOnly.length > 0, `${resource}: no rank holds ${ownKey} without also holding ${fam.any.join('/')}`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
// The concrete claim, spelled out: a xenos may act on their own claim and their
|
|
161
|
+
// own box; they may not release another builder's claim or close another's box.
|
|
162
|
+
assert.ok(RANK_ROLE_SEED.xenos.includes('task.act.own'));
|
|
163
|
+
assert.ok(!RANK_ROLE_SEED.xenos.includes('claim.release.any'));
|
|
164
|
+
assert.ok(RANK_ROLE_SEED.xenos.includes('box.manage.own'));
|
|
165
|
+
assert.ok(!RANK_ROLE_SEED.xenos.includes('box.fleet.manage'));
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test('B3: cross-owner authority is never CHEAPER than own-only authority', () => {
|
|
169
|
+
for (const [resource, fam] of PAIRED) {
|
|
170
|
+
for (const anyKey of fam.any) {
|
|
171
|
+
for (const ownKey of fam.own) {
|
|
172
|
+
assert.ok(RANK_ORDER.indexOf(byKey(anyKey).floor) >= RANK_ORDER.indexOf(byKey(ownKey).floor),
|
|
173
|
+
`${resource}: ${anyKey} sits below ${ownKey} — reaching every owner must not be easier than reaching your own`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// ═══ C — THE key test: `.own` still fails against someone else's row ══════════
|
|
180
|
+
|
|
181
|
+
test('C1: holding EVERY permission in the catalog still cannot reach another builder\'s row', async () => {
|
|
182
|
+
// The degradation this whole axis exists to prevent. Even maximal role authority
|
|
183
|
+
// supplies only fact (1); fact (2) is false, so the answer must be no.
|
|
184
|
+
await holding(EVERYTHING, async () => {
|
|
185
|
+
for (const key of ownershipScopedKeys()) {
|
|
186
|
+
assert.equal(await authorizeOwned(ME, key, { ownerId: SOMEONE_ELSE }), false,
|
|
187
|
+
`${key} admitted against another builder's row — ".own" has degraded into an unconditional grant`);
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test('C2: the same key on the caller\'s OWN row admits — but only while they hold it', async () => {
|
|
193
|
+
await holding(EVERYTHING, async () => {
|
|
194
|
+
for (const key of ownershipScopedKeys()) {
|
|
195
|
+
assert.equal(await authorizeOwned(ME, key, { ownerId: ME }), true, `${key} on own row`);
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
// Owning the row is not authority either — the conjunction cuts both ways.
|
|
199
|
+
await holding([], async () => {
|
|
200
|
+
for (const key of ownershipScopedKeys()) {
|
|
201
|
+
assert.equal(await authorizeOwned(ME, key, { ownerId: ME }), false,
|
|
202
|
+
`${key} admitted without the grant — owning a row is not a permission`);
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test('C3: no owner supplied = no admit, however the caller spells the omission', async () => {
|
|
208
|
+
// The structural bit: `ownerId` is not an option, it is the second half of the
|
|
209
|
+
// question. A handler that forgets it gets a denial, never a pass-through.
|
|
210
|
+
await holding(EVERYTHING, async () => {
|
|
211
|
+
for (const context of [undefined, null, {}, { ownerId: null }, { ownerId: undefined }, { ownerId: '' }, { ownerId: {} }, { ownerId: [] }, { ownerId: true }, { ownerId: NaN }]) {
|
|
212
|
+
assert.equal(await authorizeOwned(ME, 'box.manage.own', context), false,
|
|
213
|
+
`an unidentifiable owner (${JSON.stringify(context)}) must deny`);
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
test('C4: authorizeOwned is the OWN entry point only — other keys fail closed', async () => {
|
|
219
|
+
await holding(EVERYTHING, async () => {
|
|
220
|
+
// A cross-owner key is not an ownership-scoped one; it has its own answer via
|
|
221
|
+
// the plain resolve, and must not acquire a second, owner-shaped route in.
|
|
222
|
+
assert.equal(await authorizeOwned(ME, 'claim.release.any', { ownerId: ME }), false);
|
|
223
|
+
// Nor an unscoped operational key, a system key, a principal key, or a typo.
|
|
224
|
+
assert.equal(await authorizeOwned(ME, 'idea.triage', { ownerId: ME }), false);
|
|
225
|
+
assert.equal(await authorizeOwned(ME, 'deploy.push_main', { ownerId: ME }), false);
|
|
226
|
+
assert.equal(await authorizeOwned(ME, 'principal.bfg', { ownerId: ME }), false);
|
|
227
|
+
assert.equal(await authorizeOwned(ME, 'nope.not.a.key', { ownerId: ME }), false);
|
|
228
|
+
// A blank actor cannot own anything, even matching a blank owner.
|
|
229
|
+
assert.equal(await authorizeOwned(null, 'box.manage.own', { ownerId: null }), false);
|
|
230
|
+
assert.equal(await authorizeOwned('', 'box.manage.own', { ownerId: '' }), false);
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
test('C5: builderHasPermissions REFUSES an ownership-scoped key rather than answering half of it', async () => {
|
|
235
|
+
await holding(EVERYTHING, async () => {
|
|
236
|
+
for (const key of ownershipScopedKeys()) {
|
|
237
|
+
assert.equal(await builderHasPermissions(ME, [key]), false,
|
|
238
|
+
`${key}: "do you hold it" has no unconditional answer — the caller wants authorizeOwned()`);
|
|
239
|
+
}
|
|
240
|
+
// One own key poisons the whole AND-list; unscoped keys still answer normally.
|
|
241
|
+
assert.equal(await builderHasPermissions(ME, ['idea.triage', 'box.manage.own']), false);
|
|
242
|
+
assert.equal(await builderHasPermissions(ME, ['idea.triage', 'cost.log']), true);
|
|
243
|
+
assert.equal(await builderHasPermissions(ME, ['claim.release.any']), true);
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
// ═══ D — `.any` DOES reach another builder's resource ═════════════════════════
|
|
248
|
+
|
|
249
|
+
test('D1: the cross-owner key reaches a row the caller does not own', async () => {
|
|
250
|
+
await holding(['task.act.own', 'claim.release.any'], async () => {
|
|
251
|
+
assert.equal(await authorizeOwnedOrAny(ME, 'task.act.own', { ownerId: SOMEONE_ELSE }), true,
|
|
252
|
+
'claim.release.any is exactly the authority to act on someone else\'s claim');
|
|
253
|
+
assert.equal(await authorizeOwnedOrAny(ME, 'task.act.own', { ownerId: ME }), true);
|
|
254
|
+
});
|
|
255
|
+
await holding(['memory.manage.own', 'memory.read.any'], async () => {
|
|
256
|
+
assert.equal(await authorizeOwnedOrAny(ME, 'memory.manage.own', { ownerId: SOMEONE_ELSE }), true);
|
|
257
|
+
});
|
|
258
|
+
await holding(['box.manage.own', 'box.fleet.manage'], async () => {
|
|
259
|
+
assert.equal(await authorizeOwnedOrAny(ME, 'box.manage.own', { ownerId: SOMEONE_ELSE }), true);
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
test('D2: holding only the OWN key never reaches someone else, in either entry point', async () => {
|
|
264
|
+
for (const [, fam] of PAIRED) {
|
|
265
|
+
for (const ownKey of fam.own) {
|
|
266
|
+
await holding([ownKey], async () => {
|
|
267
|
+
assert.equal(await authorizeOwnedOrAny(ME, ownKey, { ownerId: SOMEONE_ELSE }), false,
|
|
268
|
+
`${ownKey} alone must not reach another owner's row`);
|
|
269
|
+
assert.equal(await authorizeOwnedOrAny(ME, ownKey, { ownerId: ME }), true);
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
test('D3: a family with no cross-owner form has no escalation, even at maximal authority', async () => {
|
|
276
|
+
await holding(EVERYTHING, async () => {
|
|
277
|
+
for (const key of ['credential.manage.own', 'me.wandering.set', 'goal.manage.own']) {
|
|
278
|
+
assert.equal(await authorizeOwnedOrAny(ME, key, { ownerId: SOMEONE_ELSE }), false,
|
|
279
|
+
`${key} has no declared cross-owner counterpart — nothing may escalate past it`);
|
|
280
|
+
}
|
|
281
|
+
// and the entry point still refuses a non-own key
|
|
282
|
+
assert.equal(await authorizeOwnedOrAny(ME, 'claim.release.any', { ownerId: ME }), false);
|
|
283
|
+
assert.equal(await authorizeOwnedOrAny(ME, 'nope.not.a.key', { ownerId: ME }), false);
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
// ═══ E — the owner comparison itself ══════════════════════════════════════════
|
|
288
|
+
|
|
289
|
+
test('E1: sameOwner matches scalar ids across the number/string boundary, nothing else', () => {
|
|
290
|
+
// Postgres hands bigints back as strings while JS code carries them as numbers,
|
|
291
|
+
// so 7 and '7' are the same owner.
|
|
292
|
+
assert.equal(sameOwner(7, '7'), true);
|
|
293
|
+
assert.equal(sameOwner('7', 7), true);
|
|
294
|
+
assert.equal(sameOwner(7n, '7'), true);
|
|
295
|
+
assert.equal(sameOwner(7, 7), true);
|
|
296
|
+
assert.equal(sameOwner('abc', 'abc'), true);
|
|
297
|
+
assert.equal(sameOwner(7, 8), false);
|
|
298
|
+
assert.equal(sameOwner('7', '07'), false);
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
test('E2: nothing without a scalar identity ever matches — including two of the same', () => {
|
|
302
|
+
const noIdentity = [null, undefined, '', {}, [], true, false, NaN, Infinity, -Infinity, () => {}];
|
|
303
|
+
for (const v of noIdentity) {
|
|
304
|
+
assert.equal(sameOwner(v, 7), false, `${String(v)} must not match a real id`);
|
|
305
|
+
assert.equal(sameOwner(7, v), false, `${String(v)} must not match a real id`);
|
|
306
|
+
assert.equal(sameOwner(v, v), false, `two unidentifiable values must not "match" each other (${String(v)})`);
|
|
307
|
+
}
|
|
308
|
+
// No trimming, no case-folding: normalizing harder than the data warrants is how
|
|
309
|
+
// a comparison quietly widens.
|
|
310
|
+
assert.equal(sameOwner(' 7', 7), false);
|
|
311
|
+
assert.equal(sameOwner('7 ', '7'), false);
|
|
312
|
+
assert.equal(sameOwner('ABC', 'abc'), false);
|
|
313
|
+
// type confusion via toString/valueOf gets no purchase
|
|
314
|
+
assert.equal(sameOwner({ toString: () => '7' }, 7), false);
|
|
315
|
+
assert.equal(sameOwner([7], 7), false);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
// ═══ F — the load-time invariants actually throw ══════════════════════════════
|
|
319
|
+
// Proven by loading a MUTATED COPY of the real catalog, so what is under test is
|
|
320
|
+
// the shipped assertion block, not a re-implementation of it.
|
|
321
|
+
|
|
322
|
+
function loadMutatedCatalog(label, mutate) {
|
|
323
|
+
const source = fs.readFileSync(path.join(GOV, 'catalog.js'), 'utf8');
|
|
324
|
+
const mutated = mutate(source);
|
|
325
|
+
assert.notEqual(mutated, source, `${label}: the fixture mutation did not apply — the test would be vacuous`);
|
|
326
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gov-catalog-'));
|
|
327
|
+
const file = path.join(dir, 'catalog.js');
|
|
328
|
+
fs.writeFileSync(file, mutated);
|
|
329
|
+
try {
|
|
330
|
+
require(file);
|
|
331
|
+
} finally {
|
|
332
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
test('F1: a ".own" key that lost its declared scope throws at module load', () => {
|
|
337
|
+
assert.throws(
|
|
338
|
+
() => loadMutatedCatalog('missing scope', (s) => s.replace(
|
|
339
|
+
`{ key: 'box.manage.own', system: false, floor: 'xenos', scope: 'own', resource: 'box',`,
|
|
340
|
+
`{ key: 'box.manage.own', system: false, floor: 'xenos',`)),
|
|
341
|
+
/does not declare scope/,
|
|
342
|
+
);
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
test('F2: an ownership scope with no resource family throws at module load', () => {
|
|
346
|
+
assert.throws(
|
|
347
|
+
() => loadMutatedCatalog('scope without resource', (s) => s.replace(
|
|
348
|
+
`scope: 'own', resource: 'box',`, `scope: 'own',`)),
|
|
349
|
+
/scope and resource must come together/,
|
|
350
|
+
);
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
test('F3: a cross-owner key cheaper than its own-scoped counterpart throws at module load', () => {
|
|
354
|
+
assert.throws(
|
|
355
|
+
() => loadMutatedCatalog('inverted floors', (s) => s.replace(
|
|
356
|
+
` { key: 'goal.manage.own',`,
|
|
357
|
+
` { key: 'goal.manage.any', system: false, floor: 'xenos', scope: 'any', resource: 'goal', guards: 'fixture' },\n { key: 'goal.manage.own',`)),
|
|
358
|
+
/must never be cheaper than own-only/,
|
|
359
|
+
);
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
test('F4: a cross-owner key with no ownership-scoped counterpart throws at module load', () => {
|
|
363
|
+
assert.throws(
|
|
364
|
+
() => loadMutatedCatalog('lone any', (s) => s.replace(
|
|
365
|
+
` { key: 'goal.manage.own',`,
|
|
366
|
+
` { key: 'widget.manage.any', system: false, floor: 'archon', scope: 'any', resource: 'widget', guards: 'fixture' },\n { key: 'goal.manage.own',`)),
|
|
367
|
+
/with no ownership-scoped counterpart/,
|
|
368
|
+
);
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
// ═══ G — the R94/R95/R98 invariants this slice must not have disturbed ════════
|
|
372
|
+
|
|
373
|
+
test('G1: the seed is still floor-derived and cumulative — the ownership axis adds no grants', () => {
|
|
374
|
+
for (const rank of RANK_ORDER) {
|
|
375
|
+
const held = new Set(RANK_ROLE_SEED[rank]);
|
|
376
|
+
const tier = RANK_ORDER.indexOf(rank);
|
|
377
|
+
for (const p of PERMISSIONS) {
|
|
378
|
+
assert.equal(held.has(p.key), p.floor !== null && RANK_ORDER.indexOf(p.floor) <= tier,
|
|
379
|
+
`${rank} vs ${p.key}: scope must not touch the floor derivation`);
|
|
380
|
+
}
|
|
381
|
+
assert.deepEqual([...seedForRank(rank)].sort(), [...RANK_ROLE_SEED[rank]].sort());
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
test('G2: the role resolve is untouched — own-scoped keys still resolve, principals still drop', () => {
|
|
386
|
+
// Deliberate: resolveBuilderPermissions stays the raw ROLE-axis answer, which is
|
|
387
|
+
// what R103's parity proof pins. Ownership narrows at the DECISION, not by
|
|
388
|
+
// deleting the key from the resolved set.
|
|
389
|
+
const resolved = effectivePermissions(EVERYTHING);
|
|
390
|
+
for (const key of ownershipScopedKeys()) {
|
|
391
|
+
assert.ok(resolved.includes(key), `${key} must still resolve — parity depends on it`);
|
|
392
|
+
}
|
|
393
|
+
assert.deepEqual(effectivePermissions(['principal.bfg', 'box.manage.own']), ['box.manage.own']);
|
|
394
|
+
assert.deepEqual(effectivePermissions(['idea.triage', 'not.a.real.permission']), ['idea.triage']);
|
|
395
|
+
assert.deepEqual(effectivePermissions([]), []);
|
|
396
|
+
assert.ok(Object.isFrozen(resolved));
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
test('G3: the kernel port carries the ownership axis on the ONE governance port', () => {
|
|
400
|
+
const { governancePort } = require(path.join(GOV, 'routes', 'governance.js'));
|
|
401
|
+
for (const name of ['authorizeOwned', 'authorizeOwnedOrAny', 'sameOwner', 'isOwnershipScoped', 'crossOwnerKeysFor']) {
|
|
402
|
+
assert.equal(typeof governancePort[name], 'function', `the port must expose ${name}`);
|
|
403
|
+
}
|
|
404
|
+
// still the same one object port the earlier slices registered
|
|
405
|
+
assert.equal(typeof governancePort.resolveBuilderPermissions, 'function');
|
|
406
|
+
assert.equal(typeof governancePort.resolvePrincipalPermissions, 'function');
|
|
407
|
+
assert.equal(governancePort.isOwnershipScoped('box.manage.own'), true);
|
|
408
|
+
assert.equal(governancePort.isOwnershipScoped('claim.release.any'), false);
|
|
409
|
+
assert.equal(governancePort.isOwnershipScoped('nope.not.a.key'), false);
|
|
410
|
+
});
|