@bongos/core 1.18.3 → 1.18.19
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 +1576 -286
- 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/migrate.sh +99 -14
- 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 +124 -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/migrate_root_resolution.mjs +124 -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/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,402 @@
|
|
|
1
|
+
// modules/platform-identity/routes/sso.js — the hub SSO login-handoff surface
|
|
2
|
+
// (ADR 0141 §1/§2, task #1002106). Two endpoints:
|
|
3
|
+
//
|
|
4
|
+
// GET /sso/authorize — the browser leg. Gated on a LIVE HUB SESSION (resolved
|
|
5
|
+
// inline; bounces to the hub's own sign-in when absent).
|
|
6
|
+
// Validates the client + redirect_uri, mints a single-use
|
|
7
|
+
// code, and 302s back to the instance's redirect_uri.
|
|
8
|
+
// POST /sso/token — the server-to-server leg. Authenticated by client_id +
|
|
9
|
+
// client_secret (constant-time). Atomically redeems the
|
|
10
|
+
// code and returns IDENTITY ONLY — never a rank.
|
|
11
|
+
//
|
|
12
|
+
// Reaches core ONLY through the doorway (src/module-api.js, the one-way rule,
|
|
13
|
+
// ADR 0083); requires this module's own platform-identity.js (same package),
|
|
14
|
+
// never a sibling module.
|
|
15
|
+
'use strict';
|
|
16
|
+
|
|
17
|
+
const express = require('express');
|
|
18
|
+
const api = require('../../../src/module-api');
|
|
19
|
+
const pi = require('../platform-identity');
|
|
20
|
+
const hubKeys = require('../hub-keys');
|
|
21
|
+
const hubDevice = require('../hub-device');
|
|
22
|
+
|
|
23
|
+
const { pool, validateOrRespond, resolveSession } = api;
|
|
24
|
+
|
|
25
|
+
// Minimal standalone error page for the browser leg (no template engine / deps).
|
|
26
|
+
// The /sso/authorize failures are pre-redirect (bad client / bad redirect_uri), so
|
|
27
|
+
// we must render our OWN page rather than bounce to an unvalidated URI.
|
|
28
|
+
function errorPage(detail) {
|
|
29
|
+
const esc = (s) => String(s ?? '').replace(/[&<>"']/g, (c) => (
|
|
30
|
+
{ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]
|
|
31
|
+
));
|
|
32
|
+
return `<!doctype html><html lang="en"><head><meta charset="utf-8">`
|
|
33
|
+
+ `<meta name="viewport" content="width=device-width, initial-scale=1">`
|
|
34
|
+
+ `<title>Sign-in handoff failed</title></head><body>`
|
|
35
|
+
+ `<h1>Sign-in handoff failed</h1><p>${esc(detail)}</p>`
|
|
36
|
+
+ `<p>This link came from a project trying to sign you in through Cloud Bongos. `
|
|
37
|
+
+ `If it keeps happening, the project's sign-in may be misconfigured.</p></body></html>`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// --- Federated single-logout fan-out (ADR 0144, task 1002238) --------------
|
|
41
|
+
// When a builder signs out of the HUB, core emits a `builder.logout` seam event;
|
|
42
|
+
// this module (enabled only on the hub) reacts by sending a hub-signed logout
|
|
43
|
+
// token to every federated project the builder entered, so each revokes its own
|
|
44
|
+
// local session too. Back-channel (server-to-server) so sign-out sticks even if
|
|
45
|
+
// the browser closes the tab, and doesn't depend on third-party-cookie rules.
|
|
46
|
+
|
|
47
|
+
const SLO_TIMEOUT_MS = 4000; // per-instance notify budget — a slow/down project must never wedge the sweep
|
|
48
|
+
|
|
49
|
+
// Registered once (guarded against a route rebuild, e.g. in tests) at first mount.
|
|
50
|
+
let _logoutListenerOff = null;
|
|
51
|
+
|
|
52
|
+
// POST one hub-signed logout token to a single federated instance, bounded by a
|
|
53
|
+
// short timeout. NEVER throws (the caller runs it under Promise.allSettled + this
|
|
54
|
+
// try/catch), so one unreachable project can't sink the others.
|
|
55
|
+
async function notifyInstanceLogout(membership, githubId) {
|
|
56
|
+
const origin = String(membership.project_origin).replace(/\/+$/, '');
|
|
57
|
+
try {
|
|
58
|
+
const token = hubKeys.mintHubLogoutToken({ clientId: membership.client_id, githubId });
|
|
59
|
+
const ac = new AbortController();
|
|
60
|
+
const timer = setTimeout(() => ac.abort(), SLO_TIMEOUT_MS);
|
|
61
|
+
try {
|
|
62
|
+
const resp = await fetch(`${origin}/api/gds/auth/backchannel-logout`, {
|
|
63
|
+
method: 'POST',
|
|
64
|
+
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
|
65
|
+
body: JSON.stringify({ logout_token: token }),
|
|
66
|
+
signal: ac.signal,
|
|
67
|
+
});
|
|
68
|
+
if (!resp.ok) console.warn(`[platform-identity] SLO: ${origin} returned ${resp.status}`);
|
|
69
|
+
} finally {
|
|
70
|
+
clearTimeout(timer);
|
|
71
|
+
}
|
|
72
|
+
} catch (err) {
|
|
73
|
+
console.warn(`[platform-identity] SLO: notifying ${origin} failed (non-blocking):`, err && err.message ? err.message : err);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Fan a back-channel logout out to every federated project the builder entered.
|
|
78
|
+
// Runs off a fire-and-forget event, so it MUST NOT throw (a floating rejection
|
|
79
|
+
// would be an unhandledRejection). Over-notifying is safe: an instance with no
|
|
80
|
+
// live session for the builder just revokes nothing (idempotent). Uses the
|
|
81
|
+
// builder's known memberships as the entered-instance list (ADR 0141 §3 rows) —
|
|
82
|
+
// the task's sanctioned V1 signal; a precise per-session entry ledger is a follow-up.
|
|
83
|
+
async function fanOutBackchannelLogout(githubId) {
|
|
84
|
+
try {
|
|
85
|
+
if (githubId == null) return;
|
|
86
|
+
// Only the hub mints logout tokens; no signing key ⇒ not the hub ⇒ nothing to do
|
|
87
|
+
// (defense-in-depth beyond the module being hub-only).
|
|
88
|
+
if (!hubKeys.hubSigningConfigured()) return;
|
|
89
|
+
const memberships = await pi.listMembershipsForAccount(githubId, { pool });
|
|
90
|
+
const targets = (memberships || []).filter((m) => m.project_origin && m.client_id);
|
|
91
|
+
await Promise.allSettled(targets.map((m) => notifyInstanceLogout(m, githubId)));
|
|
92
|
+
} catch (err) {
|
|
93
|
+
console.error('[platform-identity] SLO fan-out failed (non-blocking):', err && err.message ? err.message : err);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
module.exports = function ssoRoutes() {
|
|
98
|
+
const router = express.Router();
|
|
99
|
+
|
|
100
|
+
// Wire the federated single-logout fan-out to core's `builder.logout` event,
|
|
101
|
+
// once. The handler is deliberately fire-and-forget (fanOut self-isolates), so
|
|
102
|
+
// the hub's own /auth/logout returns immediately and the sweep runs server-side.
|
|
103
|
+
if (!_logoutListenerOff) {
|
|
104
|
+
_logoutListenerOff = api.on('builder.logout', ({ githubId }) => { fanOutBackchannelLogout(githubId); });
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// GET /sso/authorize?client_id=&redirect_uri=&state= — the browser authorize leg.
|
|
108
|
+
//
|
|
109
|
+
// rank: public — this is an authentication entry point, not an authorization
|
|
110
|
+
// surface. It is gated on a LIVE HUB SESSION (resolved inline via
|
|
111
|
+
// resolveSession; a missing session 302s to the hub's own web sign-in and
|
|
112
|
+
// returns here), NOT on a builder RANK. It mints identity-only codes.
|
|
113
|
+
router.get('/sso/authorize', async (req, res) => {
|
|
114
|
+
const clientId = String(req.query.client_id || '');
|
|
115
|
+
const redirectUri = String(req.query.redirect_uri || '');
|
|
116
|
+
const state = req.query.state != null ? String(req.query.state) : '';
|
|
117
|
+
|
|
118
|
+
if (!clientId || !redirectUri) {
|
|
119
|
+
return res.status(400).type('text/html').send(errorPage('Missing client_id or redirect_uri.'));
|
|
120
|
+
}
|
|
121
|
+
// Bound the query params (the POST leg validates via validateOrRespond; GET
|
|
122
|
+
// params are read raw, so cap them here). An oversized value is never useful
|
|
123
|
+
// and only amplifies work / the reflected Location header.
|
|
124
|
+
if (clientId.length > 200 || redirectUri.length > 2048 || state.length > 512) {
|
|
125
|
+
return res.status(400).type('text/html').send(errorPage('A sign-in parameter was too long.'));
|
|
126
|
+
}
|
|
127
|
+
// Validate the client + redirect_uri BEFORE doing anything else, and never
|
|
128
|
+
// redirect to an unvalidated URI (open-redirect guard, ADR 0141 §2 #3).
|
|
129
|
+
let client;
|
|
130
|
+
try {
|
|
131
|
+
client = await pi.getClient(clientId, { pool });
|
|
132
|
+
} catch (err) {
|
|
133
|
+
console.error('[platform-identity] /sso/authorize getClient', err);
|
|
134
|
+
return res.status(500).type('text/html').send(errorPage('Internal error resolving the project.'));
|
|
135
|
+
}
|
|
136
|
+
if (!client || client.status !== 'active') {
|
|
137
|
+
return res.status(400).type('text/html').send(errorPage('Unknown or suspended project (client_id).'));
|
|
138
|
+
}
|
|
139
|
+
if (!pi.redirectUriAllowed(client, redirectUri)) {
|
|
140
|
+
return res.status(400).type('text/html').send(errorPage('The redirect_uri is not registered for this project.'));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Gate on a live hub session; bounce to the hub's own sign-in if absent, then
|
|
144
|
+
// return here to complete. safeReturnTo on the web flow validates the relative
|
|
145
|
+
// return path, so req.originalUrl (same-host, relative) is accepted.
|
|
146
|
+
//
|
|
147
|
+
// resolveSession is the RAW resolver (so we can redirect rather than 401), so it
|
|
148
|
+
// does NOT apply requireBuilder's deny gates — we must. sessionUsableForFederation
|
|
149
|
+
// rejects a deactivated account, a dev-box-scoped token (outside the ADR 0016
|
|
150
|
+
// trust boundary), and service principals: none may mint a federated identity code.
|
|
151
|
+
let session;
|
|
152
|
+
try {
|
|
153
|
+
session = await resolveSession(req);
|
|
154
|
+
} catch (err) {
|
|
155
|
+
console.error('[platform-identity] /sso/authorize resolveSession', err);
|
|
156
|
+
return res.status(500).type('text/html').send(errorPage('Internal error checking your hub session.'));
|
|
157
|
+
}
|
|
158
|
+
const usable = pi.sessionUsableForFederation(session);
|
|
159
|
+
if (!usable.ok) {
|
|
160
|
+
if (usable.reason === 'no_session') {
|
|
161
|
+
return res.redirect(302, `/api/gds/auth/web/start?return=${encodeURIComponent(req.originalUrl)}`);
|
|
162
|
+
}
|
|
163
|
+
const msg = usable.reason === 'inactive'
|
|
164
|
+
? 'This account is no longer active. Ask an Archon on the hub if you believe this is an error.'
|
|
165
|
+
: usable.reason === 'box_scope'
|
|
166
|
+
? 'A dev-box session cannot be used for cross-project sign-in. Sign in from a browser or run /builder-reauth.'
|
|
167
|
+
: 'This session type cannot be used for cross-project sign-in.';
|
|
168
|
+
return res.status(403).type('text/html').send(errorPage(msg));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// A live hub session — record/refresh the platform account and mint a
|
|
172
|
+
// single-use code bound to (client_id, github_id, redirect_uri).
|
|
173
|
+
try {
|
|
174
|
+
await pi.upsertAccount({
|
|
175
|
+
githubId: session.github_id,
|
|
176
|
+
githubLogin: session.github_login,
|
|
177
|
+
displayName: session.display_name,
|
|
178
|
+
avatarUrl: session.avatar_url,
|
|
179
|
+
}, { pool });
|
|
180
|
+
const code = await pi.mintAuthCode({
|
|
181
|
+
clientId,
|
|
182
|
+
githubId: session.github_id,
|
|
183
|
+
githubLogin: session.github_login,
|
|
184
|
+
displayName: session.display_name,
|
|
185
|
+
avatarUrl: session.avatar_url,
|
|
186
|
+
redirectUri,
|
|
187
|
+
}, { pool });
|
|
188
|
+
const u = new URL(redirectUri);
|
|
189
|
+
u.searchParams.set('code', code);
|
|
190
|
+
if (state) u.searchParams.set('state', state);
|
|
191
|
+
return res.redirect(302, u.toString());
|
|
192
|
+
} catch (err) {
|
|
193
|
+
console.error('[platform-identity] /sso/authorize mint', err);
|
|
194
|
+
return res.status(500).type('text/html').send(errorPage('Could not complete the sign-in handoff.'));
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// POST /sso/token — server-to-server code redemption.
|
|
199
|
+
//
|
|
200
|
+
// rank: public — authenticated by client_id + client_secret (constant-time hash
|
|
201
|
+
// compare in clientSecretValid), NOT a builder session/rank. This is the
|
|
202
|
+
// confidential-client leg; the instance calls it from its server, never the
|
|
203
|
+
// browser. Returns IDENTITY ONLY (github_id, login, display_name, avatar_url).
|
|
204
|
+
router.post('/sso/token', async (req, res) => {
|
|
205
|
+
if (validateOrRespond(req, res, {
|
|
206
|
+
client_id: { required: true, type: 'string', maxLength: 200 },
|
|
207
|
+
client_secret: { required: true, type: 'string', maxLength: 512 },
|
|
208
|
+
code: { required: true, type: 'string', maxLength: 512 },
|
|
209
|
+
redirect_uri: { required: true, type: 'string', maxLength: 2048 },
|
|
210
|
+
})) return;
|
|
211
|
+
|
|
212
|
+
const { client_id, client_secret, code, redirect_uri } = req.body || {};
|
|
213
|
+
try {
|
|
214
|
+
const client = await pi.getClient(client_id, { pool });
|
|
215
|
+
// ALWAYS run the constant-work secret check (clientSecretValid hashes +
|
|
216
|
+
// constant-time-compares against a decoy when the client is missing), so an
|
|
217
|
+
// unknown/suspended client_id is timing-indistinguishable from a wrong secret.
|
|
218
|
+
// Then fold in the active check. One generic invalid_client for every failure.
|
|
219
|
+
const secretOk = pi.clientSecretValid(client, client_secret);
|
|
220
|
+
const active = !!client && client.status === 'active';
|
|
221
|
+
if (!active || !secretOk) {
|
|
222
|
+
return res.fail('invalid_client', 401);
|
|
223
|
+
}
|
|
224
|
+
// Atomic single-use redeem, bound to this client.
|
|
225
|
+
const identity = await pi.redeemAuthCode({ code, clientId: client_id }, { pool });
|
|
226
|
+
if (!identity) {
|
|
227
|
+
return res.fail('invalid_grant', 400);
|
|
228
|
+
}
|
|
229
|
+
// The redeem is also bound to the redirect_uri the code was minted against.
|
|
230
|
+
if (identity.redirect_uri !== redirect_uri) {
|
|
231
|
+
return res.fail('invalid_grant', 400);
|
|
232
|
+
}
|
|
233
|
+
// IDENTITY ONLY — never a rank, credits, or a session token (ADR 0016 / 0141).
|
|
234
|
+
return res.json({
|
|
235
|
+
github_id: Number(identity.github_id),
|
|
236
|
+
github_login: identity.github_login,
|
|
237
|
+
display_name: identity.display_name,
|
|
238
|
+
avatar_url: identity.avatar_url,
|
|
239
|
+
});
|
|
240
|
+
} catch (err) {
|
|
241
|
+
console.error('[platform-identity] POST /sso/token', err);
|
|
242
|
+
return res.fail('token_exchange_failed', { status: 500, message: 'internal error' });
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
// GET /sso/pubkey — the hub's Ed25519 assertion-verification key (public). An
|
|
247
|
+
// instance provisioned WITHOUT the key in env can fetch it here; the value is
|
|
248
|
+
// public by design (it only VERIFIES assertions, never signs).
|
|
249
|
+
//
|
|
250
|
+
// rank: public — a public key is not a secret; no auth needed.
|
|
251
|
+
router.get('/sso/pubkey', (_req, res) => {
|
|
252
|
+
const publicKeyB64 = hubKeys.hubPublicKeyB64();
|
|
253
|
+
if (!publicKeyB64) return res.fail('idp_signing_unconfigured', 503);
|
|
254
|
+
res.json({ alg: 'EdDSA', format: 'spki-der-base64', public_key_b64: publicKeyB64 });
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
// POST /sso/device/start — begin CLI federation. The client is resolved from the
|
|
258
|
+
// USER-TYPED instance ORIGIN via the registry (getClientByOrigin) and BOUND to the
|
|
259
|
+
// device flow — the CLI never names the audience. This closes the confused-deputy
|
|
260
|
+
// where a malicious instance manifest could steer the mint at a victim client_id
|
|
261
|
+
// (ADR 0141 §1 security fix). Runs the GitHub device flow against the HUB's app and
|
|
262
|
+
// returns the codes. Identity is established as a platform ACCOUNT at poll time —
|
|
263
|
+
// never a hub-builder session (ADR 0141: identity federated, authority local).
|
|
264
|
+
//
|
|
265
|
+
// rank: public — an authentication entry point; the origin is public, and the
|
|
266
|
+
// poll's GitHub approval is the real gate.
|
|
267
|
+
router.post('/sso/device/start', async (req, res) => {
|
|
268
|
+
if (validateOrRespond(req, res, {
|
|
269
|
+
origin: { required: true, type: 'string', maxLength: 512 },
|
|
270
|
+
})) return;
|
|
271
|
+
if (!hubKeys.hubSigningConfigured() || !hubDevice.hubDeviceConfigured()) {
|
|
272
|
+
return res.fail('idp_signing_unconfigured', 503);
|
|
273
|
+
}
|
|
274
|
+
try {
|
|
275
|
+
// Resolve the client from the user-typed origin — NOT from any client_id the
|
|
276
|
+
// caller supplies. An unregistered origin has no client → refuse (correct default).
|
|
277
|
+
const client = await pi.getClientByOrigin(req.body.origin, { pool });
|
|
278
|
+
if (!client || client.status !== 'active') return res.fail('invalid_client', 401);
|
|
279
|
+
const out = await hubDevice.startHubGithubDevice(client.client_id);
|
|
280
|
+
res.json(out);
|
|
281
|
+
} catch (err) {
|
|
282
|
+
console.error('[platform-identity] /sso/device/start', err);
|
|
283
|
+
res.fail('device_start_failed', { status: 502, message: String(err.message || 'error').slice(0, 200) });
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
// POST /sso/device/poll — poll the hub device flow. On GitHub approval, upsert the
|
|
288
|
+
// platform account and mint an audience-scoped EdDSA assertion for the client that
|
|
289
|
+
// was BOUND at start (from the origin) — the poll takes NO client_id, so the CLI
|
|
290
|
+
// cannot influence the audience. Returns the SAME poll states as the core device
|
|
291
|
+
// flow, plus { state:'ok', assertion } on success.
|
|
292
|
+
//
|
|
293
|
+
// rank: public — the device_code (GitHub's) is the bearer; the assertion carries
|
|
294
|
+
// identity ONLY and is useless to anyone but the origin-bound client (aud-scoped).
|
|
295
|
+
router.post('/sso/device/poll', async (req, res) => {
|
|
296
|
+
if (validateOrRespond(req, res, {
|
|
297
|
+
device_code: { required: true, type: 'string', maxLength: 512 },
|
|
298
|
+
})) return;
|
|
299
|
+
if (!hubKeys.hubSigningConfigured() || !hubDevice.hubDeviceConfigured()) {
|
|
300
|
+
return res.fail('idp_signing_unconfigured', 503);
|
|
301
|
+
}
|
|
302
|
+
try {
|
|
303
|
+
const out = await hubDevice.pollHubGithubDevice(req.body.device_code);
|
|
304
|
+
if (out.state !== 'ok') return res.json(out); // pending | slow_down | expired | denied
|
|
305
|
+
if (!out.client_id) return res.fail('device_poll_failed', { status: 500, message: 'internal error' });
|
|
306
|
+
const id = out.identity;
|
|
307
|
+
await pi.upsertAccount({
|
|
308
|
+
githubId: id.github_id,
|
|
309
|
+
githubLogin: id.github_login,
|
|
310
|
+
displayName: id.display_name,
|
|
311
|
+
avatarUrl: id.avatar_url,
|
|
312
|
+
}, { pool });
|
|
313
|
+
const assertion = hubKeys.mintHubAssertion({
|
|
314
|
+
clientId: out.client_id, // BOUND at start from the origin — not caller-supplied
|
|
315
|
+
githubId: id.github_id,
|
|
316
|
+
githubLogin: id.github_login,
|
|
317
|
+
displayName: id.display_name,
|
|
318
|
+
avatarUrl: id.avatar_url,
|
|
319
|
+
});
|
|
320
|
+
res.json({ state: 'ok', assertion });
|
|
321
|
+
} catch (err) {
|
|
322
|
+
console.error('[platform-identity] /sso/device/poll', err);
|
|
323
|
+
res.fail('device_poll_failed', { status: 500, message: 'internal error' });
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
// POST /sso/membership/check-in — a federated instance reports, on every sign-in,
|
|
328
|
+
// that a builder builds on its project (ADR 0141 §3). Server-to-server, authed by
|
|
329
|
+
// client_id + client_secret (same constant-time check as /sso/token). Upserts the
|
|
330
|
+
// account (so a CLI-only builder still appears in My Projects) + the membership
|
|
331
|
+
// with a COARSE role. The client_id is taken from the AUTHENTICATED client — the
|
|
332
|
+
// caller cannot report membership for a project it doesn't hold the secret for.
|
|
333
|
+
//
|
|
334
|
+
// rank: public — authenticated by client_id + client_secret, not a builder rank.
|
|
335
|
+
router.post('/sso/membership/check-in', async (req, res) => {
|
|
336
|
+
if (validateOrRespond(req, res, {
|
|
337
|
+
client_id: { required: true, type: 'string', maxLength: 200 },
|
|
338
|
+
client_secret: { required: true, type: 'string', maxLength: 512 },
|
|
339
|
+
github_id: { required: true },
|
|
340
|
+
github_login: { required: true, type: 'string', maxLength: 200 },
|
|
341
|
+
display_name: { type: 'string', maxLength: 400 },
|
|
342
|
+
avatar_url: { type: 'string', maxLength: 2048 },
|
|
343
|
+
role: { type: 'string', maxLength: 16 },
|
|
344
|
+
})) return;
|
|
345
|
+
const { client_id, client_secret, github_id, github_login, display_name, avatar_url, role } = req.body || {};
|
|
346
|
+
try {
|
|
347
|
+
const client = await pi.getClient(client_id, { pool });
|
|
348
|
+
if (!client || client.status !== 'active' || !pi.clientSecretValid(client, client_secret)) {
|
|
349
|
+
return res.fail('invalid_client', 401);
|
|
350
|
+
}
|
|
351
|
+
// Upsert the account first so a CLI-only builder is known to the hub, then the
|
|
352
|
+
// membership — keyed to the AUTHENTICATED client_id, never a caller-named one.
|
|
353
|
+
await pi.upsertAccount({ githubId: github_id, githubLogin: github_login, displayName: display_name, avatarUrl: avatar_url }, { pool });
|
|
354
|
+
await pi.upsertMembership({ clientId: client_id, githubId: github_id, role: role || 'builder' }, { pool });
|
|
355
|
+
res.json({ ok: true });
|
|
356
|
+
} catch (err) {
|
|
357
|
+
console.error('[platform-identity] /sso/membership/check-in', err);
|
|
358
|
+
res.fail('checkin_failed', { status: 500, message: 'internal error' });
|
|
359
|
+
}
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
// POST /sso/activity/rollup — a federated instance reports a builder's activity
|
|
363
|
+
// snapshot (ADR 0141 §4). Server-to-server, client_id+client_secret auth. Keyed to
|
|
364
|
+
// the AUTHENTICATED client_id. Descriptive analytics for the Archon-only scouting
|
|
365
|
+
// view — never authority.
|
|
366
|
+
//
|
|
367
|
+
// rank: public — authenticated by client_id + client_secret, not a builder rank.
|
|
368
|
+
router.post('/sso/activity/rollup', async (req, res) => {
|
|
369
|
+
if (validateOrRespond(req, res, {
|
|
370
|
+
client_id: { required: true, type: 'string', maxLength: 200 },
|
|
371
|
+
client_secret: { required: true, type: 'string', maxLength: 512 },
|
|
372
|
+
github_id: { required: true },
|
|
373
|
+
credits: { type: 'number' },
|
|
374
|
+
tasks_shipped: { type: 'number' },
|
|
375
|
+
karma: { type: 'number' },
|
|
376
|
+
rank: { type: 'string', maxLength: 32 },
|
|
377
|
+
disciplines: {},
|
|
378
|
+
})) return;
|
|
379
|
+
const b = req.body || {};
|
|
380
|
+
try {
|
|
381
|
+
const client = await pi.getClient(b.client_id, { pool });
|
|
382
|
+
if (!client || client.status !== 'active' || !pi.clientSecretValid(client, b.client_secret)) {
|
|
383
|
+
return res.fail('invalid_client', 401);
|
|
384
|
+
}
|
|
385
|
+
await pi.upsertActivity({
|
|
386
|
+
clientId: b.client_id,
|
|
387
|
+
githubId: b.github_id,
|
|
388
|
+
credits: b.credits,
|
|
389
|
+
tasksShipped: b.tasks_shipped,
|
|
390
|
+
karma: b.karma,
|
|
391
|
+
rank: b.rank || null,
|
|
392
|
+
disciplines: Array.isArray(b.disciplines) ? b.disciplines : [],
|
|
393
|
+
}, { pool });
|
|
394
|
+
res.json({ ok: true });
|
|
395
|
+
} catch (err) {
|
|
396
|
+
console.error('[platform-identity] /sso/activity/rollup', err);
|
|
397
|
+
res.fail('rollup_failed', { status: 500, message: 'internal error' });
|
|
398
|
+
}
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
return router;
|
|
402
|
+
};
|