@bongos/core 1.18.0 → 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 +1740 -640
- 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 +5 -3
- 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 +11 -2
- 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/0142-project-context-is-prose-only.md +41 -0
- package/docs/adr/0143-onboarding-repo-picker-oauth-scope.md +35 -0
- 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 +15 -2
- package/docs/api/openapi.json +2648 -483
- package/docs/api-reference.md +73 -13
- 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 +15 -7
- 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/packaging-the-core.md +13 -0
- 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/upgrading-the-core.md +5 -2
- 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/039_newcomer_friendly_column.sql +5 -0
- package/migrations/102_remove_box_first_provision_approval.sql +4 -0
- package/migrations/core_192_github_oauth_tokens.sql +33 -0
- 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/my-projects.html +78 -0
- package/modules/hall-ui/public/my-projects.js +142 -0
- 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/scouting.html +45 -0
- package/modules/hall-ui/public/scouting.js +73 -0
- 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/modules/status-ui/public/explore.html +54 -0
- package/modules/status-ui/public/explore.js +67 -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 +252 -62
- 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 -21
- 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/lint-migrations.js +102 -0
- package/scripts/gds/login.js +58 -11
- package/scripts/gds/mirror-redact.js +5 -0
- package/scripts/gds/package-core.js +263 -4
- package/scripts/gds/paste-token.js +7 -1
- package/scripts/gds/preflight.js +126 -0
- package/scripts/gds/provision.js +268 -19
- package/scripts/gds/publish-manifest.js +22 -1
- package/scripts/gds/reauth.js +7 -3
- package/scripts/gds/regen-instance-docs.js +47 -0
- package/scripts/gds/run-unit-tests.js +42 -16
- 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 +72 -5
- package/scripts/gds/shell.js +5 -3
- package/scripts/gds/ship.js +433 -59
- 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 +271 -7
- package/scripts/migrate.sh +10 -0
- package/scripts/status-mirror-sync.js +114 -84
- package/src/bongos/api-errors.js +1 -0
- package/src/bongos/auth.js +809 -107
- package/src/bongos/db-kernel.js +27 -0
- package/src/bongos/db.js +105 -0
- package/src/bongos/github-repos.js +93 -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 +312 -13
- package/src/bongos/routes/backup.js +58 -2
- package/src/bongos/routes/github.js +90 -0
- package/src/bongos/routes/instance.js +15 -3
- package/src/bongos/routes/me.js +35 -1
- package/src/bongos/routes.js +2 -0
- package/src/bongos/serve-internal.js +84 -15
- package/src/branding.js +49 -2
- package/src/instance-config.js +46 -0
- package/src/module-api.js +140 -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 +41 -23
- 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/github_repos_helpers.mjs +91 -0
- package/tests/github_token_store.mjs +121 -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 +198 -0
- package/tests/idp_federation.mjs +100 -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/migrations_forward_safe.mjs +86 -0
- package/tests/module_api.mjs +1 -1
- 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/package_core.mjs +246 -1
- 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 +289 -7
- package/tests/pruned_lockfile_integrity.mjs +79 -0
- 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 +140 -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 +393 -5
- package/tests/web_admission_status.mjs +162 -0
- package/tests/write_validation_gaps.mjs +110 -0
- package/.gitignore +0 -91
- package/docs/adr/0139-platform-history-migration-to-cloudbongos.md +0 -94
- package/docs/adr/0140-cloudbongos-standalone-reprovision.md +0 -43
- package/scripts/hall-preview/fixtures/access-requests.json +0 -20
- package/scripts/hall-preview/fixtures/achievements.json +0 -76
- package/scripts/hall-preview/fixtures/analytics__builder.json +0 -251
- package/scripts/hall-preview/fixtures/audit-log.json +0 -68
- package/scripts/hall-preview/fixtures/blockers__detail.json +0 -12
- package/scripts/hall-preview/fixtures/box__me.json +0 -5
- package/scripts/hall-preview/fixtures/boxes__cost-ledger.json +0 -69
- package/scripts/hall-preview/fixtures/builders__active-claims.json +0 -1
- package/scripts/hall-preview/fixtures/builders__directory.json +0 -81
- package/scripts/hall-preview/fixtures/builders__roster.json +0 -48
- package/scripts/hall-preview/fixtures/goals.json +0 -502
- package/scripts/hall-preview/fixtures/goals__25.json +0 -96
- package/scripts/hall-preview/fixtures/goals__26.json +0 -67
- package/scripts/hall-preview/fixtures/goals__3.json +0 -103
- package/scripts/hall-preview/fixtures/goals__35.json +0 -49
- package/scripts/hall-preview/fixtures/goals__conflicts.json +0 -154
- package/scripts/hall-preview/fixtures/goals__detail.json +0 -96
- package/scripts/hall-preview/fixtures/goals__inbox.json +0 -5
- package/scripts/hall-preview/fixtures/goals__requests.json +0 -3
- package/scripts/hall-preview/fixtures/grades__by-builder.json +0 -32
- package/scripts/hall-preview/fixtures/inbox.json +0 -330
- package/scripts/hall-preview/fixtures/inbox__detail.json +0 -15
- package/scripts/hall-preview/fixtures/instance.json +0 -124
- package/scripts/hall-preview/fixtures/leaderboard.json +0 -184
- package/scripts/hall-preview/fixtures/me.json +0 -476
- package/scripts/hall-preview/fixtures/me__art-key__own.json +0 -6
- package/scripts/hall-preview/fixtures/me__event-sound-prefs.json +0 -209
- package/scripts/hall-preview/fixtures/me__recent-ships.json +0 -304
- package/scripts/hall-preview/fixtures/me__render.json +0 -5
- package/scripts/hall-preview/fixtures/me__skill-prefs.json +0 -143
- package/scripts/hall-preview/fixtures/me__sound-prefs.json +0 -67
- package/scripts/hall-preview/fixtures/me__wandering.json +0 -43
- package/scripts/hall-preview/fixtures/override-requests.json +0 -15
- package/scripts/hall-preview/fixtures/provisioning__fleet.json +0 -1
- package/scripts/hall-preview/fixtures/provisioning__fleet__cost-ledger.json +0 -1
- package/scripts/hall-preview/fixtures/provisioning__instances.json +0 -1
- package/scripts/hall-preview/fixtures/public__analytics__ecosystem.json +0 -516
- package/scripts/hall-preview/fixtures/public__bounty-table.json +0 -37
- package/scripts/hall-preview/fixtures/public__cost-summary.json +0 -742
- package/scripts/hall-preview/fixtures/public__estimation-drift.json +0 -41
- package/scripts/hall-preview/fixtures/public__grades.json +0 -50
- package/scripts/hall-preview/fixtures/public__progress.json +0 -124
- package/scripts/hall-preview/fixtures/public__recent-shipped.json +0 -426
- package/scripts/hall-preview/fixtures/public__repo-health.json +0 -160
- package/scripts/hall-preview/fixtures/public__repo-info.json +0 -5
- package/scripts/hall-preview/fixtures/security__docs.json +0 -10
- package/scripts/hall-preview/fixtures/security__reports.json +0 -31
- package/scripts/hall-preview/fixtures/sessions__search.json +0 -9
- package/scripts/hall-preview/fixtures/tasks.json +0 -2420
- package/scripts/hall-preview/fixtures/tasks__claimable.json +0 -2555
- package/scripts/hall-preview/fixtures/tasks__detail.json +0 -111
- package/scripts/hall-preview/fixtures/tasks__peer-votes__feed.json +0 -330
- package/scripts/hall-preview/fixtures/versions.json +0 -89
|
@@ -80,6 +80,78 @@ function renderAuthErrorPage(heading, detail, opts = {}) {
|
|
|
80
80
|
</body></html>`;
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
// renderAccessPendingPage — the calm "hang tight" page shown instead of the red
|
|
84
|
+
// dead-end when admission is plausibly imminent (task 1002097): the configured
|
|
85
|
+
// firstAdmin during the bootstrap window, or a login with a pending request.
|
|
86
|
+
//
|
|
87
|
+
// It polls GET /auth/web/admission-status and, the moment the answer flips to
|
|
88
|
+
// admitted, sends the visitor back through /auth/web/start for a FRESH sign-in.
|
|
89
|
+
// A fresh start is required, not optional: the OAuth code that brought them
|
|
90
|
+
// here is already spent, so re-running the callback would fail on state
|
|
91
|
+
// mismatch. The owner chose auto-walk-in, hence no button to click.
|
|
92
|
+
//
|
|
93
|
+
// Branding resolves defensively, exactly as renderAuthErrorPage does — a
|
|
94
|
+
// branding hiccup must degrade to neutral copy, never crash the page.
|
|
95
|
+
function renderAccessPendingPage(login) {
|
|
96
|
+
const worldName = (() => { try { return branding().identity?.worldName || 'The city'; } catch { return 'The city'; } })();
|
|
97
|
+
const epigraph = (() => { try { return branding().copy?.authError?.epigraph || '— access —'; } catch { return '— access —'; } })();
|
|
98
|
+
// The probe takes the login as a query param; everything interpolated into
|
|
99
|
+
// the page is escaped, and the JS reads it from a data- attribute rather than
|
|
100
|
+
// being built by string concatenation into a script body.
|
|
101
|
+
const safeLogin = escapeHtml(String(login || ''));
|
|
102
|
+
return `<!doctype html>
|
|
103
|
+
<html lang="en"><head>
|
|
104
|
+
<meta charset="utf-8">
|
|
105
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
106
|
+
<title>${escapeHtml(worldName)} — confirming access</title>
|
|
107
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
108
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
109
|
+
<link href="https://fonts.googleapis.com/css2?family=Cinzel:wght@500;700&family=EB+Garamond:ital,wght@0,400;0,500&display=swap" rel="stylesheet">
|
|
110
|
+
<link rel="stylesheet" href="/builders/style.css">
|
|
111
|
+
</head><body>
|
|
112
|
+
<header class="hall">
|
|
113
|
+
<p class="hall__epigraph">${escapeHtml(epigraph)}</p>
|
|
114
|
+
<h1>Confirming your access</h1>
|
|
115
|
+
<p class="hall__sub">Hang tight — this usually takes a few seconds.</p>
|
|
116
|
+
</header>
|
|
117
|
+
<main>
|
|
118
|
+
<section class="scroll">
|
|
119
|
+
<div class="landing" id="pending" data-login="${safeLogin}">
|
|
120
|
+
<p>We're checking your access now. You'll be signed in automatically as soon as it clears — no need to refresh.</p>
|
|
121
|
+
<p class="landing__note" id="pending-note">Checking…</p>
|
|
122
|
+
</div>
|
|
123
|
+
</section>
|
|
124
|
+
</main>
|
|
125
|
+
<script>
|
|
126
|
+
(function () {
|
|
127
|
+
var el = document.getElementById('pending');
|
|
128
|
+
var note = document.getElementById('pending-note');
|
|
129
|
+
var login = el ? el.getAttribute('data-login') : '';
|
|
130
|
+
var EVERY_MS = 3000;
|
|
131
|
+
var GIVE_UP_MS = 5 * 60 * 1000;
|
|
132
|
+
var started = Date.now();
|
|
133
|
+
|
|
134
|
+
function land() { window.location.href = '/api/gds/auth/web/start'; }
|
|
135
|
+
|
|
136
|
+
function tick() {
|
|
137
|
+
if (Date.now() - started > GIVE_UP_MS) {
|
|
138
|
+
note.textContent = 'Still waiting on approval. You can leave this page open, or try signing in again later.';
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
fetch('/api/gds/auth/web/admission-status?login=' + encodeURIComponent(login), { credentials: 'omit' })
|
|
142
|
+
.then(function (r) { return r.ok ? r.json() : null; })
|
|
143
|
+
.then(function (j) {
|
|
144
|
+
if (j && j.admitted) { land(); return; }
|
|
145
|
+
setTimeout(tick, EVERY_MS);
|
|
146
|
+
})
|
|
147
|
+
.catch(function () { setTimeout(tick, EVERY_MS); });
|
|
148
|
+
}
|
|
149
|
+
setTimeout(tick, EVERY_MS);
|
|
150
|
+
})();
|
|
151
|
+
</script>
|
|
152
|
+
</body></html>`;
|
|
153
|
+
}
|
|
154
|
+
|
|
83
155
|
// Cookie names. The web flow writes the new `gds_session` (+ `gds_oauth_state`)
|
|
84
156
|
// names; both flows accept either the new or the legacy `pms_session` /
|
|
85
157
|
// `pms_oauth_state` cookies on read so a builder mid-flow doesn't have to
|
|
@@ -93,6 +165,13 @@ const DISCORD_STATE_COOKIE = 'gds_discord_state';
|
|
|
93
165
|
// flow can land a builder back on the subdomain they started from (#726). Set
|
|
94
166
|
// at /auth/web/start, consumed + cleared at the callback. Defaults to /builders.
|
|
95
167
|
const RETURN_COOKIE = 'gds_return';
|
|
168
|
+
// Short-lived cookie marking that THIS sign-in opted into repo access (the
|
|
169
|
+
// onboarding wizard's `?repos=1`). Set at /auth/web/start, read at the callback so
|
|
170
|
+
// it captures + persists the returned GitHub token — and cleared there. Scoping
|
|
171
|
+
// capture to an explicit intent cookie (not just "the granted scope happens to
|
|
172
|
+
// include repo") means an ordinary hall sign-in never stores a token, even for a
|
|
173
|
+
// user who previously granted the broader scope (task 1002099 / ADR 0143).
|
|
174
|
+
const REPOS_COOKIE = 'gds_oauth_repos';
|
|
96
175
|
|
|
97
176
|
// The apex domain the instance is served under, read from the instance branding
|
|
98
177
|
// pack's cookieDomain (".amazonprimea.com" → "amazonprimea.com",
|
|
@@ -182,6 +261,27 @@ function configuredOAuthOrigin() {
|
|
|
182
261
|
}
|
|
183
262
|
}
|
|
184
263
|
|
|
264
|
+
// The post-sign-in landing used when no explicit ?return= / same-apex Referer
|
|
265
|
+
// was stashed for this sign-in. Configurable per instance via
|
|
266
|
+
// branding.landing.postLogin (env override <PREFIX>_POST_LOGIN_LANDING) so a
|
|
267
|
+
// platform-home instance can land visitors on its OWN front door —
|
|
268
|
+
// cloudbongos.com sets it to /gate/ (the orbs) so a visitor who signs in from
|
|
269
|
+
// the orbs returns to the orbs, never auto-redirected into the builders' hall
|
|
270
|
+
// (task 1002240). Every other instance keeps the portable default '/builders':
|
|
271
|
+
// the hall always exists, a /gate/ orbs site does not. The configured value is
|
|
272
|
+
// run through the SAME open-redirect allowlist as ?return= (safeReturnTo), so a
|
|
273
|
+
// misconfigured landing can never become an open redirect; anything unsafe or
|
|
274
|
+
// absent falls back to '/builders'. Takes an optional resolved pack so the pure
|
|
275
|
+
// path is unit-testable without disk I/O (mirrors clientBranding()).
|
|
276
|
+
function postLoginLanding(brand) {
|
|
277
|
+
try {
|
|
278
|
+
const b = brand || branding();
|
|
279
|
+
return safeReturnTo(b.landing?.postLogin) || '/builders';
|
|
280
|
+
} catch {
|
|
281
|
+
return '/builders';
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
185
285
|
// The OAuth redirect_uri must be a FIXED, registered origin — never derived from
|
|
186
286
|
// the request's x-forwarded-host (spoofable; F2 Hacker finding). Configured via
|
|
187
287
|
// env, falling back to the configured canonical public origin, so an injected host
|
|
@@ -245,10 +345,47 @@ module.exports = function buildAuthRouter() {
|
|
|
245
345
|
}
|
|
246
346
|
});
|
|
247
347
|
|
|
348
|
+
// POST /auth/device/exchange — CLI federation (ADR 0141 §1). The CLI ran the
|
|
349
|
+
// device flow against the HUB and got a hub-signed assertion; it presents it here.
|
|
350
|
+
// The instance verifies the Ed25519 signature OFFLINE against the provisioned hub
|
|
351
|
+
// public key (aud must equal this instance's client_id, unexpired), then runs the
|
|
352
|
+
// instance's OWN admission gate + upsertBuilder + createSession(source='cli') and
|
|
353
|
+
// returns the existing { token, builder }. Only reachable when auth.idp is set.
|
|
354
|
+
//
|
|
355
|
+
// rank: public — an auth-establishment endpoint; the hub-signed assertion IS the
|
|
356
|
+
// bearer and is cryptographically verified before any session is minted.
|
|
357
|
+
router.post('/auth/device/exchange', async (req, res) => {
|
|
358
|
+
if (!auth.idpConfigured()) {
|
|
359
|
+
return res.fail('idp_not_configured', 503);
|
|
360
|
+
}
|
|
361
|
+
if (validateOrRespond(req, res, {
|
|
362
|
+
assertion: { required: true, type: 'string', maxLength: 4096 },
|
|
363
|
+
})) return;
|
|
364
|
+
try {
|
|
365
|
+
const { token, builder } = await auth.completeDeviceExchange({
|
|
366
|
+
assertion: req.body.assertion,
|
|
367
|
+
userAgent: req.headers['user-agent'],
|
|
368
|
+
});
|
|
369
|
+
res.json({ token, builder });
|
|
370
|
+
} catch (err) {
|
|
371
|
+
// Mirror the device/poll admission surface so the CLI prints the same
|
|
372
|
+
// "ask an Archon to invite you" path.
|
|
373
|
+
if (err && err.code === 'NOT_APPROVED') {
|
|
374
|
+
return res.status(403).json({ state: 'not_approved', error: 'not_approved' });
|
|
375
|
+
}
|
|
376
|
+
// A bad / expired / forged / wrong-audience assertion is an auth failure (401),
|
|
377
|
+
// not a server error — never mint a session for it.
|
|
378
|
+
console.error('[gds] /auth/device/exchange', err);
|
|
379
|
+
res.fail('invalid_assertion', { status: 401, message: String(err.message || 'invalid assertion').slice(0, 160) });
|
|
380
|
+
}
|
|
381
|
+
});
|
|
382
|
+
|
|
248
383
|
// /auth/web — browser flow (used by the /builders page).
|
|
249
384
|
// rank: public — entry into the GitHub OAuth handshake.
|
|
250
385
|
router.get('/auth/web/start', (req, res) => {
|
|
251
|
-
|
|
386
|
+
// Either direct-GitHub creds OR platform-IdP federation must be configured
|
|
387
|
+
// (ADR 0141 §1) — a federated instance has no GitHub app of its own.
|
|
388
|
+
if (!auth.authConfigured() && !auth.idpConfigured()) {
|
|
252
389
|
return res.status(503).type('text/plain').send('auth not configured');
|
|
253
390
|
}
|
|
254
391
|
const state = crypto.randomBytes(16).toString('hex');
|
|
@@ -281,12 +418,75 @@ module.exports = function buildAuthRouter() {
|
|
|
281
418
|
...sessionCookieDomainOpts(req),
|
|
282
419
|
});
|
|
283
420
|
}
|
|
284
|
-
|
|
421
|
+
// Opt-in repo access (task 1002099 / ADR 0143): the onboarding wizard passes
|
|
422
|
+
// ?repos=1 so it can list/create the user's repos. Remember the intent in a
|
|
423
|
+
// short-lived cookie (same recipe as the state cookie) and request the broader
|
|
424
|
+
// scope on the direct-GitHub leg. Absent the flag this is a no-op — hall + CLI
|
|
425
|
+
// sign-ins stay identity-only.
|
|
426
|
+
const wantsRepos = req.query.repos === '1' || req.query.repos === 'true';
|
|
427
|
+
if (wantsRepos) {
|
|
428
|
+
res.cookie(REPOS_COOKIE, '1', {
|
|
429
|
+
httpOnly: true,
|
|
430
|
+
sameSite: 'lax',
|
|
431
|
+
secure: req.secure || req.headers['x-forwarded-proto'] === 'https',
|
|
432
|
+
maxAge: 10 * 60 * 1000,
|
|
433
|
+
...sessionCookieDomainOpts(req),
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
// Federate through the hub when configured (ADR 0141 §1); otherwise the direct
|
|
437
|
+
// GitHub authorize, carrying the repo-scope opt-in. Same CSRF state cookie either
|
|
438
|
+
// way — the callback validates it identically, so the hub leg is CSRF-protected too.
|
|
439
|
+
res.redirect(auth.idpConfigured()
|
|
440
|
+
? auth.buildIdpAuthorizeUrl(state, redirectUri)
|
|
441
|
+
: auth.buildAuthorizeUrl(state, redirectUri, { repos: wantsRepos }));
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
// GET /auth/web/admission-status?login=X — the probe renderAccessPendingPage
|
|
445
|
+
// polls (task 1002097). Answers ONE question: would this login be admitted
|
|
446
|
+
// right now? It mirrors the real gate via auth.webAdmissionStatus so the page
|
|
447
|
+
// can never invite someone in the gate would refuse.
|
|
448
|
+
//
|
|
449
|
+
// rank: public — it must be reachable by a visitor with no session (that is
|
|
450
|
+
// the entire situation it exists for). It grants nothing: admission still
|
|
451
|
+
// runs through the normal sign-in, and this only reads state.
|
|
452
|
+
//
|
|
453
|
+
// WHAT IT LEAKS, stated plainly (task 1002430 — an earlier version of this
|
|
454
|
+
// comment claimed the opposite and was wrong). Returning only a boolean does
|
|
455
|
+
// NOT make it non-enumerable: with open enrollment off, and for any login
|
|
456
|
+
// that is not the configured firstAdmin, `admitted:true` means that login is
|
|
457
|
+
// a member or has been invited. That reveals account existence to an
|
|
458
|
+
// unauthenticated caller. It is also NOT throttled — rateLimitMiddleware
|
|
459
|
+
// returns early for non-write methods, so GETs bypass it entirely; the page's
|
|
460
|
+
// 3s poll bounds the honest client, not an attacker.
|
|
461
|
+
//
|
|
462
|
+
// Why it is nonetheless not a new exposure: the onboarding module's
|
|
463
|
+
// GET /access-requests/status?github_login=X is ALREADY public and returns
|
|
464
|
+
// the strictly richer member|invited|pending|dismissed|none plus its own
|
|
465
|
+
// `admitted`. Anything derivable here is already derivable there, so this
|
|
466
|
+
// adds no net-new surface — and hardening only this route would accomplish
|
|
467
|
+
// nothing while that one stays open. Whether unauthenticated login-existence
|
|
468
|
+
// probing is acceptable at all is a real question, but it belongs to both
|
|
469
|
+
// routes together, not to a comment here.
|
|
470
|
+
//
|
|
471
|
+
// That endpoint is also why this one exists: it models neither the firstAdmin
|
|
472
|
+
// bypass nor open enrollment, so it reports the OWNER as 'none' forever —
|
|
473
|
+
// the exact case this page has to resolve.
|
|
474
|
+
router.get('/auth/web/admission-status', async (req, res) => {
|
|
475
|
+
const login = typeof req.query.login === 'string' ? req.query.login.slice(0, 100) : '';
|
|
476
|
+
if (!login) return res.json({ admitted: false });
|
|
477
|
+
try {
|
|
478
|
+
const { admitted } = await auth.webAdmissionStatus(login);
|
|
479
|
+
return res.json({ admitted: !!admitted });
|
|
480
|
+
} catch (err) {
|
|
481
|
+
// Fail closed and quietly — the page just keeps polling.
|
|
482
|
+
console.error('[gds] auth/web/admission-status', err);
|
|
483
|
+
return res.json({ admitted: false });
|
|
484
|
+
}
|
|
285
485
|
});
|
|
286
486
|
|
|
287
487
|
// rank: public — OAuth callback; the state cookie + code are the bearer.
|
|
288
488
|
router.get('/auth/web/callback', async (req, res) => {
|
|
289
|
-
if (!auth.authConfigured()) {
|
|
489
|
+
if (!auth.authConfigured() && !auth.idpConfigured()) {
|
|
290
490
|
return res.status(503).type('text/plain').send('auth not configured');
|
|
291
491
|
}
|
|
292
492
|
const expectedState = parseCookie(req, STATE_COOKIE) || parseCookie(req, STATE_COOKIE_LEGACY);
|
|
@@ -304,14 +504,19 @@ module.exports = function buildAuthRouter() {
|
|
|
304
504
|
));
|
|
305
505
|
}
|
|
306
506
|
try {
|
|
307
|
-
// Must match the PINNED redirect_uri used at /auth/web/start (#734) —
|
|
308
|
-
// GitHub token exchange
|
|
507
|
+
// Must match the PINNED redirect_uri used at /auth/web/start (#734) — both
|
|
508
|
+
// the GitHub token exchange and the hub /sso/token redemption require an
|
|
509
|
+
// exact string match against the value sent at authorize time.
|
|
309
510
|
const redirectUri = `${WEB_REDIRECT_ORIGIN}/api/gds/auth/web/callback`;
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
511
|
+
// Did THIS sign-in opt into repo access? (the wizard's ?repos=1 intent cookie)
|
|
512
|
+
const wantsRepos = parseCookie(req, REPOS_COOKIE) === '1';
|
|
513
|
+
// Federated (hub) vs direct-GitHub — same downstream: admission gate,
|
|
514
|
+
// upsertBuilder, createSession, gds_session cookie (ADR 0141 §1). The direct
|
|
515
|
+
// leg additionally captures the opt-in repo token (task 1002099 / ADR 0143);
|
|
516
|
+
// the hub leg has no direct GitHub token, so the wizard degrades to manual entry.
|
|
517
|
+
const { token, expiresAt, builder, githubToken, githubScope } = auth.idpConfigured()
|
|
518
|
+
? await auth.completeIdpFlow({ code, redirectUri, userAgent: req.headers['user-agent'] })
|
|
519
|
+
: await auth.completeWebFlow({ code, redirectUri, userAgent: req.headers['user-agent'], captureRepoToken: wantsRepos });
|
|
315
520
|
res.cookie(SESSION_COOKIE, token, {
|
|
316
521
|
httpOnly: true,
|
|
317
522
|
sameSite: 'lax',
|
|
@@ -322,26 +527,61 @@ module.exports = function buildAuthRouter() {
|
|
|
322
527
|
// amazonprimea.com + builders. + status. (#726); host-only elsewhere.
|
|
323
528
|
...sessionCookieDomainOpts(req),
|
|
324
529
|
});
|
|
530
|
+
// Persist the wizard's repo token (task 1002099 / ADR 0143): encrypted at rest,
|
|
531
|
+
// short-TTL, keyed to the builder — the repo picker/creator reads it back over
|
|
532
|
+
// the next few minutes. Gated on secret-box being configured (upsertGithubToken
|
|
533
|
+
// no-ops when it isn't). Best-effort: a capture failure must NEVER break sign-in.
|
|
534
|
+
if (wantsRepos && githubToken) {
|
|
535
|
+
try {
|
|
536
|
+
await db.upsertGithubToken({ builderId: builder.id, token: githubToken, scope: githubScope });
|
|
537
|
+
} catch (e) {
|
|
538
|
+
console.error('[gds] web/callback: repo-token capture failed (non-blocking):', e.message);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
325
541
|
// The state cookie is apex-scoped now (#734) — clear it with the same
|
|
326
542
|
// domain or the browser keeps it. Clear the host-only form too for any
|
|
327
|
-
// cookie set before this change.
|
|
543
|
+
// cookie set before this change. Clear the repos-intent cookie the same way.
|
|
328
544
|
const domainOpts = sessionCookieDomainOpts(req);
|
|
329
545
|
res.clearCookie(STATE_COOKIE);
|
|
330
546
|
res.clearCookie(STATE_COOKIE_LEGACY);
|
|
547
|
+
res.clearCookie(REPOS_COOKIE);
|
|
331
548
|
if (domainOpts.domain) {
|
|
332
549
|
res.clearCookie(STATE_COOKIE, domainOpts);
|
|
333
550
|
res.clearCookie(STATE_COOKIE_LEGACY, domainOpts);
|
|
551
|
+
res.clearCookie(REPOS_COOKIE, domainOpts);
|
|
334
552
|
}
|
|
335
553
|
// Land back where sign-in began if a validated return target was stashed
|
|
336
|
-
// (e.g. the builders subdomain bounced through here); else the
|
|
554
|
+
// (e.g. the builders subdomain bounced through here); else the instance's
|
|
555
|
+
// configured post-login landing (default '/builders'; cloudbongos.com →
|
|
556
|
+
// /gate/, the orbs — task 1002240).
|
|
337
557
|
const returnTo = safeReturnTo(parseCookie(req, RETURN_COOKIE));
|
|
338
558
|
res.clearCookie(RETURN_COOKIE, domainOpts);
|
|
339
|
-
res.redirect(returnTo ||
|
|
559
|
+
res.redirect(returnTo || postLoginLanding());
|
|
340
560
|
} catch (err) {
|
|
341
561
|
// Approval gate (2026-05-30): a first-time GitHub account that hasn't been
|
|
342
562
|
// approved by an Archon can't enter. Send them to the hall to request
|
|
343
563
|
// access rather than dead-ending on a generic error.
|
|
344
564
|
if (err && err.code === 'NOT_APPROVED') {
|
|
565
|
+
// task 1002097: a blocked sign-in is not always a rejection. On a fresh
|
|
566
|
+
// or just-restarted instance the configured firstAdmin lands here for a
|
|
567
|
+
// few seconds — before FIRST_ADMIN is loaded / the bootstrap finishes —
|
|
568
|
+
// and so does anyone whose access_request an Archon is about to mark
|
|
569
|
+
// invited. The red dead-end looked broken, and could not be retried:
|
|
570
|
+
// the OAuth code is single-use, so a reload throws "State mismatch"
|
|
571
|
+
// instead. For those two, show a calm waiting page that polls and walks
|
|
572
|
+
// them in by itself. Genuine strangers still get the request-access
|
|
573
|
+
// page below.
|
|
574
|
+
let expectedSoon = false;
|
|
575
|
+
try {
|
|
576
|
+
({ expectedSoon } = await auth.webAdmissionStatus(err.login));
|
|
577
|
+
} catch (probeErr) {
|
|
578
|
+
// Never let the classifier turn a clean 403 into a 500 — fall through
|
|
579
|
+
// to the existing page, which is the safe answer for a stranger.
|
|
580
|
+
console.error('[gds] web/callback admission classify', probeErr);
|
|
581
|
+
}
|
|
582
|
+
if (expectedSoon) {
|
|
583
|
+
return res.status(202).type('text/html').send(renderAccessPendingPage(err.login));
|
|
584
|
+
}
|
|
345
585
|
return res.status(403).type('text/html').send(renderAuthErrorPage(
|
|
346
586
|
'This account is not approved yet',
|
|
347
587
|
'Signing in with GitHub is only for builders an Archon has already approved. If you are new, request access at the builders’ hall — an Archon will approve you, then this sign-in will let you in. If you were just approved, the Archon may still need to mark you invited; give it a moment and try again.',
|
|
@@ -535,9 +775,67 @@ module.exports = function buildAuthRouter() {
|
|
|
535
775
|
res.clearCookie(SESSION_COOKIE, domainOpts);
|
|
536
776
|
res.clearCookie(SESSION_COOKIE_LEGACY, domainOpts);
|
|
537
777
|
}
|
|
778
|
+
// Federated single-logout (ADR 0144, task 1002238): announce the logout so the
|
|
779
|
+
// hub (platform-identity, enabled only on cloudbongos.com) can fan a signed
|
|
780
|
+
// back-channel logout out to every federated project this builder entered. A
|
|
781
|
+
// fire-and-forget seam EMIT — zero listeners on a non-hub instance, so it's a
|
|
782
|
+
// no-op there; the fan-out is best-effort server-side and never blocks or fails
|
|
783
|
+
// this logout (the hub session is already revoked above).
|
|
784
|
+
seams.emit('builder.logout', {
|
|
785
|
+
builderId: req.builder.id,
|
|
786
|
+
githubId: req.builder.github_id,
|
|
787
|
+
githubLogin: req.builder.github_login,
|
|
788
|
+
});
|
|
538
789
|
res.json({ ok: true });
|
|
539
790
|
});
|
|
540
791
|
|
|
792
|
+
// POST /auth/backchannel-logout — the INSTANCE side of federated single-logout
|
|
793
|
+
// (ADR 0144, task 1002238). The hub POSTs a hub-signed logout token here when a
|
|
794
|
+
// builder signs out of the hub; we verify it and revoke ALL of that builder's
|
|
795
|
+
// local sessions, so one hub logout ends the session on every federated project.
|
|
796
|
+
//
|
|
797
|
+
// rank: public — the hub-signed EdDSA logout token IS the credential (like the
|
|
798
|
+
// OAuth callback), NOT a builder session. Inert (404) on an instance that does
|
|
799
|
+
// not federate through a hub (idp unset), so a self-hosted instance exposes no
|
|
800
|
+
// new surface. Idempotent: an unknown/replayed subject just revokes nothing.
|
|
801
|
+
router.post('/auth/backchannel-logout', async (req, res) => {
|
|
802
|
+
const idp = auth.loadIdpConfig();
|
|
803
|
+
if (!idp) {
|
|
804
|
+
// Not a federated instance — indistinguishable from "no such route".
|
|
805
|
+
return res.status(404).type('text/plain').send('not found');
|
|
806
|
+
}
|
|
807
|
+
if (validateOrRespond(req, res, {
|
|
808
|
+
logout_token: { required: true, type: 'string', maxLength: 4096 },
|
|
809
|
+
})) return;
|
|
810
|
+
const publicKey = auth.loadHubPublicKey();
|
|
811
|
+
if (!publicKey) return res.fail('idp_unconfigured', { status: 503, message: 'hub public key not configured' });
|
|
812
|
+
let payload;
|
|
813
|
+
try {
|
|
814
|
+
payload = auth.verifyHubLogoutToken(req.body.logout_token, { publicKey, clientId: idp.clientId });
|
|
815
|
+
} catch (err) {
|
|
816
|
+
// A bad / expired / forged / wrong-audience token is an auth failure (401),
|
|
817
|
+
// never a server error — and never revokes anything.
|
|
818
|
+
return res.fail('invalid_logout_token', { status: 401, message: String(err.message || 'invalid logout token').slice(0, 160) });
|
|
819
|
+
}
|
|
820
|
+
// Anti-replay (defense-in-depth, symmetric with the CLI assertion path): honor
|
|
821
|
+
// a logout token's jti ONCE. A replay within the ~120s TTL is an idempotent
|
|
822
|
+
// no-op, so a captured token can't keep killing sessions the builder creates
|
|
823
|
+
// AFTER it was minted.
|
|
824
|
+
if (auth.logoutTokenJtiSeenAndRecord(payload.jti, payload.exp)) {
|
|
825
|
+
return res.json({ ok: true });
|
|
826
|
+
}
|
|
827
|
+
try {
|
|
828
|
+
const builder = await db.getBuilderByGithubId(payload.sub);
|
|
829
|
+
// Revoke ALL of this builder's local sessions (web + CLI) — the hub said sign
|
|
830
|
+
// them out. No-op + 200 if we don't know this builder (never leak existence).
|
|
831
|
+
if (builder) await db.revokeAllSessionsForBuilder(builder.id);
|
|
832
|
+
res.json({ ok: true });
|
|
833
|
+
} catch (err) {
|
|
834
|
+
console.error('[gds] /auth/backchannel-logout revoke', err);
|
|
835
|
+
res.fail('server_error', { status: 500, message: 'logout failed' });
|
|
836
|
+
}
|
|
837
|
+
});
|
|
838
|
+
|
|
541
839
|
// POST /auth/cli-token/issue — V3.R91 / #401. Issue a fresh CLI bearer for
|
|
542
840
|
// the requesting builder, gated on a WEB COOKIE session (not a bearer). The
|
|
543
841
|
// browser already proves identity via the gds_session cookie; this endpoint
|
|
@@ -753,6 +1051,7 @@ module.exports = function buildAuthRouter() {
|
|
|
753
1051
|
// open-redirect guard on the post-sign-in return target and the cross-subdomain
|
|
754
1052
|
// cookie-domain decision — worth locking down independently of a running server.
|
|
755
1053
|
module.exports.safeReturnTo = safeReturnTo;
|
|
1054
|
+
module.exports.postLoginLanding = postLoginLanding;
|
|
756
1055
|
module.exports.sessionCookieDomainOpts = sessionCookieDomainOpts;
|
|
757
1056
|
module.exports.reqHost = reqHost;
|
|
758
1057
|
// The auth-error page renderer (task 1439): pure over the branding pack, so a
|
|
@@ -37,7 +37,39 @@ const BACKUP_SCRIPT = process.env.GDS_BACKUP_SCRIPT
|
|
|
37
37
|
// In-memory guard: true while a trigger-initiated dump is running.
|
|
38
38
|
let triggerRunning = false;
|
|
39
39
|
|
|
40
|
-
|
|
40
|
+
// A dump older than this counts as STALE. The nightly timer fires daily, so 36h
|
|
41
|
+
// allows one missed run plus slack before we call it broken.
|
|
42
|
+
const STALE_AFTER_HOURS = Number(process.env.GDS_BACKUP_STALE_HOURS) || 36;
|
|
43
|
+
|
|
44
|
+
// parseDumpTimestamp — the dump filename stamp `YYYY-MM-DDTHHMMSSZ` is ALMOST
|
|
45
|
+
// ISO-8601 but omits the time separators, so Date.parse rejects it. Re-insert
|
|
46
|
+
// the colons and parse. Returns epoch ms, or null if it doesn't match.
|
|
47
|
+
// Pure + exported so tests can pin it without touching the filesystem.
|
|
48
|
+
function parseDumpTimestamp(stamp) {
|
|
49
|
+
const m = /^(\d{4}-\d{2}-\d{2})T(\d{2})(\d{2})(\d{2})Z$/.exec(String(stamp || ''));
|
|
50
|
+
if (!m) return null;
|
|
51
|
+
const ms = Date.parse(`${m[1]}T${m[2]}:${m[3]}:${m[4]}Z`);
|
|
52
|
+
return Number.isFinite(ms) ? ms : null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// dumpAgeHours — hours between a dump stamp and now. null when unparseable, so
|
|
56
|
+
// callers can tell "cannot tell" apart from "fresh". Pure (now is injectable).
|
|
57
|
+
function dumpAgeHours(stamp, nowMs = Date.now()) {
|
|
58
|
+
const ms = parseDumpTimestamp(stamp);
|
|
59
|
+
if (ms === null) return null;
|
|
60
|
+
return Math.max(0, (nowMs - ms) / 3_600_000);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// isStaleDump — fail LOUD on an unreadable/absent stamp. A dump we cannot date
|
|
64
|
+
// must not read as healthy: that silence is exactly how the cloudbongos nightly
|
|
65
|
+
// sat failed for 13 days (2026-07-15 → 07-28) with nothing noticing (1002479).
|
|
66
|
+
function isStaleDump(stamp, nowMs = Date.now(), staleAfterHours = STALE_AFTER_HOURS) {
|
|
67
|
+
const age = dumpAgeHours(stamp, nowMs);
|
|
68
|
+
if (age === null) return true;
|
|
69
|
+
return age > staleAfterHours;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function latestDump(nowMs = Date.now()) {
|
|
41
73
|
let files;
|
|
42
74
|
try {
|
|
43
75
|
files = fs.readdirSync(BACKUP_DIR);
|
|
@@ -54,7 +86,18 @@ function latestDump() {
|
|
|
54
86
|
try { size_bytes = fs.statSync(full).size; } catch { /* ok */ }
|
|
55
87
|
// Filename: <db>-<YYYY-MM-DDTHHMMSSZ>.sql.gz — extract timestamp from it.
|
|
56
88
|
const ts = name.replace(`${DB_NAME}-`, '').replace('.sql.gz', '');
|
|
57
|
-
|
|
89
|
+
const age = dumpAgeHours(ts, nowMs);
|
|
90
|
+
return {
|
|
91
|
+
filename: name,
|
|
92
|
+
timestamp: ts,
|
|
93
|
+
size_bytes,
|
|
94
|
+
count: dumps.length,
|
|
95
|
+
// Staleness is reported, not just implied by the timestamp, so a consumer
|
|
96
|
+
// (CLI, dashboard, alert) can act on one boolean instead of re-deriving it.
|
|
97
|
+
age_hours: age === null ? null : Math.round(age * 10) / 10,
|
|
98
|
+
stale: isStaleDump(ts, nowMs),
|
|
99
|
+
stale_after_hours: STALE_AFTER_HOURS,
|
|
100
|
+
};
|
|
58
101
|
}
|
|
59
102
|
|
|
60
103
|
module.exports = function buildBackupRouter() {
|
|
@@ -68,6 +111,11 @@ module.exports = function buildBackupRouter() {
|
|
|
68
111
|
backup_dir: BACKUP_DIR,
|
|
69
112
|
db_name: DB_NAME,
|
|
70
113
|
latest_dump: dump,
|
|
114
|
+
// Top-level too, so a consumer gets a health signal WITHOUT having to
|
|
115
|
+
// special-case latest_dump===null. No dump at all is the worst state,
|
|
116
|
+
// not a neutral one, so it reports stale.
|
|
117
|
+
stale: dump ? dump.stale : true,
|
|
118
|
+
age_hours: dump ? dump.age_hours : null,
|
|
71
119
|
trigger_running: triggerRunning,
|
|
72
120
|
});
|
|
73
121
|
}));
|
|
@@ -102,3 +150,11 @@ module.exports = function buildBackupRouter() {
|
|
|
102
150
|
|
|
103
151
|
return router;
|
|
104
152
|
};
|
|
153
|
+
|
|
154
|
+
// Pure helpers exported for tests (tests/backup_cli.mjs) — importing the real
|
|
155
|
+
// implementations beats re-implementing them in the test, which is how a test
|
|
156
|
+
// can pass while the shipped code is wrong.
|
|
157
|
+
module.exports.parseDumpTimestamp = parseDumpTimestamp;
|
|
158
|
+
module.exports.dumpAgeHours = dumpAgeHours;
|
|
159
|
+
module.exports.isStaleDump = isStaleDump;
|
|
160
|
+
module.exports.STALE_AFTER_HOURS = STALE_AFTER_HOURS;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// src/bongos/routes/github.js — GitHub repo list/create for the signed-in user, the
|
|
2
|
+
// server half of the onboarding wizard's repo picker (adopt) + auto-create (fresh).
|
|
3
|
+
// Task 1002099 / ADR 0143.
|
|
4
|
+
//
|
|
5
|
+
// These act on the user's OWN GitHub identity, so they live in core auth alongside
|
|
6
|
+
// the sign-in flow that captured the token — not the provisioning module (whose
|
|
7
|
+
// charter holds no tokens / calls no cloud API, ADR 0111 §2). They read the
|
|
8
|
+
// short-lived, secret-box-encrypted `public_repo` token the web callback stored
|
|
9
|
+
// (db.js github_oauth_tokens). Two "please reconnect / fall back" signals the wizard
|
|
10
|
+
// branches on:
|
|
11
|
+
// • no stored token → repo_scope_required (403) → prompt a repos sign-in
|
|
12
|
+
// • secret-box unconfigured → github_repos_unavailable (503) → manual repo entry
|
|
13
|
+
// Both are behind requireBuilder + the router-wide rate-limit middleware.
|
|
14
|
+
|
|
15
|
+
const express = require('express');
|
|
16
|
+
const auth = require('../auth');
|
|
17
|
+
const db = require('../db');
|
|
18
|
+
const secretBox = require('../secret-box');
|
|
19
|
+
const { listUserRepos, createUserRepo } = require('../github-repos');
|
|
20
|
+
const { validateOrRespond } = require('./_helpers');
|
|
21
|
+
|
|
22
|
+
// GitHub repo names: 1–100 chars of [A-Za-z0-9._-]. We validate strictly for a clean
|
|
23
|
+
// 400; anything GitHub still rejects surfaces as the 422→409 name_taken path.
|
|
24
|
+
const REPO_NAME_RE = /^[A-Za-z0-9._-]{1,100}$/;
|
|
25
|
+
|
|
26
|
+
// Resolve the caller's stored repo token, or send the right failure and return null.
|
|
27
|
+
// Centralizes the 503-vs-403 decision both endpoints share.
|
|
28
|
+
async function resolveRepoToken(req, res) {
|
|
29
|
+
if (!secretBox.isConfigured()) {
|
|
30
|
+
res.fail('github_repos_unavailable', { status: 503, message: 'Repo access is not configured on this server. Enter the repository manually.' });
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
const row = await db.getGithubToken({ builderId: req.builder.id });
|
|
34
|
+
if (!row) {
|
|
35
|
+
res.fail('repo_scope_required', { status: 403, message: 'Grant repository access to list or create repos.' });
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
return row.token;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
module.exports = function buildGithubRouter() {
|
|
42
|
+
const router = express.Router();
|
|
43
|
+
|
|
44
|
+
// rank: any-builder — acts only on the caller's OWN GitHub identity (the token
|
|
45
|
+
// they granted). No rank gate beyond being a signed-in builder.
|
|
46
|
+
router.get('/github/repos', auth.requireBuilder, async (req, res) => {
|
|
47
|
+
const token = await resolveRepoToken(req, res);
|
|
48
|
+
if (!token) return;
|
|
49
|
+
try {
|
|
50
|
+
const q = typeof req.query.q === 'string' ? req.query.q.slice(0, 100) : '';
|
|
51
|
+
const repos = await listUserRepos(token, { q });
|
|
52
|
+
res.json({ repos });
|
|
53
|
+
} catch (err) {
|
|
54
|
+
if (err && err.status === 401) {
|
|
55
|
+
// The stored token was revoked/expired GitHub-side — drop it and re-prompt.
|
|
56
|
+
await db.deleteGithubToken({ builderId: req.builder.id }).catch(() => {});
|
|
57
|
+
return res.fail('repo_scope_required', { status: 403, message: 'Your GitHub authorization expired. Reconnect to list your repos.' });
|
|
58
|
+
}
|
|
59
|
+
console.error('[gds] GET /github/repos', err && err.message);
|
|
60
|
+
res.fail('github_error', { status: 502, message: 'GitHub could not be reached. Try again, or enter the repository manually.' });
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// rank: any-builder — creates a repo in the caller's OWN account.
|
|
65
|
+
router.post('/github/repos', auth.requireBuilder, async (req, res) => {
|
|
66
|
+
if (validateOrRespond(req, res, { name: { required: true, type: 'string', maxLength: 100 } })) return;
|
|
67
|
+
const name = String((req.body && req.body.name) || '').trim();
|
|
68
|
+
if (!REPO_NAME_RE.test(name)) {
|
|
69
|
+
return res.fail('bad_repo_name', { status: 400, message: 'Repository names use letters, numbers, and . _ - (1–100 chars).' });
|
|
70
|
+
}
|
|
71
|
+
const token = await resolveRepoToken(req, res);
|
|
72
|
+
if (!token) return;
|
|
73
|
+
try {
|
|
74
|
+
const repo = await createUserRepo(token, { name });
|
|
75
|
+
res.status(201).json({ repo });
|
|
76
|
+
} catch (err) {
|
|
77
|
+
if (err && err.code === 'name_taken') {
|
|
78
|
+
return res.fail('name_taken', { status: 409, message: 'You already have a repository with that name. Pick another, or adopt the existing one.' });
|
|
79
|
+
}
|
|
80
|
+
if (err && err.status === 401) {
|
|
81
|
+
await db.deleteGithubToken({ builderId: req.builder.id }).catch(() => {});
|
|
82
|
+
return res.fail('repo_scope_required', { status: 403, message: 'Your GitHub authorization expired. Reconnect to create the repo.' });
|
|
83
|
+
}
|
|
84
|
+
console.error('[gds] POST /github/repos', err && err.message);
|
|
85
|
+
res.fail('github_error', { status: 502, message: 'GitHub could not create the repo. Try again, or create it manually.' });
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
return router;
|
|
90
|
+
};
|
|
@@ -32,7 +32,7 @@ const { corsPublicGet, asyncHandler } = require('./_helpers');
|
|
|
32
32
|
const { clientBranding } = require('../../branding');
|
|
33
33
|
const { clientModules, isModuleEnabled } = require('../../modules');
|
|
34
34
|
const { loadRepoInfo } = require('../repo-info');
|
|
35
|
-
const { authConfigured, openEnrollmentEnabled } = require('../auth');
|
|
35
|
+
const { authConfigured, openEnrollmentEnabled, loadIdpConfig } = require('../auth');
|
|
36
36
|
const { RANK_TIER } = require('../permission-path-check');
|
|
37
37
|
|
|
38
38
|
// The manifest format id — bumped when the SHAPE changes so a CLI can detect a
|
|
@@ -59,6 +59,7 @@ function buildInstanceManifest({
|
|
|
59
59
|
authConfigured: authOk,
|
|
60
60
|
openEnrollment,
|
|
61
61
|
devBoxEnabled,
|
|
62
|
+
idp = null,
|
|
62
63
|
} = {}) {
|
|
63
64
|
const b = branding || {};
|
|
64
65
|
const rankLabels = b.rankLabels || {};
|
|
@@ -89,8 +90,15 @@ function buildInstanceManifest({
|
|
|
89
90
|
ranks,
|
|
90
91
|
repo: repoOut,
|
|
91
92
|
auth: {
|
|
92
|
-
|
|
93
|
-
|
|
93
|
+
// Sign-in works when the instance has its OWN GitHub app (authOk) OR it
|
|
94
|
+
// federates through the hub (idp) — a hosted instance has no app of its own.
|
|
95
|
+
methods: (authOk || idp) ? AUTH_METHODS.slice() : [],
|
|
96
|
+
configured: !!authOk || !!idp,
|
|
97
|
+
// Federation hint (ADR 0141 §1): present only when this instance federates.
|
|
98
|
+
// origin + client_id are PUBLIC (like an OAuth client_id); the client SECRET
|
|
99
|
+
// is never projected here. The CLI reads this to route device-flow + web
|
|
100
|
+
// sign-in through the hub instead of expecting a local GitHub app.
|
|
101
|
+
idp: idp || null,
|
|
94
102
|
},
|
|
95
103
|
admission: { policy: openEnrollment ? 'open' : 'invite-only' },
|
|
96
104
|
devBox: { available: !!devBoxEnabled },
|
|
@@ -109,6 +117,7 @@ module.exports = function buildInstanceRouter() {
|
|
|
109
117
|
// err.message if branding/modules fail to resolve (misconfigured instance) —
|
|
110
118
|
// never leaks internals.
|
|
111
119
|
router.get('/instance', asyncHandler('GET /instance', (_req, res) => {
|
|
120
|
+
const idpCfg = loadIdpConfig();
|
|
112
121
|
res.json(buildInstanceManifest({
|
|
113
122
|
branding: clientBranding(),
|
|
114
123
|
modules: clientModules(),
|
|
@@ -117,6 +126,9 @@ module.exports = function buildInstanceRouter() {
|
|
|
117
126
|
authConfigured: authConfigured(),
|
|
118
127
|
openEnrollment: openEnrollmentEnabled(),
|
|
119
128
|
devBoxEnabled: isModuleEnabled('dev-box'),
|
|
129
|
+
// Project OUT the client secret — the manifest is public; only origin +
|
|
130
|
+
// client_id (both public) advertise federation.
|
|
131
|
+
idp: idpCfg ? { origin: idpCfg.origin, client_id: idpCfg.clientId } : null,
|
|
120
132
|
}));
|
|
121
133
|
}, { message: false, errorCode: 'instance_manifest_failed' }));
|
|
122
134
|
|