@bongos/core 1.17.0
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 +5727 -0
- package/.claude/skills/blocker-review/SKILL.md +91 -0
- package/.claude/skills/blocker-solve/SKILL.md +96 -0
- package/.claude/skills/builder-backup/SKILL.md +54 -0
- package/.claude/skills/builder-box/SKILL.md +45 -0
- package/.claude/skills/builder-claim/SKILL.md +76 -0
- package/.claude/skills/builder-connect/SKILL.md +59 -0
- package/.claude/skills/builder-cost/SKILL.md +47 -0
- package/.claude/skills/builder-end/SKILL.md +81 -0
- package/.claude/skills/builder-exit/SKILL.md +88 -0
- package/.claude/skills/builder-key/SKILL.md +40 -0
- package/.claude/skills/builder-reauth/SKILL.md +50 -0
- package/.claude/skills/builder-redteam/SKILL.md +109 -0
- package/.claude/skills/builder-release/SKILL.md +41 -0
- package/.claude/skills/builder-setup/SKILL.md +62 -0
- package/.claude/skills/builder-ship/SKILL.md +79 -0
- package/.claude/skills/builder-stage/SKILL.md +51 -0
- package/.claude/skills/builder-start/SKILL.md +46 -0
- package/.claude/skills/close-box/SKILL.md +43 -0
- package/.claude/skills/design-sync/SKILL.md +69 -0
- package/.claude/skills/dev/SKILL.md +49 -0
- package/.claude/skills/feedback/SKILL.md +32 -0
- package/.claude/skills/figma-design-sync/SKILL.md +74 -0
- package/.claude/skills/goal-review/SKILL.md +89 -0
- package/.claude/skills/idea-triage/SKILL.md +144 -0
- package/.claude/skills/ideate/SKILL.md +82 -0
- package/.claude/skills/merge-mode/SKILL.md +169 -0
- package/.claude/skills/new-project/SKILL.md +108 -0
- package/.claude/skills/otb-character-review/SKILL.md +71 -0
- package/.claude/skills/otb-design-review/SKILL.md +47 -0
- package/.claude/skills/otb-feedback-capture/SKILL.md +45 -0
- package/.claude/skills/otb-figma-sync/SKILL.md +54 -0
- package/.claude/skills/otb-tile-generate/SKILL.md +45 -0
- package/.claude/skills/paint/SKILL.md +70 -0
- package/.claude/skills/planning-session/SKILL.md +299 -0
- package/.claude/skills/priority-session/SKILL.md +134 -0
- package/.claude/skills/read-session-export/SKILL.md +58 -0
- package/.claude/skills/recall/SKILL.md +52 -0
- package/.claude/skills/session-handoff/SKILL.md +46 -0
- package/.claude/skills/status/SKILL.md +33 -0
- package/.devcontainer/Dockerfile +69 -0
- package/.devcontainer/README.md +73 -0
- package/.devcontainer/devcontainer-lock.json +19 -0
- package/.devcontainer/devcontainer.json +93 -0
- package/.devcontainer/healthcheck.sh +84 -0
- package/.devcontainer/init-firewall.sh +198 -0
- package/.devcontainer/post-create.sh +153 -0
- package/.dockerignore +36 -0
- package/.env.local.example +32 -0
- package/.gitattributes +83 -0
- package/.gitignore +91 -0
- package/CLAUDE.md +242 -0
- package/CODE_OF_CONDUCT.md +46 -0
- package/CONTRIBUTING.md +51 -0
- package/Dockerfile +46 -0
- package/GOVERNANCE.md +44 -0
- package/LICENSE +661 -0
- package/README.md +88 -0
- package/bin/bongos.js +222 -0
- package/clients/bongos-client/README.md +48 -0
- package/clients/bongos-client/bongos-client.global.js +617 -0
- package/clients/bongos-client/examples/hello-world.mjs +62 -0
- package/clients/bongos-client/index.cjs +619 -0
- package/clients/bongos-client/index.d.ts +793 -0
- package/clients/bongos-client/index.mjs +614 -0
- package/clients/bongos-client/package.json +32 -0
- package/config/branding.neutral.json +60 -0
- package/config/design-tokens.neutral.json +51 -0
- package/config/design-tokens.schema.json +96 -0
- package/config/modules.neutral.json +9 -0
- package/docker-compose.yml +52 -0
- package/docker-entrypoint.sh +30 -0
- package/docs/adr/0001-stack-choice-single-vps.md +45 -0
- package/docs/adr/0002-digitalocean-over-hetzner.md +27 -0
- package/docs/adr/0003-cloudflare-origin-cert-over-letsencrypt.md +35 -0
- package/docs/adr/0004-amazonprimea-name-and-trademark-acceptance.md +45 -0
- package/docs/adr/0005-invisible-other-players-in-v1.md +37 -0
- package/docs/adr/0006-singleton-worldroom-plus-queueroom.md +30 -0
- package/docs/adr/0007-persistent-identity-via-localstorage-uuid.md +31 -0
- package/docs/adr/0008-family-based-tile-generation.md +86 -0
- package/docs/adr/0008-google-chat-oauth-user-auth.md +37 -0
- package/docs/adr/0009-autotile-architecture.md +181 -0
- package/docs/adr/0010-pms-architecture.md +70 -0
- package/docs/adr/0011-terrain-id-rendering-model.md +174 -0
- package/docs/adr/0012-ground-object-cell-split.md +208 -0
- package/docs/adr/0013-paths-as-object-overlays-with-autotile-dispatch.md +158 -0
- package/docs/adr/0014-families-canonical-over-vocabulary.md +103 -0
- package/docs/adr/0015-task-dependencies-and-auto-promotion.md +125 -0
- package/docs/adr/0016-trust-boundary-server-enforced-permissions.md +132 -0
- package/docs/adr/0017-code-simplification-and-repo-cleanliness.md +74 -0
- package/docs/adr/0018-three-rank-model-goes-live.md +97 -0
- package/docs/adr/0019-red-team-bounty-payout-table.md +52 -0
- package/docs/adr/0020-security-model-public-stub.md +70 -0
- package/docs/adr/0021-per-builder-skill-model-preferences.md +99 -0
- package/docs/adr/0022-secrets-policy.md +124 -0
- package/docs/adr/0023-kind-multipliers-and-idea-bonus.md +118 -0
- package/docs/adr/0024-cloneable-repo-local-first-memory.md +277 -0
- package/docs/adr/0024-multi-agent-system-architecture.md +125 -0
- package/docs/adr/0025-offsite-backup-vendor-digitalocean-spaces.md +59 -0
- package/docs/adr/0025-structured-criterion-task-link.md +44 -0
- package/docs/adr/0026-bfg-memory-hygiene-and-cross-builder-learning.md +117 -0
- package/docs/adr/0027-bfg-session-inefficiency-evaluator.md +170 -0
- package/docs/adr/0028-honest-llm-spend-accounting.md +109 -0
- package/docs/adr/0029-status-page-outage-resilience.md +59 -0
- package/docs/adr/0030-builders-hall-live-updates.md +59 -0
- package/docs/adr/0031-cloud-dev-environments-for-builders.md +222 -0
- package/docs/adr/0032-discord-mirror-of-gds.md +50 -0
- package/docs/adr/0033-discord-bot-service-principal.md +82 -0
- package/docs/adr/0034-thetes-graduated-newcomer-rank.md +53 -0
- package/docs/adr/0035-builder-onboarding-three-paths.md +128 -0
- package/docs/adr/0036-builders-subdomain-and-ui-reorg.md +40 -0
- package/docs/adr/0037-discord-channel-management.md +51 -0
- package/docs/adr/0038-chromebook-ttyd-cloudflare-tunnel.md +111 -0
- package/docs/adr/0039-setup-first-onboarding-ux.md +49 -0
- package/docs/adr/0040-remote-control-default-browser-onramp.md +47 -0
- package/docs/adr/0041-temporary-grader-bypass-killswitch.md +40 -0
- package/docs/adr/0042-builder-self-deploy-ci-auto-merge.md +82 -0
- package/docs/adr/0043-git-ssh-trust-boundary-and-rank-floor-on-permission-paths.md +74 -0
- package/docs/adr/0044-mediterranean-palette-replacement.md +73 -0
- package/docs/adr/0044-per-box-live-game-preview.md +57 -0
- package/docs/adr/0045-devbox-desktop-app.md +133 -0
- package/docs/adr/0046-sandbox-first-review-gate.md +72 -0
- package/docs/adr/0047-discord-bugs-intake-anti-abuse.md +36 -0
- package/docs/adr/0048-works-in-progress-ia-split.md +32 -0
- package/docs/adr/0049-split-parallel-safety-contract.md +46 -0
- package/docs/adr/0050-device-flow-admission-invite-gated-by-default.md +43 -0
- package/docs/adr/0051-full-session-transcript-corpus.md +57 -0
- package/docs/adr/0052-sandbox-for-everyone-game-only-preview.md +70 -0
- package/docs/adr/0053-scoped-dev-box-session.md +55 -0
- package/docs/adr/0054-session-token-reward-cost-plus.md +63 -0
- package/docs/adr/0055-server-mediated-branch-publish.md +47 -0
- package/docs/adr/0056-prod-deploy-script-mirror-and-healthcheck-retry.md +62 -0
- package/docs/adr/0057-container-cost-ledger.md +28 -0
- package/docs/adr/0057-discord-archon-approval-channels.md +64 -0
- package/docs/adr/0058-automatic-gate-surface-gatekeeper.md +64 -0
- package/docs/adr/0059-single-approval-remove-devbox-approval-gate.md +37 -0
- package/docs/adr/0060-gds-retrieval-layer.md +97 -0
- package/docs/adr/0061-context-layer-decomposition.md +61 -0
- package/docs/adr/0062-medusa-instance-model-decoupling.md +107 -0
- package/docs/adr/0063-generated-repo-map-symbol-skeleton.md +44 -0
- package/docs/adr/0064-rename-medusa-to-cloud-bongos.md +49 -0
- package/docs/adr/0065-agpl-nonprofit-ai-first-governance.md +69 -0
- package/docs/adr/0066-generated-file-map-sections.md +51 -0
- package/docs/adr/0067-session-start-fast-forward-pull.md +43 -0
- package/docs/adr/0068-shared-art-key-for-newcomers.md +38 -0
- package/docs/adr/0069-hall-live-channel-sse-decouple.md +35 -0
- package/docs/adr/0070-deterministic-session-card-delivery.md +44 -0
- package/docs/adr/0071-box-confirm-before-destroyed-and-drift-reconcile.md +59 -0
- package/docs/adr/0072-bongos-app-mac-signed-first-windows-deferred.md +35 -0
- package/docs/adr/0072-dev-box-code-staleness-visibility.md +39 -0
- package/docs/adr/0073-builder-needs-signal-and-byok-gemini-key.md +64 -0
- package/docs/adr/0073-secrets-scan-exclude-uri-detector.md +37 -0
- package/docs/adr/0074-memory-map-stays-light.md +32 -0
- package/docs/adr/0075-per-system-cost-attribution.md +37 -0
- package/docs/adr/0076-in-session-deterministic-compaction.md +58 -0
- package/docs/adr/0077-content-addressed-llm-cache.md +32 -0
- package/docs/adr/0078-deterministic-first-cron.md +48 -0
- package/docs/adr/0079-value-gated-autonomous-engagement.md +39 -0
- package/docs/adr/0080-llm-cache-api-transport-and-trust-model.md +46 -0
- package/docs/adr/0081-tool-agnostic-design-layer.md +62 -0
- package/docs/adr/0082-productized-key-storage-provisioning.md +32 -0
- package/docs/adr/0082-server-side-merge-conflict-auto-resolution.md +55 -0
- package/docs/adr/0083-modular-architecture-strangler-build-plan.md +143 -0
- package/docs/adr/0084-auto-derived-per-task-rank-floor.md +86 -0
- package/docs/adr/0085-remove-default-per-builder-spend-cap.md +39 -0
- package/docs/adr/0086-goal-scoped-work-hierarchy.md +123 -0
- package/docs/adr/0087-bongos-app-architecture-and-handoff-contract.md +49 -0
- package/docs/adr/0087-compete-on-governance-not-tooling-cursor.md +65 -0
- package/docs/adr/0088-consistent-character-generative-animation.md +169 -0
- package/docs/adr/0089-modular-multi-model-adversarial-grader.md +179 -0
- package/docs/adr/0090-metic-task-authoring.md +73 -0
- package/docs/adr/0091-bounding-the-kernel-and-db-carve.md +106 -0
- package/docs/adr/0092-hosting-module-and-on-prem-target.md +117 -0
- package/docs/adr/0093-tranche-2-core-carve-sequence.md +72 -0
- package/docs/adr/0094-repo-atlas-self-drawing-codebase-map.md +49 -0
- package/docs/adr/0095-borrowed-memory-and-retrieval-concepts.md +65 -0
- package/docs/adr/0095-cross-agent-context-management.md +58 -0
- package/docs/adr/0096-require-reward-before-workable.md +209 -0
- package/docs/adr/0097-one-active-claim-per-session-and-worktree-binding.md +46 -0
- package/docs/adr/0097-retroactive-reward-backfill.md +124 -0
- package/docs/adr/0098-public-mirror-publish-manifest.md +92 -0
- package/docs/adr/0099-delayed-redacted-mirror-export.md +55 -0
- package/docs/adr/0100-two-repo-instance-model.md +60 -0
- package/docs/adr/0101-goal-hierarchy-phase-5-judge-membership-reward.md +90 -0
- package/docs/adr/0102-finish-bounding-the-kernel.md +36 -0
- package/docs/adr/0103-core-first-extraction-cloud-bongos-trunk.md +70 -0
- package/docs/adr/0103-gdsv4-bongos-consolidation.md +28 -0
- package/docs/adr/0104-trust-gds-api-channel-in-auto-mode.md +42 -0
- package/docs/adr/0105-instance-seed-migrations-out-of-core.md +47 -0
- package/docs/adr/0106-goal-owner-manager-authority.md +84 -0
- package/docs/adr/0107-module-upstreaming-contribution-to-core-catalog.md +83 -0
- package/docs/adr/0108-instance-composition-configurable-root.md +87 -0
- package/docs/adr/0109-self-describing-openapi-and-hosted-docs.md +44 -0
- package/docs/adr/0110-untrack-whole-file-generated-nav-docs.md +49 -0
- package/docs/adr/0111-instance-hosting-provisioning-module.md +135 -0
- package/docs/adr/0112-private-goals-invitations-join-requests.md +72 -0
- package/docs/adr/0113-sandbox-studio-descope-superseded-by-0044-0052.md +41 -0
- package/docs/adr/0114-instance-docs-discovery-contract.md +32 -0
- package/docs/adr/0115-scheduled-routines-portability.md +31 -0
- package/docs/adr/0116-api-error-envelope-and-code-taxonomy.md +62 -0
- package/docs/adr/0117-api-versioning-and-deprecation-policy.md +50 -0
- package/docs/adr/0118-api-contract-source-of-truth-code-first.md +47 -0
- package/docs/adr/0119-api-pagination-contract.md +34 -0
- package/docs/adr/0120-pay-on-land-and-builder-owned-rebase-gate.md +108 -0
- package/docs/adr/0121-greenfield-vs-brownfield-onboarding-adopt-existing-repo.md +97 -0
- package/docs/adr/0122-onboarding-diagram-renders-deploy-rendered-off-git.md +43 -0
- package/docs/adr/0123-box-idle-sweep-autosave-before-destroy.md +36 -0
- package/docs/adr/0124-typed-api-response-bodies.md +37 -0
- package/docs/adr/0125-control-plane-scaffolds-standalone-instance-repo.md +36 -0
- package/docs/adr/0126-dedicated-cloudbongos-control-plane-droplet.md +30 -0
- package/docs/adr/0127-cross-box-fleet-observability.md +64 -0
- package/docs/adr/0128-provisioning-runner-privilege-and-db-auth-model.md +46 -0
- package/docs/adr/0129-cloudbongos-control-plane-pull-deploy.md +34 -0
- package/docs/adr/0130-provision-remote-exec-over-ssh.md +61 -0
- package/docs/adr/0131-rank-scoped-skill-visibility.md +47 -0
- package/docs/adr/0132-cohost-fleet-control-plane-migration.md +54 -0
- package/docs/adr/0133-github-app-manifest-onboarding-signin.md +42 -0
- package/docs/adr/0134-private-first-npm-distribution.md +38 -0
- package/docs/adr/README.md +208 -0
- package/docs/api/openapi.json +13720 -0
- package/docs/api-reference.md +437 -0
- package/docs/architecture.md +244 -0
- package/docs/branding-contract.md +66 -0
- package/docs/branding-fill-prompt.md +25 -0
- package/docs/canonical-permissions.md +241 -0
- package/docs/design/container-exterior-tiles.md +49 -0
- package/docs/design/container-exterior.md +81 -0
- package/docs/design/container-interior.md +77 -0
- package/docs/design/modular-architecture/00-research-report.md +107 -0
- package/docs/design/modular-architecture/01-architecture.md +125 -0
- package/docs/design/modular-architecture/02-module-map.md +116 -0
- package/docs/design/modular-architecture/03-builder-flows.md +104 -0
- package/docs/design/modular-architecture/04-module-lifecycle.md +62 -0
- package/docs/design/modular-architecture/README.md +27 -0
- package/docs/design/vanilla-hall-ui-redesign-scope.md +284 -0
- package/docs/design/world-map-scroll.md +41 -0
- package/docs/design-contract.md +320 -0
- package/docs/file-map.md +418 -0
- package/docs/handoff-template.md +92 -0
- package/docs/modules-contract.md +293 -0
- package/docs/onboarding/browser-terminal-guide.md +73 -0
- package/docs/onboarding/diagrams/01-task-lifecycle.mmd +28 -0
- package/docs/onboarding/diagrams/02-rank-ladder.mmd +54 -0
- package/docs/onboarding/diagrams/03-drachmae-karma.mmd +37 -0
- package/docs/onboarding/diagrams/04-architecture.mmd +51 -0
- package/docs/onboarding/diagrams/05-versioning.mmd +29 -0
- package/docs/onboarding/diagrams/README.md +94 -0
- package/docs/onboarding/diagrams/assertions.json +170 -0
- package/docs/onboarding/diagrams/puppeteer-config.json +1 -0
- package/docs/onboarding/diagrams/render-config.json +9 -0
- package/docs/onboarding/drachmae-and-karma.md +178 -0
- package/docs/onboarding/primer.md +202 -0
- package/docs/project-context.template.md +67 -0
- package/docs/recipes/README.md +35 -0
- package/docs/recipes/audit-replay.md +100 -0
- package/docs/recipes/bongos-cli-release.md +66 -0
- package/docs/recipes/builder-box-lifecycle.md +152 -0
- package/docs/recipes/claude-design-loop.md +143 -0
- package/docs/recipes/claude-gemini-bongos-doors.md +155 -0
- package/docs/recipes/cloudbongos-staging-gate.md +139 -0
- package/docs/recipes/cloudbongos-standup.md +111 -0
- package/docs/recipes/devbox-app-release.md +93 -0
- package/docs/recipes/devbox-app-windows-test.md +128 -0
- package/docs/recipes/failed-ship-recovery-proof.md +102 -0
- package/docs/recipes/figma-design-loop.md +129 -0
- package/docs/recipes/gds-db-backup.md +255 -0
- package/docs/recipes/instance-deploy.md +37 -0
- package/docs/recipes/managed-settings-remote-control.md +416 -0
- package/docs/recipes/memory-backup.md +71 -0
- package/docs/recipes/memory-sync-conflicts.md +98 -0
- package/docs/recipes/multi-builder-merge.md +78 -0
- package/docs/recipes/ops-gotchas.md +212 -0
- package/docs/recipes/overnight-builder.md +53 -0
- package/docs/recipes/packaging-the-core.md +49 -0
- package/docs/recipes/parallel-load-results-2026-05-30.md +63 -0
- package/docs/recipes/parallel-load-test.md +32 -0
- package/docs/recipes/pixel-art-pipeline.md +411 -0
- package/docs/recipes/private-npm-distribution.md +79 -0
- package/docs/recipes/search-index-ingestion.md +67 -0
- package/docs/recipes/self-host.md +60 -0
- package/docs/recipes/ship-and-regrade.md +72 -0
- package/docs/recipes/staging-data.md +50 -0
- package/docs/recipes/staging-environment.md +215 -0
- package/docs/recipes/upgrading-the-core.md +58 -0
- package/docs/research/cursor-competitive-analysis.md +255 -0
- package/docs/research/llm-model-agnostic-and-cheaper-grader.md +227 -0
- package/migrations/003_pms.sql +284 -0
- package/migrations/004_pms_cost_idempotency.sql +13 -0
- package/migrations/005_pms_smoke_filter.sql +44 -0
- package/migrations/006_pms_v2.sql +378 -0
- package/migrations/007_pms_v2_hierarchy_trigger.sql +59 -0
- package/migrations/008_pms_v2_migration_allocator.sql +27 -0
- package/migrations/009_pms_v2_blockers_table.sql +68 -0
- package/migrations/010_uptime.sql +29 -0
- package/migrations/011_backfill_v2_v3_done_when.sql +20 -0
- package/migrations/012_pms_v2_task_lifecycle_states.sql +82 -0
- package/migrations/013_claim_locking.sql +22 -0
- package/migrations/014_pms_task_discipline.sql +86 -0
- package/migrations/015_pms_estimation_calibration.sql +140 -0
- package/migrations/016_pms_task_grades.sql +60 -0
- package/migrations/017_version_progress_exclude_abandoned.sql +49 -0
- package/migrations/019_rename_pms_v3_to_gds_v3.sql +65 -0
- package/migrations/020_task_dependencies.sql +99 -0
- package/migrations/021_v3_renumber_and_deps.sql +224 -0
- package/migrations/022_v3_reconcile_dep_gated_status.sql +41 -0
- package/migrations/023_pms_rank.sql +30 -0
- package/migrations/025_builder_exit_status.sql +15 -0
- package/migrations/027_builders_preferred_disciplines.sql +46 -0
- package/migrations/028_tasks_parallel_safe.sql +71 -0
- package/migrations/029_rank_three_live.sql +32 -0
- package/migrations/030_vulnerability_reports.sql +61 -0
- package/migrations/031_reward_simplification.sql +57 -0
- package/migrations/032_repo_health_snapshots.sql +56 -0
- package/migrations/033_audit_log.sql +64 -0
- package/migrations/034_lifecycle_breakdown_leaf_only.sql +62 -0
- package/migrations/035_auth_token_hygiene.sql +66 -0
- package/migrations/037_builder_skill_model_prefs.sql +44 -0
- package/migrations/038_task_override_requests.sql +51 -0
- package/migrations/039_newcomer_friendly_column.sql +124 -0
- package/migrations/040_builder_karma.sql +58 -0
- package/migrations/041_kind_multipliers_and_idea_bonus.sql +42 -0
- package/migrations/042_criterion_proposal_workflow.sql +78 -0
- package/migrations/043_builder_onboarding_observability.sql +66 -0
- package/migrations/044_tasks_requires_rank.sql +97 -0
- package/migrations/045_builder_onboarding_state.sql +147 -0
- package/migrations/046_cost_log_builder_attribution.sql +42 -0
- package/migrations/054_builder_memory.sql +108 -0
- package/migrations/055_task_criteria.sql +90 -0
- package/migrations/058_peer_votes.sql +41 -0
- package/migrations/059_onboarding_pick_disciplines.sql +59 -0
- package/migrations/060_access_requests.sql +46 -0
- package/migrations/061_backfill_override_ship_attribution.sql +29 -0
- package/migrations/062_session_records.sql +34 -0
- package/migrations/063_session_records_tool_errors_idx.sql +11 -0
- package/migrations/064_grade_attempts.sql +18 -0
- package/migrations/065_bfg_principal.sql +58 -0
- package/migrations/066_live_notify_triggers.sql +69 -0
- package/migrations/067_builder_boxes.sql +117 -0
- package/migrations/067_discord_bot_foundation.sql +69 -0
- package/migrations/068_thetes_requires_rank.sql +27 -0
- package/migrations/069_builder_ssh_keys.sql +66 -0
- package/migrations/070_box_intents.sql +101 -0
- package/migrations/071_box_first_provision_approval.sql +46 -0
- package/migrations/072_onboarding_ship_three_remove_primer.sql +80 -0
- package/migrations/073_box_terminal_access.sql +46 -0
- package/migrations/075_builder_sound_prefs.sql +28 -0
- package/migrations/078_box_claude_active.sql +22 -0
- package/migrations/079_box_first_connected_at.sql +20 -0
- package/migrations/081_tasks_needs_migration.sql +34 -0
- package/migrations/084_tasks_security_sensitive.sql +24 -0
- package/migrations/085_security_engagement_docs.sql +34 -0
- package/migrations/086_rename_disciplines.sql +125 -0
- package/migrations/088_builders_anthropic_link.sql +28 -0
- package/migrations/089_builders_monthly_budget.sql +19 -0
- package/migrations/090_box_scoped_sessions.sql +38 -0
- package/migrations/091_session_token_reward.sql +44 -0
- package/migrations/093_tasks_sandbox_review.sql +22 -0
- package/migrations/102_remove_box_first_provision_approval.sql +58 -0
- package/migrations/103_search_chunks.sql +90 -0
- package/migrations/104_builder_box_blocked.sql +34 -0
- package/migrations/105_box_host_keys.sql +35 -0
- package/migrations/111_medusa_customer0_framing.sql +35 -0
- package/migrations/112_drop_track_check.sql +25 -0
- package/migrations/113_merge_lease.sql +16 -0
- package/migrations/114_cloudbongos_rename_live_strings.sql +32 -0
- package/migrations/118_currency_label_decoupling.sql +23 -0
- package/migrations/119_builder_behavior_prefs.sql +31 -0
- package/migrations/121_box_code_version.sql +37 -0
- package/migrations/122_builder_gemini_key.sql +30 -0
- package/migrations/123_cost_log_tiers.sql +33 -0
- package/migrations/124_cost_baseline.sql +45 -0
- package/migrations/125_llm_cache.sql +54 -0
- package/migrations/126_autonomy_runs.sql +33 -0
- package/migrations/143_cost_calibration.sql +63 -0
- package/migrations/145_backfill_task_requires_rank.sql +84 -0
- package/migrations/160_goal_tier.sql +88 -0
- package/migrations/163_polymorphic_dependencies.sql +187 -0
- package/migrations/164_event_sound_prefs.sql +20 -0
- package/migrations/165_task_module_key.sql +15 -0
- package/migrations/171_claims_published_head_sha.sql +24 -0
- package/migrations/174_close_orphaned_claims_on_terminal_tasks.sql +30 -0
- package/migrations/175_task_grades_diff_hash.sql +9 -0
- package/migrations/176_promotion_respects_open_blockers.sql +58 -0
- package/migrations/176_ui_discipline.sql +40 -0
- package/migrations/177_require_reward_before_workable.sql +76 -0
- package/migrations/178_reward_gate_auto_assign_revert_autopromote.sql +61 -0
- package/migrations/179_search_chunk_usefulness.sql +33 -0
- package/migrations/180_one_active_claim_per_session.sql +54 -0
- package/migrations/181_cost_log_skill_name.sql +33 -0
- package/migrations/184_claim_release_requested.sql +25 -0
- package/migrations/185_module_upstreaming_goal_wiring.sql +59 -0
- package/migrations/187_goal_sort_order.sql +30 -0
- package/migrations/187_tasks_kind_check_add_verify.sql +21 -0
- package/migrations/README.md +88 -0
- package/migrations/core_188_goal_visibility_and_membership_requests.sql +71 -0
- package/migrations/core_189_core_upgrades_ledger.sql +37 -0
- package/migrations/core_189_credit_log_task_id_index.sql +19 -0
- package/migrations/core_190_rebuild_claimable_task_views.sql +71 -0
- package/migrations/instance/018_pms_v3_planning_seed.sql +131 -0
- package/migrations/instance/080_gds_v4_planning_seed.sql +75 -0
- package/migrations/instance/110_medusa_planning_seed.sql +70 -0
- package/migrations/instance/120_bongos_v1_planning_seed.sql +69 -0
- package/migrations/instance/144_bongos_v1_module_system_criterion.sql +26 -0
- package/migrations/instance/159_bongos_v1_goal_hierarchy_criterion.sql +29 -0
- package/migrations/instance/172_v2_world_planning_seed.sql +163 -0
- package/migrations/instance/173_bongos_v1_c7_core_modularized.sql +33 -0
- package/migrations/instance/182_gdsv4_bongos_consolidation.sql +132 -0
- package/migrations/instance/183_gdsv4_carryover_themed_goals.sql +116 -0
- package/migrations/instance/186_bongos_v1_governance_planning_seed.sql +69 -0
- package/modules/autonomy/CLAUDE.md +49 -0
- package/modules/autonomy/db.js +50 -0
- package/modules/autonomy/module.json +13 -0
- package/modules/autonomy/pollers/routine-timers.js +146 -0
- package/modules/autonomy/routes/autonomy.js +37 -0
- package/modules/builder-settings/CLAUDE.md +58 -0
- package/modules/builder-settings/builder-needs.js +115 -0
- package/modules/builder-settings/db.js +168 -0
- package/modules/builder-settings/display-name.js +84 -0
- package/modules/builder-settings/event-sound-prefs.js +169 -0
- package/modules/builder-settings/interaction-prefs.js +201 -0
- package/modules/builder-settings/model-alloc-prefs.js +147 -0
- package/modules/builder-settings/module.json +10 -0
- package/modules/builder-settings/render-prefs.js +103 -0
- package/modules/builder-settings/routes/settings.js +417 -0
- package/modules/builder-settings/settings.js +52 -0
- package/modules/builder-settings/skill-prefs.js +196 -0
- package/modules/builder-settings/sound-prefs.js +178 -0
- package/modules/builder-settings/wandering-prefs.js +282 -0
- package/modules/dev-box/CLAUDE.md +38 -0
- package/modules/dev-box/box-access.js +275 -0
- package/modules/dev-box/box-credential.js +94 -0
- package/modules/dev-box/box-onboard.js +486 -0
- package/modules/dev-box/boxes.js +842 -0
- package/modules/dev-box/db.js +39 -0
- package/modules/dev-box/module.json +14 -0
- package/modules/dev-box/routes/box.js +977 -0
- package/modules/discord/CLAUDE.md +50 -0
- package/modules/discord/auth.js +54 -0
- package/modules/discord/bug-attachments.js +210 -0
- package/modules/discord/db.js +96 -0
- package/modules/discord/discord-approvals.js +145 -0
- package/modules/discord/discord-audit.js +29 -0
- package/modules/discord/discord-bot.js +545 -0
- package/modules/discord/discord-bugs.js +257 -0
- package/modules/discord/discord-channels.js +412 -0
- package/modules/discord/discord-inbound.js +147 -0
- package/modules/discord/discord-message-buffer.js +54 -0
- package/modules/discord/discord-reaction.js +186 -0
- package/modules/discord/discord-role-sync.js +318 -0
- package/modules/discord/module.json +14 -0
- package/modules/discord/pollers/discord-bot.js +19 -0
- package/modules/discord/routes/bug-attachments.js +44 -0
- package/modules/discord/routes/discord.js +102 -0
- package/modules/discord/ship-broadcast.js +78 -0
- package/modules/discord/webhook.js +74 -0
- package/modules/economy/CLAUDE.md +48 -0
- package/modules/economy/achievements.js +594 -0
- package/modules/economy/cost-classify.js +331 -0
- package/modules/economy/cost-summary-shape.js +90 -0
- package/modules/economy/cost.js +438 -0
- package/modules/economy/credits.js +430 -0
- package/modules/economy/migrations/economy_001_fix_gds_shipper_description.sql +91 -0
- package/modules/economy/module.json +10 -0
- package/modules/economy/reward.js +61 -0
- package/modules/economy/routes/cost.js +97 -0
- package/modules/economy/routes/leaderboard.js +26 -0
- package/modules/economy/routes/reward.js +42 -0
- package/modules/economy/streak.js +285 -0
- package/modules/grading/CLAUDE.md +42 -0
- package/modules/grading/db.js +39 -0
- package/modules/grading/grader-rubric.json +151 -0
- package/modules/grading/grader-workers/efficiency.js +213 -0
- package/modules/grading/grader-workers/extract-json.js +80 -0
- package/modules/grading/grader-workers/fence.js +100 -0
- package/modules/grading/grader-workers/hacker.js +239 -0
- package/modules/grading/grader-workers/index.js +183 -0
- package/modules/grading/grader-workers/narc.js +207 -0
- package/modules/grading/grader-workers/quality.js +198 -0
- package/modules/grading/grader-workers/rubric.js +20 -0
- package/modules/grading/grader-workers/worker-prompt.js +89 -0
- package/modules/grading/grader-workers/worker-verdict.js +50 -0
- package/modules/grading/grader.js +1790 -0
- package/modules/grading/module.json +12 -0
- package/modules/grading/routes/grade.js +51 -0
- package/modules/hall-ui/CLAUDE.md +61 -0
- package/modules/hall-ui/module.json +14 -0
- package/modules/hall-ui/public/atlas.html +180 -0
- package/modules/hall-ui/public/atlas.js +336 -0
- package/modules/hall-ui/public/box-panel.js +423 -0
- package/modules/hall-ui/public/brand-apply.js +117 -0
- package/modules/hall-ui/public/builders.js +3028 -0
- package/modules/hall-ui/public/chime.wav +0 -0
- package/modules/hall-ui/public/copy-on-click.js +174 -0
- package/modules/hall-ui/public/cursors/arrow.png +0 -0
- package/modules/hall-ui/public/cursors/hand-pointer.png +0 -0
- package/modules/hall-ui/public/cursors/magnifier-out.png +0 -0
- package/modules/hall-ui/public/cursors/magnifier.png +0 -0
- package/modules/hall-ui/public/diagram-viewer.css +125 -0
- package/modules/hall-ui/public/diagram-viewer.js +330 -0
- package/modules/hall-ui/public/diagrams.html +113 -0
- package/modules/hall-ui/public/dom-utils.js +212 -0
- package/modules/hall-ui/public/drachmae.html +68 -0
- package/modules/hall-ui/public/drachmae.js +136 -0
- package/modules/hall-ui/public/gate.html +143 -0
- package/modules/hall-ui/public/gate.js +228 -0
- package/modules/hall-ui/public/goals-page.js +1297 -0
- package/modules/hall-ui/public/goals.html +376 -0
- package/modules/hall-ui/public/goals.js +119 -0
- package/modules/hall-ui/public/hall-widgets.js +189 -0
- package/modules/hall-ui/public/harbor.html +167 -0
- package/modules/hall-ui/public/harbor.js +335 -0
- package/modules/hall-ui/public/index.html +116 -0
- package/modules/hall-ui/public/instances-panel.js +172 -0
- package/modules/hall-ui/public/md-reader.js +234 -0
- package/modules/hall-ui/public/modules.html +119 -0
- package/modules/hall-ui/public/modules.js +154 -0
- package/modules/hall-ui/public/new-project-wizard.js +186 -0
- package/modules/hall-ui/public/not-ready.html +48 -0
- package/modules/hall-ui/public/pair.html +119 -0
- package/modules/hall-ui/public/pair.js +132 -0
- package/modules/hall-ui/public/primer.css +404 -0
- package/modules/hall-ui/public/primer.html +85 -0
- package/modules/hall-ui/public/primer.js +641 -0
- package/modules/hall-ui/public/profile.html +85 -0
- package/modules/hall-ui/public/profile.js +236 -0
- package/modules/hall-ui/public/ranks.html +69 -0
- package/modules/hall-ui/public/ranks.js +206 -0
- package/modules/hall-ui/public/roadmap.html +209 -0
- package/modules/hall-ui/public/roadmap.js +375 -0
- package/modules/hall-ui/public/sessions.html +92 -0
- package/modules/hall-ui/public/sessions.js +222 -0
- package/modules/hall-ui/public/settings.html +640 -0
- package/modules/hall-ui/public/settings.js +1219 -0
- package/modules/hall-ui/public/shell.js +495 -0
- package/modules/hall-ui/public/style.css +1748 -0
- package/modules/hall-ui/public/task-detail.js +253 -0
- package/modules/hall-ui/public/task.html +83 -0
- package/modules/hall-ui/public/task.js +326 -0
- package/modules/hall-ui/public/watch.html +333 -0
- package/modules/hall-ui/public/watch.js +859 -0
- package/modules/hall-ui/public/work.html +469 -0
- package/modules/hall-ui/public/work.js +1292 -0
- package/modules/ideas/CLAUDE.md +37 -0
- package/modules/ideas/blockers.js +100 -0
- package/modules/ideas/inbox.js +459 -0
- package/modules/ideas/module.json +10 -0
- package/modules/ideas/routes/blockers.js +120 -0
- package/modules/ideas/routes/inbox.js +230 -0
- package/modules/lifecycle/CLAUDE.md +63 -0
- package/modules/lifecycle/conflict-resolve.js +436 -0
- package/modules/lifecycle/db.js +4970 -0
- package/modules/lifecycle/done-when.js +355 -0
- package/modules/lifecycle/github-push.js +969 -0
- package/modules/lifecycle/goal-conflicts.js +133 -0
- package/modules/lifecycle/hierarchy.js +104 -0
- package/modules/lifecycle/lifecycle.js +58 -0
- package/modules/lifecycle/merge-lock.js +261 -0
- package/modules/lifecycle/migrations/lifecycle_001_goal_first_leads.sql +28 -0
- package/modules/lifecycle/migrations/lifecycle_002_backfill_orphan_task_goals.sql +36 -0
- package/modules/lifecycle/module.json +13 -0
- package/modules/lifecycle/pollers/publish-reconciler.js +20 -0
- package/modules/lifecycle/publish-reconciler.js +418 -0
- package/modules/lifecycle/routes/analytics.js +63 -0
- package/modules/lifecycle/routes/claims.js +506 -0
- package/modules/lifecycle/routes/dependencies.js +82 -0
- package/modules/lifecycle/routes/done-when.js +136 -0
- package/modules/lifecycle/routes/gate-approvals.js +197 -0
- package/modules/lifecycle/routes/goals.js +1419 -0
- package/modules/lifecycle/routes/lifecycle.js +43 -0
- package/modules/lifecycle/routes/tasks.js +1703 -0
- package/modules/lifecycle/routes/versions.js +128 -0
- package/modules/lifecycle/ship-card.js +505 -0
- package/modules/lifecycle/ship-preflight.js +225 -0
- package/modules/lifecycle/task-classifier.js +512 -0
- package/modules/lifecycle/version-close.js +106 -0
- package/modules/memory/CLAUDE.md +40 -0
- package/modules/memory/learnings.js +275 -0
- package/modules/memory/memory.js +572 -0
- package/modules/memory/module.json +12 -0
- package/modules/memory/routes/learnings.js +90 -0
- package/modules/memory/routes/memory.js +916 -0
- package/modules/memory/routes/search.js +188 -0
- package/modules/memory/search-chunker.js +252 -0
- package/modules/memory/search.js +320 -0
- package/modules/onboarding/CLAUDE.md +55 -0
- package/modules/onboarding/approval-broadcast.js +59 -0
- package/modules/onboarding/db.js +54 -0
- package/modules/onboarding/module.json +10 -0
- package/modules/onboarding/newcomer-restock.js +212 -0
- package/modules/onboarding/onboarding-state.js +532 -0
- package/modules/onboarding/onboarding.js +508 -0
- package/modules/onboarding/port.js +63 -0
- package/modules/onboarding/routes/access-requests.js +208 -0
- package/modules/onboarding/routes/onboarding.js +54 -0
- package/modules/security/CLAUDE.md +44 -0
- package/modules/security/module.json +10 -0
- package/modules/security/routes/reports.js +251 -0
- package/modules/security/secret-scrub.js +201 -0
- package/modules/security/security.js +294 -0
- package/modules/sessions/CLAUDE.md +39 -0
- package/modules/sessions/bfg.js +357 -0
- package/modules/sessions/db.js +326 -0
- package/modules/sessions/module.json +10 -0
- package/modules/sessions/pulse.js +149 -0
- package/modules/sessions/routes/sessions.js +331 -0
- package/modules/status-ui/CLAUDE.md +47 -0
- package/modules/status-ui/module.json +14 -0
- package/modules/status-ui/public/README.md +29 -0
- package/modules/status-ui/public/cursors/arrow.png +0 -0
- package/modules/status-ui/public/cursors/hand-pointer.png +0 -0
- package/modules/status-ui/public/index.html +363 -0
- package/modules/status-ui/public/snapshots/.gitkeep +0 -0
- package/modules/status-ui/public/status.js +1375 -0
- package/modules/status-ui/public/style.css +1342 -0
- package/package-lock.json +975 -0
- package/package.json +28 -0
- package/public-docs/index.html +35 -0
- package/public-docs/redoc.standalone.js +1838 -0
- package/public-docs/redoc.standalone.js.LICENSE.txt +102 -0
- package/public-landing/buddha.png +0 -0
- package/public-landing/create.html +868 -0
- package/public-landing/index.html +168 -0
- package/scripts/bongos-mcp.js +28 -0
- package/scripts/cron/style_guide_audit.sh +134 -0
- package/scripts/discord/README.md +52 -0
- package/scripts/discord/lib.js +80 -0
- package/scripts/discord/post.js +51 -0
- package/scripts/gds/CLAUDE.md +144 -0
- package/scripts/gds/abandon.js +58 -0
- package/scripts/gds/adopt-detect.js +123 -0
- package/scripts/gds/adopt-import.js +111 -0
- package/scripts/gds/adopt-preflight.js +121 -0
- package/scripts/gds/api.js +78 -0
- package/scripts/gds/architect-audit.js +288 -0
- package/scripts/gds/art-stage.js +152 -0
- package/scripts/gds/audit-authorship.sh +243 -0
- package/scripts/gds/audit-deps.js +121 -0
- package/scripts/gds/audit-exports.js +128 -0
- package/scripts/gds/audit-replay.sh +161 -0
- package/scripts/gds/audit-ships.js +251 -0
- package/scripts/gds/autonomy-gate.js +135 -0
- package/scripts/gds/backfill-cost.js +165 -0
- package/scripts/gds/backfill-sessions.js +351 -0
- package/scripts/gds/backfill-task-rewards.js +408 -0
- package/scripts/gds/backup.js +94 -0
- package/scripts/gds/bfg-principal-token.js +65 -0
- package/scripts/gds/box-auth-check.js +160 -0
- package/scripts/gds/box-connect-lib.js +453 -0
- package/scripts/gds/box.js +1258 -0
- package/scripts/gds/build-bongos.js +94 -0
- package/scripts/gds/capture.js +123 -0
- package/scripts/gds/cf-tunnel.js +419 -0
- package/scripts/gds/ci-grade.js +82 -0
- package/scripts/gds/claim-recall.js +112 -0
- package/scripts/gds/claim.js +333 -0
- package/scripts/gds/classify-discipline.js +153 -0
- package/scripts/gds/claude-materialize.js +176 -0
- package/scripts/gds/cleanup-orphans.js +74 -0
- package/scripts/gds/cli-lib.js +518 -0
- package/scripts/gds/code.js +101 -0
- package/scripts/gds/codemod-rename-src-gds.js +145 -0
- package/scripts/gds/conductor-dispatch.js +426 -0
- package/scripts/gds/context-pack.js +228 -0
- package/scripts/gds/coreB-carve-proof.sh +716 -0
- package/scripts/gds/cost.js +47 -0
- package/scripts/gds/db-backup-nightly.sh +160 -0
- package/scripts/gds/db-backup-offsite.sh +152 -0
- package/scripts/gds/demote.js +40 -0
- package/scripts/gds/design-sync.js +62 -0
- package/scripts/gds/diagnose-achievements.js +118 -0
- package/scripts/gds/diagram-facts.js +319 -0
- package/scripts/gds/discipline-modes.json +25 -0
- package/scripts/gds/discord-channels.js +66 -0
- package/scripts/gds/do-api.js +176 -0
- package/scripts/gds/docs-entropy.js +403 -0
- package/scripts/gds/doctor.js +389 -0
- package/scripts/gds/feedback-latest.js +94 -0
- package/scripts/gds/fetch-art-key.js +106 -0
- package/scripts/gds/fetch-sound-prefs.js +88 -0
- package/scripts/gds/figma-design-sync.js +61 -0
- package/scripts/gds/fitness.js +1446 -0
- package/scripts/gds/gate-review.js +322 -0
- package/scripts/gds/gating.js +325 -0
- package/scripts/gds/gds-db-backup.service +24 -0
- package/scripts/gds/gds-db-backup.timer +20 -0
- package/scripts/gds/gds-db-offsite.service +29 -0
- package/scripts/gds/gds-db-offsite.timer +18 -0
- package/scripts/gds/gen-api-client.js +508 -0
- package/scripts/gds/gen-api-docs.js +1037 -0
- package/scripts/gds/gen-atlas.js +366 -0
- package/scripts/gds/gen-diagrams.js +608 -0
- package/scripts/gds/gen-file-map.js +196 -0
- package/scripts/gds/gen-repo-map.js +519 -0
- package/scripts/gds/gen-session-index.js +210 -0
- package/scripts/gds/git-merge-regen.js +147 -0
- package/scripts/gds/grade-smoke.js +205 -0
- package/scripts/gds/hooks/pre-push.js +169 -0
- package/scripts/gds/http-api-client.js +88 -0
- package/scripts/gds/init.js +903 -0
- package/scripts/gds/install-git-hooks.js +114 -0
- package/scripts/gds/key-card.js +213 -0
- package/scripts/gds/key-status.js +126 -0
- package/scripts/gds/land-watch.js +168 -0
- package/scripts/gds/learning-capture.js +145 -0
- package/scripts/gds/lib.sh +44 -0
- package/scripts/gds/linkify-refs.js +473 -0
- package/scripts/gds/local-preview-lib.js +159 -0
- package/scripts/gds/local-preview.js +95 -0
- package/scripts/gds/login.js +225 -0
- package/scripts/gds/main-audit.js +279 -0
- package/scripts/gds/me.js +88 -0
- package/scripts/gds/memory-map.js +174 -0
- package/scripts/gds/merge-lock.js +71 -0
- package/scripts/gds/migration-namespace.js +200 -0
- package/scripts/gds/mirror-lineage-check.js +319 -0
- package/scripts/gds/mirror-redact.js +305 -0
- package/scripts/gds/module.js +482 -0
- package/scripts/gds/newcomer-floor.config.json +13 -0
- package/scripts/gds/newcomer-floor.js +415 -0
- package/scripts/gds/newcomer-floor.templates.json +86 -0
- package/scripts/gds/newcomer-floor.test.js +235 -0
- package/scripts/gds/next-number.js +163 -0
- package/scripts/gds/oauth-secret.js +197 -0
- package/scripts/gds/onboard.js +217 -0
- package/scripts/gds/onboarding-analyze.js +226 -0
- package/scripts/gds/onboarding-config.js +178 -0
- package/scripts/gds/onboarding-ship.js +92 -0
- package/scripts/gds/package-core.js +684 -0
- package/scripts/gds/paste-token.js +131 -0
- package/scripts/gds/portability-smoke-art.sh +85 -0
- package/scripts/gds/portability-smoke-memory.sh +147 -0
- package/scripts/gds/provision.js +1324 -0
- package/scripts/gds/publish-manifest.js +307 -0
- package/scripts/gds/pulse.js +309 -0
- package/scripts/gds/push-main.js +45 -0
- package/scripts/gds/read-session-export.js +359 -0
- package/scripts/gds/reauth.js +149 -0
- package/scripts/gds/recall.js +135 -0
- package/scripts/gds/release.js +71 -0
- package/scripts/gds/render-ideas.js +173 -0
- package/scripts/gds/repo-metrics.js +363 -0
- package/scripts/gds/resolve-skill-model.js +57 -0
- package/scripts/gds/run-routine.js +197 -0
- package/scripts/gds/run-unit-tests.js +178 -0
- package/scripts/gds/sandbox-stage.js +653 -0
- package/scripts/gds/search-ingest-db.js +209 -0
- package/scripts/gds/search-ingest-md.js +225 -0
- package/scripts/gds/security-baseline-check.sh +180 -0
- package/scripts/gds/seed-adopt-tasks.js +75 -0
- package/scripts/gds/seed-api-contract-goal-tasks.js +141 -0
- package/scripts/gds/seed-bongos-app-tasks.js +294 -0
- package/scripts/gds/seed-bongos-coreB-tranche1-tasks.js +81 -0
- package/scripts/gds/seed-bongos-coreB-tranche2-tasks.js +100 -0
- package/scripts/gds/seed-bongos-goal-hierarchy-tasks.js +307 -0
- package/scripts/gds/seed-bongos-module-system-tasks.js +346 -0
- package/scripts/gds/seed-bongos-tasks.js +214 -0
- package/scripts/gds/seed-gds-v4-tasks.js +624 -0
- package/scripts/gds/seed-governance-tasks.js +340 -0
- package/scripts/gds/seed-medusa-tasks.js +440 -0
- package/scripts/gds/seed-module-upstreaming.js +219 -0
- package/scripts/gds/seed-productization-tasks.js +235 -0
- package/scripts/gds/seed-provisioning-tasks.js +83 -0
- package/scripts/gds/seed-staging.sh +104 -0
- package/scripts/gds/seed-v2-world-tasks.js +283 -0
- package/scripts/gds/seed-v3-tasks.js +1981 -0
- package/scripts/gds/session-cost.js +393 -0
- package/scripts/gds/session-digest.js +179 -0
- package/scripts/gds/session-eval.js +576 -0
- package/scripts/gds/session.js +404 -0
- package/scripts/gds/setup.js +504 -0
- package/scripts/gds/shell.js +100 -0
- package/scripts/gds/ship.js +3618 -0
- package/scripts/gds/smoke-batch-claim.sh +213 -0
- package/scripts/gds/smoke-batch-release.sh +296 -0
- package/scripts/gds/smoke-box.sh +150 -0
- package/scripts/gds/smoke-claim-consistency.sh +212 -0
- package/scripts/gds/smoke-dependencies.sh +84 -0
- package/scripts/gds/smoke-gds.js +125 -0
- package/scripts/gds/smoke-write.sh +168 -0
- package/scripts/gds/start.js +782 -0
- package/scripts/gds/status.js +365 -0
- package/scripts/gds/triage.js +366 -0
- package/scripts/gds/upgrade.js +421 -0
- package/scripts/gds/validate-design.js +254 -0
- package/scripts/gds/version-close.js +549 -0
- package/scripts/migrate.sh +183 -0
- package/scripts/migrate_cost_ledger.sh +178 -0
- package/scripts/public-mirror-export.js +291 -0
- package/scripts/render-diagrams.sh +88 -0
- package/scripts/security/dep-audit.js +276 -0
- package/scripts/setup-builder.js +274 -0
- package/scripts/setup-builder.sh +28 -0
- package/scripts/snapshots/backfill-history.mjs +292 -0
- package/scripts/snapshots/maybe-render-on-deploy.sh +66 -0
- package/scripts/snapshots/render-pre-art-eras.mjs +329 -0
- package/scripts/snapshots/render-world.mjs +322 -0
- package/scripts/status-mirror-sync.js +192 -0
- package/scripts/status-probe.mjs +185 -0
- package/server.js +212 -0
- package/src/asset-version.js +71 -0
- package/src/bongos/CLAUDE.md +64 -0
- package/src/bongos/api-errors.js +281 -0
- package/src/bongos/api-path-404.js +67 -0
- package/src/bongos/api-prefix.js +38 -0
- package/src/bongos/app-pair.js +172 -0
- package/src/bongos/auth.js +979 -0
- package/src/bongos/cascade-dispatch.js +123 -0
- package/src/bongos/db-kernel.js +252 -0
- package/src/bongos/db.js +911 -0
- package/src/bongos/goal-scope-check.js +120 -0
- package/src/bongos/hierarchy-config.js +189 -0
- package/src/bongos/live-channel.js +145 -0
- package/src/bongos/llm-cache.js +260 -0
- package/src/bongos/llm-pricing.js +220 -0
- package/src/bongos/metrics.js +286 -0
- package/src/bongos/middleware/audit.js +274 -0
- package/src/bongos/middleware/error-envelope.js +93 -0
- package/src/bongos/middleware/rate-limit.js +195 -0
- package/src/bongos/module-scope-map.js +271 -0
- package/src/bongos/optimizer.js +166 -0
- package/src/bongos/path-match.js +47 -0
- package/src/bongos/permission-path-check.js +248 -0
- package/src/bongos/pool.js +55 -0
- package/src/bongos/repo-info.js +91 -0
- package/src/bongos/route-rank-check.js +420 -0
- package/src/bongos/routes/CLAUDE.md +43 -0
- package/src/bongos/routes/_helpers.js +369 -0
- package/src/bongos/routes/audit-log.js +59 -0
- package/src/bongos/routes/auth.js +755 -0
- package/src/bongos/routes/backup.js +104 -0
- package/src/bongos/routes/builders.js +526 -0
- package/src/bongos/routes/healthz.js +16 -0
- package/src/bongos/routes/instance.js +129 -0
- package/src/bongos/routes/live.js +125 -0
- package/src/bongos/routes/llm-cache.js +119 -0
- package/src/bongos/routes/me.js +380 -0
- package/src/bongos/routes/modules.js +131 -0
- package/src/bongos/routes/optimizer.js +54 -0
- package/src/bongos/routes/public.js +476 -0
- package/src/bongos/routes/security.js +207 -0
- package/src/bongos/routes.js +166 -0
- package/src/bongos/secret-box.js +177 -0
- package/src/bongos/sensitive-surfaces.js +30 -0
- package/src/bongos/serve-internal.js +889 -0
- package/src/bongos/uptime-poller.js +90 -0
- package/src/bongos-downloads.js +70 -0
- package/src/branding.js +182 -0
- package/src/build-info.js +77 -0
- package/src/devbox-downloads.js +67 -0
- package/src/instance-config.js +200 -0
- package/src/module-api.js +366 -0
- package/src/module-loader/loader.js +268 -0
- package/src/module-loader/manifest-schema.js +140 -0
- package/src/module-loader/provenance.js +28 -0
- package/src/module-loader/semver.js +100 -0
- package/src/module-seams.js +291 -0
- package/src/modules.js +254 -0
- package/src/platform-server.js +135 -0
- package/src/ui/adapters/claude-design/index.js +333 -0
- package/src/ui/adapters/figma/index.js +328 -0
- package/tests/abandon_task.mjs +138 -0
- package/tests/achievements_reconcile.mjs +167 -0
- package/tests/adopt_detect.mjs +141 -0
- package/tests/adopt_import.mjs +132 -0
- package/tests/adopt_preflight.mjs +105 -0
- package/tests/anti_grid.mjs +89 -0
- package/tests/api_client.mjs +231 -0
- package/tests/api_docs.mjs +283 -0
- package/tests/api_error_envelope_buildcheck.mjs +56 -0
- package/tests/api_error_envelope_middleware.mjs +99 -0
- package/tests/api_error_registry.mjs +108 -0
- package/tests/api_pagination_contract.mjs +51 -0
- package/tests/api_path_404.mjs +96 -0
- package/tests/app_pair.mjs +168 -0
- package/tests/architect.mjs +140 -0
- package/tests/art_key.mjs +121 -0
- package/tests/art_stage.mjs +121 -0
- package/tests/asset_version.mjs +94 -0
- package/tests/atlas.mjs +69 -0
- package/tests/audit_deps.mjs +74 -0
- package/tests/audit_middleware.mjs +385 -0
- package/tests/auth_admission_gate.mjs +164 -0
- package/tests/auth_anthropic_identity.mjs +95 -0
- package/tests/auth_duplicate_cookie.mjs +56 -0
- package/tests/auth_error_brand.mjs +77 -0
- package/tests/auth_flow_slice1.mjs +223 -0
- package/tests/auth_inactive_builder.mjs +94 -0
- package/tests/auth_page_gate.mjs +92 -0
- package/tests/auth_session_ttl.mjs +155 -0
- package/tests/autonomous_fit_protected_paths.mjs +115 -0
- package/tests/autonomy_gate.mjs +85 -0
- package/tests/autotile_variants.mjs +256 -0
- package/tests/autotile_xverify.mjs +133 -0
- package/tests/backfill_sessions.mjs +101 -0
- package/tests/backfill_task_rewards.mjs +152 -0
- package/tests/backup_cli.mjs +104 -0
- package/tests/bfg.mjs +327 -0
- package/tests/bfg_write.mjs +423 -0
- package/tests/bongos_box_connect.mjs +89 -0
- package/tests/bongos_dispatch.mjs +74 -0
- package/tests/bongos_downloads.mjs +118 -0
- package/tests/bongos_login.mjs +34 -0
- package/tests/box_access.mjs +186 -0
- package/tests/box_auth_check.mjs +114 -0
- package/tests/box_code_staleness.mjs +116 -0
- package/tests/box_connect_e2e.mjs +196 -0
- package/tests/box_cost.mjs +70 -0
- package/tests/box_dns_repoint.mjs +151 -0
- package/tests/box_host_keys.mjs +102 -0
- package/tests/box_onboard.mjs +317 -0
- package/tests/box_scope_session.mjs +115 -0
- package/tests/box_ship_permissions.mjs +71 -0
- package/tests/box_terminal_ssrf.mjs +90 -0
- package/tests/boxes.mjs +1255 -0
- package/tests/branding.mjs +88 -0
- package/tests/bug_attachments.mjs +189 -0
- package/tests/build_info.mjs +88 -0
- package/tests/builder_needs.mjs +99 -0
- package/tests/cap_newcomer_tasks.mjs +114 -0
- package/tests/cascade-dispatch.mjs +95 -0
- package/tests/cf_tunnel.mjs +459 -0
- package/tests/claim_recall.mjs +150 -0
- package/tests/claims_bind_session.mjs +47 -0
- package/tests/claude_materialize.mjs +148 -0
- package/tests/cli_box_scope.mjs +52 -0
- package/tests/cli_lib_formatters.mjs +87 -0
- package/tests/cli_token_reissue.mjs +147 -0
- package/tests/client_bundle_route.mjs +47 -0
- package/tests/cloud_bongos_pack.mjs +85 -0
- package/tests/conductor.mjs +496 -0
- package/tests/confirm_attribution.mjs +156 -0
- package/tests/conflict_resolve.mjs +462 -0
- package/tests/context_pack.mjs +240 -0
- package/tests/cost.mjs +94 -0
- package/tests/cost_audit.mjs +122 -0
- package/tests/cost_budget_status.mjs +78 -0
- package/tests/cost_reconcile.mjs +102 -0
- package/tests/cost_summary_shape.mjs +94 -0
- package/tests/credit_grant.mjs +376 -0
- package/tests/criterion_progress.mjs +220 -0
- package/tests/criterion_proposal.mjs +109 -0
- package/tests/criterion_ref_parse.mjs +53 -0
- package/tests/currency_label.mjs +119 -0
- package/tests/day_cycle.mjs +197 -0
- package/tests/dependencies_migration.mjs +77 -0
- package/tests/dependencies_polymorphic.mjs +212 -0
- package/tests/deploy_instance.mjs +78 -0
- package/tests/devbox_downloads.mjs +122 -0
- package/tests/diff_derived_signals.mjs +115 -0
- package/tests/discord_approvals.mjs +174 -0
- package/tests/discord_autojoin.mjs +158 -0
- package/tests/discord_bot.mjs +94 -0
- package/tests/discord_bugs.mjs +297 -0
- package/tests/discord_channels.mjs +337 -0
- package/tests/discord_foundation.mjs +76 -0
- package/tests/discord_inbound.mjs +190 -0
- package/tests/discord_message_buffer.mjs +87 -0
- package/tests/discord_oauth.mjs +55 -0
- package/tests/discord_reaction.mjs +170 -0
- package/tests/discord_role_sync.mjs +151 -0
- package/tests/display_name.mjs +123 -0
- package/tests/dns_preflight.mjs +74 -0
- package/tests/event_sound_prefs.mjs +194 -0
- package/tests/eviction_policy.mjs +279 -0
- package/tests/figma_adapter.mjs +199 -0
- package/tests/file_map.mjs +91 -0
- package/tests/first_admin_bootstrap.mjs +40 -0
- package/tests/fitness.mjs +435 -0
- package/tests/fitness_two_root.mjs +104 -0
- package/tests/fixtures/session-506-digest.json +34 -0
- package/tests/gate_approvals.mjs +451 -0
- package/tests/gate_review.mjs +152 -0
- package/tests/git_merge_regen.mjs +151 -0
- package/tests/github_push_ancestry.mjs +119 -0
- package/tests/github_push_land_proof.mjs +127 -0
- package/tests/github_push_merge_driver.mjs +175 -0
- package/tests/goal_abuse_matrix.mjs +187 -0
- package/tests/goal_achievement.mjs +146 -0
- package/tests/goal_conflicts.mjs +305 -0
- package/tests/goal_dependencies_read.mjs +73 -0
- package/tests/goal_member_roles.mjs +420 -0
- package/tests/goal_membership_requests.mjs +332 -0
- package/tests/goal_routes.mjs +203 -0
- package/tests/goal_scope_check.mjs +167 -0
- package/tests/goal_tier.mjs +312 -0
- package/tests/goal_wall_proof.mjs +112 -0
- package/tests/grade_attempts.mjs +73 -0
- package/tests/grade_attribution.mjs +117 -0
- package/tests/grade_diff_hash.mjs +191 -0
- package/tests/grade_server_authoritative.mjs +305 -0
- package/tests/grade_unavailable_display.mjs +66 -0
- package/tests/grader.mjs +1960 -0
- package/tests/grader_bypass.mjs +57 -0
- package/tests/grader_panel.mjs +252 -0
- package/tests/grader_prompt_fencing.mjs +192 -0
- package/tests/helpers.mjs +52 -0
- package/tests/hierarchy-config.mjs +81 -0
- package/tests/init.mjs +463 -0
- package/tests/instance_config.mjs +76 -0
- package/tests/instance_manifest.mjs +110 -0
- package/tests/interaction_prefs.mjs +72 -0
- package/tests/key-card.mjs +110 -0
- package/tests/key-status.mjs +45 -0
- package/tests/land_watch.mjs +115 -0
- package/tests/lifecycle-card.mjs +145 -0
- package/tests/lifecycle_breakdown_view.mjs +236 -0
- package/tests/linkify_refs.mjs +264 -0
- package/tests/live_channel.mjs +128 -0
- package/tests/live_sse.mjs +122 -0
- package/tests/llm-cache-route.mjs +110 -0
- package/tests/llm-cache.mjs +140 -0
- package/tests/llm-pricing.mjs +152 -0
- package/tests/local_preview.mjs +136 -0
- package/tests/main_audit.mjs +137 -0
- package/tests/memory_isolation.mjs +431 -0
- package/tests/memory_map.mjs +107 -0
- package/tests/memory_pull.mjs +429 -0
- package/tests/merge_lease.mjs +100 -0
- package/tests/merge_lock.mjs +312 -0
- package/tests/metrics.mjs +147 -0
- package/tests/migration_allocation.mjs +146 -0
- package/tests/migration_namespace.mjs +185 -0
- package/tests/migrations_fresh_replay_guard.mjs +38 -0
- package/tests/mirror_lineage_fulltree.mjs +45 -0
- package/tests/mirror_lineage_proof.mjs +72 -0
- package/tests/mirror_redact.mjs +193 -0
- package/tests/model_alloc_prefs.mjs +63 -0
- package/tests/module-scope-map.mjs +88 -0
- package/tests/module_api.mjs +110 -0
- package/tests/module_cli.mjs +292 -0
- package/tests/module_contributions.mjs +148 -0
- package/tests/module_loader.mjs +286 -0
- package/tests/module_manifest.mjs +194 -0
- package/tests/module_owned_tests.mjs +105 -0
- package/tests/module_provenance.mjs +34 -0
- package/tests/module_route_rank.mjs +147 -0
- package/tests/module_seams.mjs +68 -0
- package/tests/modules.mjs +156 -0
- package/tests/modules_route.mjs +38 -0
- package/tests/newcomer_floor.mjs +17 -0
- package/tests/newcomer_restock.mjs +80 -0
- package/tests/next_number.mjs +154 -0
- package/tests/oauth_secret.mjs +60 -0
- package/tests/onboard.mjs +70 -0
- package/tests/onboarding_gate.mjs +267 -0
- package/tests/onboarding_observability.mjs +344 -0
- package/tests/onboarding_route_signals.mjs +175 -0
- package/tests/onboarding_state.mjs +346 -0
- package/tests/one_active_claim_per_session.mjs +111 -0
- package/tests/orphaned_claim_ship.mjs +135 -0
- package/tests/override_attribution.mjs +142 -0
- package/tests/override_request_ownership.mjs +235 -0
- package/tests/package_core.mjs +435 -0
- package/tests/parallel-load.mjs +402 -0
- package/tests/path_match.mjs +62 -0
- package/tests/permission_path.mjs +151 -0
- package/tests/platform_boot.mjs +167 -0
- package/tests/pre_compact.mjs +60 -0
- package/tests/preview_server.mjs +127 -0
- package/tests/profile_route.mjs +197 -0
- package/tests/provision.mjs +805 -0
- package/tests/provisioning_github_app_routes.mjs +98 -0
- package/tests/public_mirror_export.mjs +124 -0
- package/tests/public_task_resolver.mjs +112 -0
- package/tests/publish_branch_route.mjs +422 -0
- package/tests/publish_manifest.mjs +184 -0
- package/tests/publish_reconciler.mjs +728 -0
- package/tests/rank_gate.mjs +463 -0
- package/tests/rate_limit_public.mjs +217 -0
- package/tests/read_session_export.mjs +89 -0
- package/tests/recall_cli.mjs +82 -0
- package/tests/recall_hints.mjs +119 -0
- package/tests/recent_shipped_window.mjs +80 -0
- package/tests/regrade_eligibility.mjs +202 -0
- package/tests/render_prefs.mjs +122 -0
- package/tests/repo_map.mjs +92 -0
- package/tests/reward_gate.mjs +284 -0
- package/tests/route_rank_check.mjs +243 -0
- package/tests/route_validation.mjs +490 -0
- package/tests/routine_timers.mjs +72 -0
- package/tests/rubric_fixtures.mjs +140 -0
- package/tests/run_routine.mjs +69 -0
- package/tests/sandbox_stage.mjs +582 -0
- package/tests/scrubber_corpus.mjs +257 -0
- package/tests/search_chunker.mjs +209 -0
- package/tests/search_ingest.mjs +96 -0
- package/tests/search_ingest_db.mjs +88 -0
- package/tests/search_isolation.mjs +204 -0
- package/tests/search_route.mjs +95 -0
- package/tests/secret_box.mjs +189 -0
- package/tests/security_docs.mjs +186 -0
- package/tests/security_route.mjs +108 -0
- package/tests/served_paths_shipped.mjs +118 -0
- package/tests/session-cards.mjs +142 -0
- package/tests/session_506_regression.mjs +69 -0
- package/tests/session_caps.mjs +238 -0
- package/tests/session_cost.mjs +133 -0
- package/tests/session_digest.mjs +73 -0
- package/tests/session_eval.mjs +386 -0
- package/tests/session_index.mjs +105 -0
- package/tests/session_records.mjs +369 -0
- package/tests/session_search.mjs +78 -0
- package/tests/session_start_freshness.mjs +123 -0
- package/tests/setup_smoke.mjs +154 -0
- package/tests/ship_autolinkify.mjs +60 -0
- package/tests/ship_automerge_injection.mjs +174 -0
- package/tests/ship_broadcast_routing.mjs +125 -0
- package/tests/ship_broadcast_server.mjs +175 -0
- package/tests/ship_card.mjs +280 -0
- package/tests/ship_ci_deploy.mjs +374 -0
- package/tests/ship_preflight.mjs +368 -0
- package/tests/ship_premerge.mjs +75 -0
- package/tests/ship_resume.mjs +129 -0
- package/tests/ship_upload_skip.mjs +89 -0
- package/tests/ship_zero_commit.mjs +60 -0
- package/tests/single_host_hall.mjs +98 -0
- package/tests/skill_prefs.mjs +150 -0
- package/tests/smoke_preflight.mjs +66 -0
- package/tests/snapshot_picker.mjs +233 -0
- package/tests/sound_prefs.mjs +198 -0
- package/tests/sprite_facing.mjs +35 -0
- package/tests/status-probe.mjs +89 -0
- package/tests/status_backfill.mjs +94 -0
- package/tests/stop_refcheck.mjs +127 -0
- package/tests/streak.mjs +175 -0
- package/tests/subdomain_auth.mjs +58 -0
- package/tests/task_classifier_vocab.mjs +41 -0
- package/tests/task_create_idempotency.mjs +96 -0
- package/tests/task_goal_link.mjs +93 -0
- package/tests/task_ownership_gate.mjs +125 -0
- package/tests/terrain_picker.mjs +298 -0
- package/tests/touches_overlap.mjs +75 -0
- package/tests/triage_development.mjs +140 -0
- package/tests/turn_in_place.mjs +280 -0
- package/tests/upgrade.mjs +357 -0
- package/tests/verify_task_guard.mjs +64 -0
- package/tests/version_close.mjs +76 -0
- package/tests/versions_route.mjs +93 -0
- package/tests/walkability.mjs +188 -0
- package/tests/wandering_prefs.mjs +176 -0
- package/tests/worktree_disambiguation.mjs +105 -0
- package/tests/worktree_guard.mjs +90 -0
|
@@ -0,0 +1,979 @@
|
|
|
1
|
+
// GDS auth (renamed from PMS in V3.R01).
|
|
2
|
+
//
|
|
3
|
+
// Two flows that produce the same `builder_sessions` row:
|
|
4
|
+
//
|
|
5
|
+
// - Device Flow (CLI):
|
|
6
|
+
// 1. POST /api/gds/auth/device/start
|
|
7
|
+
// → { user_code, verification_uri, device_code, interval, expires_in }
|
|
8
|
+
// The CLI shows user_code to the user and asks them to open
|
|
9
|
+
// verification_uri in a browser.
|
|
10
|
+
// 2. CLI polls POST /api/gds/auth/device/poll { device_code }
|
|
11
|
+
// → 'pending' | 'slow_down' | 'expired' | 'denied' |
|
|
12
|
+
// { token, builder: {...} }
|
|
13
|
+
//
|
|
14
|
+
// - Web Flow (browser):
|
|
15
|
+
// 1. GET /builders/auth/github → 302 to github.com/login/oauth/authorize
|
|
16
|
+
// 2. GET /builders/auth/github/callback?code=... → exchanges, sets cookie, 302 /builders
|
|
17
|
+
//
|
|
18
|
+
// Both flows hit GitHub's `GET /user` to learn the GitHub user id+login,
|
|
19
|
+
// then upsert into `builders` and create a `builder_sessions` row.
|
|
20
|
+
//
|
|
21
|
+
// Credentials:
|
|
22
|
+
// GDS_GITHUB_CLIENT_ID / GDS_GITHUB_CLIENT_SECRET in env (systemd EnvironmentFile).
|
|
23
|
+
// Back-compat: also accepts the legacy PMS_GITHUB_CLIENT_ID / PMS_GITHUB_CLIENT_SECRET
|
|
24
|
+
// names so the droplet's EnvironmentFile rename can lag the code deploy.
|
|
25
|
+
// Local-dev fallback: ~/.config/otb/gds-github.json (or legacy pms-github.json).
|
|
26
|
+
|
|
27
|
+
const crypto = require('node:crypto');
|
|
28
|
+
const fs = require('node:fs');
|
|
29
|
+
const path = require('node:path');
|
|
30
|
+
const os = require('node:os');
|
|
31
|
+
|
|
32
|
+
// Identity/auth/admission + the rank vocabulary are KERNEL primitives — auth.js is
|
|
33
|
+
// a KERNEL_FILE, so it resolves them from ./db-kernel, never the ./db domain residue
|
|
34
|
+
// (ADR 0102, finishing ADR 0091 §1). db.js re-exports the same names for its other
|
|
35
|
+
// callers, so nothing downstream changed.
|
|
36
|
+
const {
|
|
37
|
+
upsertBuilder,
|
|
38
|
+
seatFirstAdminIfUnclaimed,
|
|
39
|
+
createSession,
|
|
40
|
+
lookupSession,
|
|
41
|
+
getBuilderById,
|
|
42
|
+
getBuilderByGithubId,
|
|
43
|
+
hasInvitedAccessRequest,
|
|
44
|
+
RANKS,
|
|
45
|
+
} = require('./db-kernel');
|
|
46
|
+
const { pool } = require('./pool');
|
|
47
|
+
// The onboarding state machine carved into modules/onboarding/ (BV1.R81 / ADR
|
|
48
|
+
// 0093 §1). The auth callbacks mark `auth_complete` through the `onboarding`
|
|
49
|
+
// kernel port — never a direct require of a module. Default-on (present in prod);
|
|
50
|
+
// the optional `?.` degrades to a no-op when the module is off (the stage tick is
|
|
51
|
+
// best-effort anyway).
|
|
52
|
+
const seams = require('../module-seams');
|
|
53
|
+
const ic = require('../instance-config');
|
|
54
|
+
const { branding, userAgent } = require('../branding');
|
|
55
|
+
|
|
56
|
+
const SCOPE = 'read:user';
|
|
57
|
+
|
|
58
|
+
// In-memory map of device_code → { interval, expires_at, last_polled_at, github_creds }
|
|
59
|
+
// Device flow is ephemeral; restart of the Node process invalidates pending flows
|
|
60
|
+
// (the user just re-runs the setup skill). Acceptable for V1 — scaling up means
|
|
61
|
+
// moving this to Redis or a tiny `device_flows` table.
|
|
62
|
+
const deviceFlows = new Map();
|
|
63
|
+
|
|
64
|
+
function loadGithubCreds() {
|
|
65
|
+
// Resolve the configured instance prefix first (CLOUDBONGOS_ on a fresh
|
|
66
|
+
// instance, OTB_ on this one), then the legacy GDS_/PMS_ names — so the
|
|
67
|
+
// droplet's EnvironmentFile rename can lag the code deploy without breaking
|
|
68
|
+
// auth (R61 / task 1200).
|
|
69
|
+
const clientId = ic.resolveEnv('GITHUB_CLIENT_ID');
|
|
70
|
+
const clientSecret = ic.resolveEnv('GITHUB_CLIENT_SECRET');
|
|
71
|
+
if (clientId && clientSecret) {
|
|
72
|
+
return { client_id: clientId, client_secret: clientSecret };
|
|
73
|
+
}
|
|
74
|
+
// Local-dev fallback: try the new filename first, then the legacy one — each
|
|
75
|
+
// across the configured config dir and the legacy ~/.config/otb dir.
|
|
76
|
+
for (const name of ['gds-github.json', 'pms-github.json']) {
|
|
77
|
+
try {
|
|
78
|
+
const raw = ic.readConfigFileSync(name);
|
|
79
|
+
if (raw) {
|
|
80
|
+
const creds = JSON.parse(raw);
|
|
81
|
+
if (creds.client_id && creds.client_secret) return creds;
|
|
82
|
+
}
|
|
83
|
+
} catch (_) {
|
|
84
|
+
/* try next */
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function authConfigured() {
|
|
91
|
+
return !!loadGithubCreds();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Admission posture (SR-19 / task #1000). BOTH enrollment paths — the web hall
|
|
95
|
+
// sign-in (completeWebFlow) and the CLI device flow (pollDeviceFlow) — are
|
|
96
|
+
// "invite-gated by default": a FIRST-TIME GitHub account is admitted only if an
|
|
97
|
+
// Archon marked their access_request 'invited'. An EXISTING builder (a row
|
|
98
|
+
// already exists for their github_id) ALWAYS passes — the gate never locks out
|
|
99
|
+
// someone already enrolled.
|
|
100
|
+
//
|
|
101
|
+
// OTB_OPEN_XENOS_ENROLLMENT is the escape-hatch: set it truthy ('1' or 'true')
|
|
102
|
+
// to restore fully-open self-enrollment (anyone with a GitHub account becomes a
|
|
103
|
+
// Xenos) WITHOUT a code change, for an owner who deliberately wants open
|
|
104
|
+
// enrollment. Default (unset/anything-else) = invite required. See ADR 0050.
|
|
105
|
+
function openEnrollmentEnabled() {
|
|
106
|
+
const v = ic.resolveEnv('OPEN_XENOS_ENROLLMENT');
|
|
107
|
+
return v === '1' || v === 'true';
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// PURE admission decision shared by both flows so they CANNOT drift (SR-19).
|
|
111
|
+
// Returns true when a first-time account must be blocked. `existing` is the
|
|
112
|
+
// builder row (or null) from getBuilderByGithubId; `approved` is the result of
|
|
113
|
+
// hasInvitedAccessRequest. Blocks only when: not already a builder AND open
|
|
114
|
+
// enrollment is off AND not invite-approved. Exported for direct unit testing.
|
|
115
|
+
function firstTimeAdmissionBlocked({ existing, approved, login }) {
|
|
116
|
+
if (existing || openEnrollmentEnabled() || approved) return false;
|
|
117
|
+
// The configured firstAdmin is always admitted on a fresh instance so the
|
|
118
|
+
// bootstrap (maybeSeatFirstAdmin) can seat them as the first Archon — the owner
|
|
119
|
+
// must not be gated out of their own new instance when no Archon exists yet to
|
|
120
|
+
// approve them (the chicken-and-egg R62 / ADR 0062 §6 left open).
|
|
121
|
+
// seatFirstAdminIfUnclaimed is race-safe and only fires while NO Archon exists,
|
|
122
|
+
// so this never escalates an account on an established instance; it only lets
|
|
123
|
+
// the configured owner login through admission.
|
|
124
|
+
const fa = firstAdminLogin();
|
|
125
|
+
if (fa && String(login || '').toLowerCase() === fa.toLowerCase()) return false;
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// First-admin bootstrap (R62 / task 1201, ADR 0062). The configured firstAdmin
|
|
130
|
+
// login — an install-time env override (<PREFIX>_FIRST_ADMIN, incl. legacy) wins,
|
|
131
|
+
// else branding.firstAdmin. Empty/unset (the vanilla starter) means "no one is
|
|
132
|
+
// auto-seated"; the owner sets it when scaffolding the instance. Trimmed; '' → null.
|
|
133
|
+
function firstAdminLogin() {
|
|
134
|
+
let login = ic.resolveEnv('FIRST_ADMIN');
|
|
135
|
+
if (login === undefined) {
|
|
136
|
+
try { login = branding().firstAdmin; } catch { login = undefined; }
|
|
137
|
+
}
|
|
138
|
+
login = (login || '').trim();
|
|
139
|
+
return login || null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Seat the configured firstAdmin as Archon at their first enrollment, but only
|
|
143
|
+
// while NO Archon exists yet (the db guard is atomic + race-safe, so this can
|
|
144
|
+
// never escalate an account once an owner exists). Best-effort: a bootstrap
|
|
145
|
+
// hiccup must never fail the sign-in. Returns the builder, with rank updated to
|
|
146
|
+
// 'archon' on the returned object if this call seated it.
|
|
147
|
+
async function maybeSeatFirstAdmin(builder) {
|
|
148
|
+
const configured = firstAdminLogin();
|
|
149
|
+
if (!configured || !builder || builder.rank === 'archon') return builder;
|
|
150
|
+
if (String(builder.github_login || '').toLowerCase() !== configured.toLowerCase()) return builder;
|
|
151
|
+
try {
|
|
152
|
+
const seated = await seatFirstAdminIfUnclaimed(builder.id, builder.github_login);
|
|
153
|
+
if (seated) {
|
|
154
|
+
console.log(`[auth] first-admin bootstrap: seated ${builder.github_login} as Archon (configured firstAdmin, no prior Archon).`);
|
|
155
|
+
return { ...builder, rank: 'archon' };
|
|
156
|
+
}
|
|
157
|
+
} catch (err) {
|
|
158
|
+
console.error('[auth] first-admin bootstrap failed (non-fatal):', err.message);
|
|
159
|
+
}
|
|
160
|
+
return builder;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function newSessionToken() {
|
|
164
|
+
return crypto.randomBytes(32).toString('hex');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// -------------------------------------------------------------------------
|
|
168
|
+
// GitHub helpers
|
|
169
|
+
// -------------------------------------------------------------------------
|
|
170
|
+
|
|
171
|
+
async function fetchGithubUser(accessToken) {
|
|
172
|
+
const res = await fetch('https://api.github.com/user', {
|
|
173
|
+
headers: {
|
|
174
|
+
Authorization: `Bearer ${accessToken}`,
|
|
175
|
+
'User-Agent': userAgent(),
|
|
176
|
+
Accept: 'application/vnd.github+json',
|
|
177
|
+
},
|
|
178
|
+
});
|
|
179
|
+
if (!res.ok) {
|
|
180
|
+
const text = await res.text();
|
|
181
|
+
throw new Error(`GitHub /user failed (${res.status}): ${text}`);
|
|
182
|
+
}
|
|
183
|
+
return res.json();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function exchangeCodeForToken(code, redirectUri) {
|
|
187
|
+
const creds = loadGithubCreds();
|
|
188
|
+
if (!creds) throw new Error('GitHub OAuth credentials not configured');
|
|
189
|
+
const res = await fetch('https://github.com/login/oauth/access_token', {
|
|
190
|
+
method: 'POST',
|
|
191
|
+
headers: {
|
|
192
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
193
|
+
Accept: 'application/json',
|
|
194
|
+
},
|
|
195
|
+
body: new URLSearchParams({
|
|
196
|
+
client_id: creds.client_id,
|
|
197
|
+
client_secret: creds.client_secret,
|
|
198
|
+
code,
|
|
199
|
+
redirect_uri: redirectUri,
|
|
200
|
+
}).toString(),
|
|
201
|
+
});
|
|
202
|
+
const data = await res.json();
|
|
203
|
+
if (data.error) throw new Error(`GitHub token exchange: ${data.error_description || data.error}`);
|
|
204
|
+
return data.access_token;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// -------------------------------------------------------------------------
|
|
208
|
+
// Device Flow (CLI)
|
|
209
|
+
// -------------------------------------------------------------------------
|
|
210
|
+
|
|
211
|
+
async function startDeviceFlow() {
|
|
212
|
+
const creds = loadGithubCreds();
|
|
213
|
+
if (!creds) throw new Error('GitHub OAuth credentials not configured');
|
|
214
|
+
const res = await fetch('https://github.com/login/device/code', {
|
|
215
|
+
method: 'POST',
|
|
216
|
+
headers: {
|
|
217
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
218
|
+
Accept: 'application/json',
|
|
219
|
+
},
|
|
220
|
+
body: new URLSearchParams({ client_id: creds.client_id, scope: SCOPE }).toString(),
|
|
221
|
+
});
|
|
222
|
+
if (!res.ok) {
|
|
223
|
+
const text = await res.text();
|
|
224
|
+
throw new Error(`device/code failed (${res.status}): ${text}`);
|
|
225
|
+
}
|
|
226
|
+
const data = await res.json();
|
|
227
|
+
// data: { device_code, user_code, verification_uri, expires_in, interval }
|
|
228
|
+
deviceFlows.set(data.device_code, {
|
|
229
|
+
interval: data.interval || 5,
|
|
230
|
+
expires_at: Date.now() + data.expires_in * 1000,
|
|
231
|
+
last_polled_at: 0,
|
|
232
|
+
});
|
|
233
|
+
// Reap expired flows (cheap)
|
|
234
|
+
for (const [k, v] of deviceFlows) {
|
|
235
|
+
if (v.expires_at < Date.now()) deviceFlows.delete(k);
|
|
236
|
+
}
|
|
237
|
+
return {
|
|
238
|
+
device_code: data.device_code,
|
|
239
|
+
user_code: data.user_code,
|
|
240
|
+
verification_uri: data.verification_uri,
|
|
241
|
+
expires_in: data.expires_in,
|
|
242
|
+
interval: data.interval || 5,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async function pollDeviceFlow(deviceCode, userAgent) {
|
|
247
|
+
const flow = deviceFlows.get(deviceCode);
|
|
248
|
+
if (!flow) return { state: 'expired' };
|
|
249
|
+
if (flow.expires_at < Date.now()) {
|
|
250
|
+
deviceFlows.delete(deviceCode);
|
|
251
|
+
return { state: 'expired' };
|
|
252
|
+
}
|
|
253
|
+
// Server-side throttling: GitHub returns slow_down if we hammer; we
|
|
254
|
+
// also enforce the interval client-to-server.
|
|
255
|
+
const now = Date.now();
|
|
256
|
+
if (now - flow.last_polled_at < flow.interval * 1000) {
|
|
257
|
+
return { state: 'slow_down', interval: flow.interval };
|
|
258
|
+
}
|
|
259
|
+
flow.last_polled_at = now;
|
|
260
|
+
|
|
261
|
+
const creds = loadGithubCreds();
|
|
262
|
+
const res = await fetch('https://github.com/login/oauth/access_token', {
|
|
263
|
+
method: 'POST',
|
|
264
|
+
headers: {
|
|
265
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
266
|
+
Accept: 'application/json',
|
|
267
|
+
},
|
|
268
|
+
body: new URLSearchParams({
|
|
269
|
+
client_id: creds.client_id,
|
|
270
|
+
device_code: deviceCode,
|
|
271
|
+
grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
|
|
272
|
+
}).toString(),
|
|
273
|
+
});
|
|
274
|
+
const data = await res.json();
|
|
275
|
+
if (data.error === 'authorization_pending') return { state: 'pending' };
|
|
276
|
+
if (data.error === 'slow_down') {
|
|
277
|
+
flow.interval = (flow.interval || 5) + 5;
|
|
278
|
+
return { state: 'slow_down', interval: flow.interval };
|
|
279
|
+
}
|
|
280
|
+
if (data.error === 'expired_token') {
|
|
281
|
+
deviceFlows.delete(deviceCode);
|
|
282
|
+
return { state: 'expired' };
|
|
283
|
+
}
|
|
284
|
+
if (data.error === 'access_denied') {
|
|
285
|
+
deviceFlows.delete(deviceCode);
|
|
286
|
+
return { state: 'denied' };
|
|
287
|
+
}
|
|
288
|
+
if (data.access_token) {
|
|
289
|
+
deviceFlows.delete(deviceCode);
|
|
290
|
+
const ghUser = await fetchGithubUser(data.access_token);
|
|
291
|
+
// Admission gate (SR-19 / task #1000): mirror completeWebFlow EXACTLY. The
|
|
292
|
+
// CLI device flow used to call upsertBuilder unconditionally, so any GitHub
|
|
293
|
+
// account could self-enroll as a Xenos + mint a CLI session with no Archon
|
|
294
|
+
// approval — contradicting the "Archon admission is the only gate" rule. Now
|
|
295
|
+
// a FIRST-TIME account (no existing builder row for this github_id) is
|
|
296
|
+
// admitted only if open enrollment is on OR an Archon marked their
|
|
297
|
+
// access_request 'invited'. An EXISTING builder ALWAYS passes, so an approved
|
|
298
|
+
// builder can never be locked out of CLI enrollment. See ADR 0050.
|
|
299
|
+
const existing = await getBuilderByGithubId(ghUser.id);
|
|
300
|
+
// Only consult the access_request table for a genuine first-timer (and only
|
|
301
|
+
// when open enrollment is off) — an existing builder short-circuits to
|
|
302
|
+
// ALWAYS-pass, matching completeWebFlow's `if (!existing)` lookup.
|
|
303
|
+
if (!existing && !openEnrollmentEnabled()) {
|
|
304
|
+
const approved = await hasInvitedAccessRequest(ghUser.login);
|
|
305
|
+
if (firstTimeAdmissionBlocked({ existing, approved, login: ghUser.login })) {
|
|
306
|
+
const err = new Error('not_approved');
|
|
307
|
+
err.code = 'NOT_APPROVED';
|
|
308
|
+
throw err;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
let builder = await upsertBuilder({
|
|
312
|
+
githubId: ghUser.id,
|
|
313
|
+
githubLogin: ghUser.login,
|
|
314
|
+
displayName: ghUser.name || ghUser.login,
|
|
315
|
+
avatarUrl: ghUser.avatar_url,
|
|
316
|
+
});
|
|
317
|
+
builder = await maybeSeatFirstAdmin(builder);
|
|
318
|
+
const token = newSessionToken();
|
|
319
|
+
await createSession({
|
|
320
|
+
builderId: builder.id,
|
|
321
|
+
token,
|
|
322
|
+
source: 'cli',
|
|
323
|
+
userAgent: userAgent ?? null,
|
|
324
|
+
// CLI tokens have no expiry (manually revocable). Web tokens expire.
|
|
325
|
+
expiresAt: null,
|
|
326
|
+
});
|
|
327
|
+
// V3.R75 (#294): tick the onboarding state machine. Best-effort — auth
|
|
328
|
+
// success must NOT fail because the onboarding plumbing tripped.
|
|
329
|
+
await seams.resolveOptional('onboarding')?.safeMarkStage(pool, builder.id, 'auth_complete', builder);
|
|
330
|
+
return { state: 'ok', token, builder };
|
|
331
|
+
}
|
|
332
|
+
return { state: 'pending' };
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// -------------------------------------------------------------------------
|
|
336
|
+
// Web Flow (browser)
|
|
337
|
+
// -------------------------------------------------------------------------
|
|
338
|
+
|
|
339
|
+
function buildAuthorizeUrl(state, redirectUri) {
|
|
340
|
+
const creds = loadGithubCreds();
|
|
341
|
+
if (!creds) throw new Error('GitHub OAuth credentials not configured');
|
|
342
|
+
const u = new URL('https://github.com/login/oauth/authorize');
|
|
343
|
+
u.searchParams.set('client_id', creds.client_id);
|
|
344
|
+
u.searchParams.set('redirect_uri', redirectUri);
|
|
345
|
+
u.searchParams.set('scope', SCOPE);
|
|
346
|
+
u.searchParams.set('state', state);
|
|
347
|
+
return u.toString();
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
async function completeWebFlow({ code, redirectUri, userAgent }) {
|
|
351
|
+
const accessToken = await exchangeCodeForToken(code, redirectUri);
|
|
352
|
+
const ghUser = await fetchGithubUser(accessToken);
|
|
353
|
+
// Approval gate (2026-05-30): the web hall sign-in no longer admits anyone
|
|
354
|
+
// with a GitHub account. A first-time visitor must have been approved by an
|
|
355
|
+
// Archon (their access_request marked 'invited') before a builder row is
|
|
356
|
+
// created. This Archon approval is the ONLY gate — newcomers build on hosted
|
|
357
|
+
// dev boxes, so there is no GitHub repo-access / collaborator check anywhere
|
|
358
|
+
// (2026-06-07). Established builders (a row already exists for this github_id)
|
|
359
|
+
// always pass — this only blocks NEW first-time entries.
|
|
360
|
+
//
|
|
361
|
+
// SR-19 / task #1000 (2026-06-13): the CLI device flow (pollDeviceFlow,
|
|
362
|
+
// /builder-setup) is now gated the SAME way — it previously called
|
|
363
|
+
// upsertBuilder unconditionally, letting any GitHub account self-enroll as a
|
|
364
|
+
// Xenos with no Archon approval. Both paths share firstTimeAdmissionBlocked()
|
|
365
|
+
// so they cannot drift. An owner who wants fully-open self-enrollment sets
|
|
366
|
+
// OTB_OPEN_XENOS_ENROLLMENT truthy (no code change); default = invite required.
|
|
367
|
+
// See ADR 0050.
|
|
368
|
+
const existing = await getBuilderByGithubId(ghUser.id);
|
|
369
|
+
if (!existing && !openEnrollmentEnabled()) {
|
|
370
|
+
const approved = await hasInvitedAccessRequest(ghUser.login);
|
|
371
|
+
// Pass `login` so the configured-firstAdmin bypass (firstTimeAdmissionBlocked
|
|
372
|
+
// L124-125) fires on WEB sign-in too — without it the founding owner is gated
|
|
373
|
+
// out of their own fresh instance at "The Gate is Closed", a chicken-and-egg
|
|
374
|
+
// since no Archon exists yet to approve them. The device flow (pollDeviceFlow,
|
|
375
|
+
// ~L305) already passes login; this keeps the two paths from drifting (SR-19).
|
|
376
|
+
if (firstTimeAdmissionBlocked({ existing, approved, login: ghUser.login })) {
|
|
377
|
+
const err = new Error('not_approved');
|
|
378
|
+
err.code = 'NOT_APPROVED';
|
|
379
|
+
throw err;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
let builder = await upsertBuilder({
|
|
383
|
+
githubId: ghUser.id,
|
|
384
|
+
githubLogin: ghUser.login,
|
|
385
|
+
displayName: ghUser.name || ghUser.login,
|
|
386
|
+
avatarUrl: ghUser.avatar_url,
|
|
387
|
+
});
|
|
388
|
+
builder = await maybeSeatFirstAdmin(builder);
|
|
389
|
+
const token = newSessionToken();
|
|
390
|
+
// Web sessions expire after 30 days; the user re-auths on the page
|
|
391
|
+
// when the cookie expires.
|
|
392
|
+
const expiresAt = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000);
|
|
393
|
+
await createSession({
|
|
394
|
+
builderId: builder.id,
|
|
395
|
+
token,
|
|
396
|
+
source: 'web',
|
|
397
|
+
userAgent: userAgent ?? null,
|
|
398
|
+
expiresAt,
|
|
399
|
+
});
|
|
400
|
+
// V3.R75 (#294): tick the onboarding state machine. Best-effort.
|
|
401
|
+
await seams.resolveOptional('onboarding')?.safeMarkStage(pool, builder.id, 'auth_complete', builder);
|
|
402
|
+
return { token, builder, expiresAt };
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// -------------------------------------------------------------------------
|
|
406
|
+
// Discord OAuth (account-linking — Phase 3 F2, ADR 0033 §2)
|
|
407
|
+
// -------------------------------------------------------------------------
|
|
408
|
+
// Links a Discord account to an ALREADY-AUTHENTICATED builder. Client creds
|
|
409
|
+
// come from env (DISCORD_CLIENT_ID / DISCORD_CLIENT_SECRET) or, for local dev,
|
|
410
|
+
// ~/.config/otb/discord-bot.json — same env-first-then-file shape as GitHub.
|
|
411
|
+
// scope=identify (the Discord user id, for the link) + guilds.join (so the
|
|
412
|
+
// server can drop the just-linked builder straight into the guild — the
|
|
413
|
+
// "link-first auto-join" onboarding gate, F7/ADR 0033: a builder approves the
|
|
414
|
+
// link BEFORE entering, so their rank role is already correct on arrival; there
|
|
415
|
+
// is no invite link to share or leak). guilds.join is only USABLE alongside the
|
|
416
|
+
// bot adding the member, and only if the bot holds CREATE_INSTANT_INVITE — see
|
|
417
|
+
// addDiscordGuildMember + docs/discord/role-sync.md. The pure helpers
|
|
418
|
+
// (discordAuthorizeUrl, isValidDiscordUserId) are exported for unit testing
|
|
419
|
+
// without creds or network.
|
|
420
|
+
const DISCORD_SCOPE = 'identify guilds.join';
|
|
421
|
+
|
|
422
|
+
function loadDiscordCreds() {
|
|
423
|
+
const clientId = process.env.DISCORD_CLIENT_ID;
|
|
424
|
+
const clientSecret = process.env.DISCORD_CLIENT_SECRET;
|
|
425
|
+
if (clientId && clientSecret) return { client_id: clientId, client_secret: clientSecret };
|
|
426
|
+
try {
|
|
427
|
+
const raw = ic.readConfigFileSync('discord-bot.json');
|
|
428
|
+
if (raw) {
|
|
429
|
+
const creds = JSON.parse(raw);
|
|
430
|
+
if (creds.client_id && creds.client_secret) {
|
|
431
|
+
return { client_id: creds.client_id, client_secret: creds.client_secret };
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
} catch (_) { /* not configured */ }
|
|
435
|
+
return null;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function discordConfigured() {
|
|
439
|
+
return !!loadDiscordCreds();
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// Bot token loader (Phase 3 F3, ADR 0033 §F3). The bot authenticates to the
|
|
443
|
+
// Discord GATEWAY with a bot token — distinct from the OAuth client_id/secret
|
|
444
|
+
// used for account-linking (F2). Same env-first-then-file shape as everything
|
|
445
|
+
// else: env DISCORD_BOT_TOKEN wins (the systemd path ADR 0022 prefers for the
|
|
446
|
+
// always-on droplet process), else the bot_token field of the local
|
|
447
|
+
// ~/.config/otb/discord-bot.json file (already on the droplet, mode 600). The
|
|
448
|
+
// token is a secret — never log it, never return it anywhere user-facing.
|
|
449
|
+
function loadDiscordBotToken() {
|
|
450
|
+
const envToken = process.env.DISCORD_BOT_TOKEN;
|
|
451
|
+
if (envToken) return envToken;
|
|
452
|
+
try {
|
|
453
|
+
const raw = ic.readConfigFileSync('discord-bot.json');
|
|
454
|
+
if (raw) {
|
|
455
|
+
const creds = JSON.parse(raw);
|
|
456
|
+
if (creds.bot_token) return creds.bot_token;
|
|
457
|
+
}
|
|
458
|
+
} catch (_) { /* not configured */ }
|
|
459
|
+
return null;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function discordBotConfigured() {
|
|
463
|
+
return !!loadDiscordBotToken();
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// PURE — build the Discord authorize URL (testable without creds/network).
|
|
467
|
+
function discordAuthorizeUrl(clientId, state, redirectUri) {
|
|
468
|
+
const u = new URL('https://discord.com/api/oauth2/authorize');
|
|
469
|
+
u.searchParams.set('client_id', clientId);
|
|
470
|
+
u.searchParams.set('redirect_uri', redirectUri);
|
|
471
|
+
u.searchParams.set('response_type', 'code');
|
|
472
|
+
u.searchParams.set('scope', DISCORD_SCOPE);
|
|
473
|
+
u.searchParams.set('state', state);
|
|
474
|
+
return u.toString();
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function buildDiscordAuthorizeUrl(state, redirectUri) {
|
|
478
|
+
const creds = loadDiscordCreds();
|
|
479
|
+
if (!creds) throw new Error('Discord OAuth credentials not configured');
|
|
480
|
+
return discordAuthorizeUrl(creds.client_id, state, redirectUri);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
async function exchangeDiscordCode(code, redirectUri) {
|
|
484
|
+
const creds = loadDiscordCreds();
|
|
485
|
+
if (!creds) throw new Error('Discord OAuth credentials not configured');
|
|
486
|
+
const res = await fetch('https://discord.com/api/oauth2/token', {
|
|
487
|
+
method: 'POST',
|
|
488
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
489
|
+
body: new URLSearchParams({
|
|
490
|
+
client_id: creds.client_id,
|
|
491
|
+
client_secret: creds.client_secret,
|
|
492
|
+
grant_type: 'authorization_code',
|
|
493
|
+
code,
|
|
494
|
+
redirect_uri: redirectUri,
|
|
495
|
+
}).toString(),
|
|
496
|
+
});
|
|
497
|
+
if (!res.ok) {
|
|
498
|
+
const text = await res.text();
|
|
499
|
+
throw new Error(`Discord token exchange failed (${res.status}): ${text}`);
|
|
500
|
+
}
|
|
501
|
+
const data = await res.json();
|
|
502
|
+
return data.access_token;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
async function fetchDiscordUser(accessToken) {
|
|
506
|
+
const res = await fetch('https://discord.com/api/users/@me', {
|
|
507
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
508
|
+
});
|
|
509
|
+
if (!res.ok) {
|
|
510
|
+
const text = await res.text();
|
|
511
|
+
throw new Error(`Discord /users/@me failed (${res.status}): ${text}`);
|
|
512
|
+
}
|
|
513
|
+
return res.json(); // { id, username, ... }
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// Kick a member from the guild when they unlink their Discord account (F7
|
|
517
|
+
// follow-up). Uses the bot token only — no user OAuth needed for a kick.
|
|
518
|
+
// Requires the bot to hold Kick Members. Returns { removed: true } on 204
|
|
519
|
+
// (success) or { removed: false, reason } when the member wasn't in the guild
|
|
520
|
+
// (404) or the bot lacks permission (403) — both non-fatal for an unlink.
|
|
521
|
+
// Throws on unexpected errors so the caller can log; unlink itself must still
|
|
522
|
+
// succeed even if the kick can't.
|
|
523
|
+
async function removeDiscordGuildMember({ guildId, discordUserId }) {
|
|
524
|
+
const botToken = loadDiscordBotToken();
|
|
525
|
+
if (!botToken) throw new Error('Discord bot token not configured');
|
|
526
|
+
if (!guildId) throw new Error('Discord guild id not configured');
|
|
527
|
+
const res = await fetch(`https://discord.com/api/guilds/${guildId}/members/${discordUserId}`, {
|
|
528
|
+
method: 'DELETE',
|
|
529
|
+
headers: { Authorization: `Bot ${botToken}` },
|
|
530
|
+
});
|
|
531
|
+
if (res.status === 204) return { removed: true };
|
|
532
|
+
if (res.status === 404) return { removed: false, reason: 'not_in_guild' };
|
|
533
|
+
if (res.status === 403) return { removed: false, reason: 'missing_permission' };
|
|
534
|
+
const text = await res.text();
|
|
535
|
+
throw new Error(`Discord guild remove failed (${res.status}): ${text}`);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// PURE — a Discord user id is a numeric snowflake (17–20 digits). Reject
|
|
539
|
+
// anything else BEFORE it reaches db.linkDiscord (F1 Hacker advisory).
|
|
540
|
+
function isValidDiscordUserId(id) {
|
|
541
|
+
return typeof id === 'string' && /^\d{17,20}$/.test(id);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// Add the just-linked builder straight into the guild (F7, ADR 0033 — the
|
|
545
|
+
// "link-first auto-join" gate). Uses the user's own OAuth access token (granted
|
|
546
|
+
// via the guilds.join scope) together with the BOT token — Discord's add-member
|
|
547
|
+
// endpoint requires both: the user consent (access_token) and a bot with the
|
|
548
|
+
// CREATE_INSTANT_INVITE permission. `roleIds` are applied at add-time so the
|
|
549
|
+
// member's rank badge is present the instant they arrive (Discord only honors
|
|
550
|
+
// the roles array when CREATING the membership — for an already-present member
|
|
551
|
+
// it 204s and ignores them, which is why the callback also runs syncBuilderRole
|
|
552
|
+
// belt-and-braces). Returns { added } — 201 = newly added, 204 = already a
|
|
553
|
+
// member. Throws on any other status so the caller can log; auto-join is
|
|
554
|
+
// best-effort and must never break the link itself.
|
|
555
|
+
async function addDiscordGuildMember({ guildId, discordUserId, accessToken, roleIds }) {
|
|
556
|
+
const botToken = loadDiscordBotToken();
|
|
557
|
+
if (!botToken) throw new Error('Discord bot token not configured');
|
|
558
|
+
if (!guildId) throw new Error('Discord guild id not configured');
|
|
559
|
+
const res = await fetch(`https://discord.com/api/guilds/${guildId}/members/${discordUserId}`, {
|
|
560
|
+
method: 'PUT',
|
|
561
|
+
headers: { Authorization: `Bot ${botToken}`, 'Content-Type': 'application/json' },
|
|
562
|
+
body: JSON.stringify({ access_token: accessToken, roles: Array.isArray(roleIds) ? roleIds : [] }),
|
|
563
|
+
});
|
|
564
|
+
if (res.status === 201 || res.status === 204) return { added: res.status === 201 };
|
|
565
|
+
const text = await res.text();
|
|
566
|
+
throw new Error(`Discord guild add failed (${res.status}): ${text}`);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// -------------------------------------------------------------------------
|
|
570
|
+
// Express middleware: authenticate by Bearer token (CLI) or gds_session cookie
|
|
571
|
+
// (web). Legacy pms_session cookie still accepted on read so a builder mid-
|
|
572
|
+
// session at the V3.R01 rollout doesn't have to re-auth.
|
|
573
|
+
// -------------------------------------------------------------------------
|
|
574
|
+
|
|
575
|
+
// Collect EVERY candidate auth token on the request, in priority order, with
|
|
576
|
+
// duplicates removed. A browser can legitimately carry MORE THAN ONE
|
|
577
|
+
// gds_session cookie: a host-only one (set on amazonprimea.com before the
|
|
578
|
+
// #726/#734 apex-scoping rollout) AND the current apex-scoped one
|
|
579
|
+
// (.amazonprimea.com). The browser sends both in the Cookie header. We must try
|
|
580
|
+
// them ALL — picking only the first (as the old single-value extractToken did)
|
|
581
|
+
// meant a stale/expired duplicate could shadow the live session and 401 the
|
|
582
|
+
// builder (hit on the Discord link callback for builder 25). Order: Bearer
|
|
583
|
+
// (CLI) first, then every gds_session value in header order, then legacy
|
|
584
|
+
// pms_session — so a valid current session always wins over a stale one.
|
|
585
|
+
// SECURITY (#871): cap the number of distinct candidate tokens we will try per
|
|
586
|
+
// request. resolveSession does ONE DB lookup per candidate, so an attacker who
|
|
587
|
+
// stuffs the Cookie header with dozens of gds_session=<junk> values could force
|
|
588
|
+
// dozens of DB round-trips per request (amplification / DoS) even while
|
|
589
|
+
// unauthenticated. A legitimate browser carries at most a Bearer + a couple of
|
|
590
|
+
// gds_session cookies (host-only + apex during the #734 rollout) + one legacy
|
|
591
|
+
// pms_session, so a small cap never hurts a real client.
|
|
592
|
+
const MAX_AUTH_TOKEN_CANDIDATES = 6;
|
|
593
|
+
|
|
594
|
+
function extractTokens(req) {
|
|
595
|
+
const out = [];
|
|
596
|
+
const push = (t) => {
|
|
597
|
+
if (t && out.length < MAX_AUTH_TOKEN_CANDIDATES && !out.includes(t)) out.push(t);
|
|
598
|
+
};
|
|
599
|
+
const auth = req.headers['authorization'] || '';
|
|
600
|
+
const m = /^Bearer\s+([a-zA-Z0-9]+)$/.exec(auth);
|
|
601
|
+
if (m) push(m[1]);
|
|
602
|
+
// Cookie parsing: minimal, no dep. Prefer new name, fall back to legacy.
|
|
603
|
+
const cookieHeader = req.headers['cookie'] || '';
|
|
604
|
+
const legacy = [];
|
|
605
|
+
for (const part of cookieHeader.split(';')) {
|
|
606
|
+
const idx = part.indexOf('=');
|
|
607
|
+
if (idx < 0) continue;
|
|
608
|
+
const k = part.slice(0, idx).trim();
|
|
609
|
+
const v = part.slice(idx + 1).trim();
|
|
610
|
+
if (k === 'gds_session' && v) push(v);
|
|
611
|
+
else if (k === 'pms_session' && v) legacy.push(v);
|
|
612
|
+
}
|
|
613
|
+
for (const v of legacy) push(v);
|
|
614
|
+
return out;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// Resolve the request to a live session by trying every candidate token until
|
|
618
|
+
// one maps to a non-revoked, unexpired session. Returns null if none do.
|
|
619
|
+
async function resolveSession(req) {
|
|
620
|
+
for (const token of extractTokens(req)) {
|
|
621
|
+
const session = await lookupSession(token);
|
|
622
|
+
if (session) return session;
|
|
623
|
+
}
|
|
624
|
+
return null;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// SECURITY (#919): opt-in marker for the handful of dev-box bootstrap routes a
|
|
628
|
+
// box-scoped session (source='box') is permitted to reach. Mount it BEFORE
|
|
629
|
+
// requireBuilder on those routes; it only flags the request, and requireBuilder's
|
|
630
|
+
// deny-by-default box-scope gate reads the flag. A route WITHOUT this middleware
|
|
631
|
+
// rejects a box session, so this allow-list IS the complete box surface and any
|
|
632
|
+
// new route is denied to boxes automatically (fail closed). No-op for cli/web
|
|
633
|
+
// sessions — requireBuilder only consults the flag when source === 'box'.
|
|
634
|
+
function allowBoxScope(req, _res, next) {
|
|
635
|
+
req.allowBoxScope = true;
|
|
636
|
+
next();
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
async function requireBuilder(req, res, next) {
|
|
640
|
+
const session = await resolveSession(req);
|
|
641
|
+
if (!session) {
|
|
642
|
+
res.fail('unauthenticated', 401);
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
// SECURITY (#871): a deactivated/offboarded builder must not retain API access
|
|
646
|
+
// even if a live (unrevoked) session token survives. deactivateBuilder now
|
|
647
|
+
// revokes sessions on exit, but this is the belt-and-braces hot-path gate that
|
|
648
|
+
// also closes any session minted before that fix (and the never-expiring CLI
|
|
649
|
+
// tokens). Deny ONLY on the known 'inactive' status — never on a missing/
|
|
650
|
+
// unknown value — so an active builder can never be locked out by this check.
|
|
651
|
+
// status is read fresh per request via lookupSession (never cached), so a
|
|
652
|
+
// later reactivation takes effect on the builder's very next request.
|
|
653
|
+
if (session.status === 'inactive') {
|
|
654
|
+
res.fail('builder_inactive', 403);
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
// SECURITY (#919, SR#2): box-scoped sessions are DENY-BY-DEFAULT. The dev-box
|
|
658
|
+
// provisioner (scripts/gds/box.js) bakes a session into cloud-init so the box can
|
|
659
|
+
// bootstrap (#818 — box-source-fetch clones the rank-scoped repo before
|
|
660
|
+
// /builder-setup exists). That token lives ON the box, which is OUTSIDE the ADR
|
|
661
|
+
// 0016 trust boundary, so it must NOT carry the builder's full authority. A
|
|
662
|
+
// source='box' session may reach ONLY the routes that opted in via allowBoxScope
|
|
663
|
+
// (the 4 endpoints the on-box crons call — see routes/box.js); EVERY other route
|
|
664
|
+
// (all of /tasks, /me, the rest of /box …) fails closed here, including any route
|
|
665
|
+
// added later. A builder who wants to do real work FROM the box runs
|
|
666
|
+
// /builder-reauth, minting a normal cli/web session (no box scope) like any laptop.
|
|
667
|
+
if (session.source === 'box' && !req.allowBoxScope) {
|
|
668
|
+
res.fail('box_scope', { status: 403, message: 'This is a box-scoped session; it may only call dev-box bootstrap endpoints.' });
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
// Anthropic-identity pin — REMOVED (task 1039, idea 258). This used to 403
|
|
672
|
+
// `identity_mismatch` when a linked builder's CLI echoed an X-Anthropic-User
|
|
673
|
+
// header that differed from the stored anthropic_user_id (V3.R10 #235, C3).
|
|
674
|
+
// It is gone because:
|
|
675
|
+
// - The drachmae reward values work at a UNIFORM server-side list price
|
|
676
|
+
// (ADR 0054), NOT against each builder's own Anthropic account, so there
|
|
677
|
+
// is no economic reason to bind a session to one Anthropic identity —
|
|
678
|
+
// attribution is simply the GDS session-token holder.
|
|
679
|
+
// - The pin locked a linked builder out of their OWN session the moment
|
|
680
|
+
// they used a second Claude account; multi-account is fine.
|
|
681
|
+
// - It was attestation, not strong auth — an attacker bypasses it by
|
|
682
|
+
// omitting the header — so it added friction with no real security value.
|
|
683
|
+
// anthropic_email / anthropic_user_id are still captured at link time as
|
|
684
|
+
// optional cosmetic attribution; they are no longer an auth boundary, and the
|
|
685
|
+
// X-Anthropic-User header is no longer read here.
|
|
686
|
+
req.gdsSession = session;
|
|
687
|
+
req.builder = {
|
|
688
|
+
id: session.builder_id,
|
|
689
|
+
github_id: session.github_id,
|
|
690
|
+
github_login: session.github_login,
|
|
691
|
+
display_name: session.display_name,
|
|
692
|
+
avatar_url: session.avatar_url,
|
|
693
|
+
total_credits: session.total_credits,
|
|
694
|
+
status: session.status ?? null,
|
|
695
|
+
// rank is the single source of authority (ADR 0016). Read fresh from the
|
|
696
|
+
// DB on every request via lookupSession — never cached, never from the
|
|
697
|
+
// token — so demotion takes effect on the builder's very next request.
|
|
698
|
+
rank: session.rank,
|
|
699
|
+
// system_role identifies a non-human service principal (NULL for every
|
|
700
|
+
// human builder). The BFG cross-builder write gate (requireBfgPrincipal)
|
|
701
|
+
// keys on this EXACT value — never on rank — so an Archon token cannot
|
|
702
|
+
// reach the write path (ADR 0026 §6C). Read fresh per request like rank.
|
|
703
|
+
system_role: session.system_role ?? null,
|
|
704
|
+
};
|
|
705
|
+
next();
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
// Page-level sign-in gate for the authenticated /builders HTML surface and its
|
|
709
|
+
// content assets (#488). Same identity check as requireBuilder — token from the
|
|
710
|
+
// gds_session cookie (or legacy pms_session / Bearer), looked up fresh against
|
|
711
|
+
// builder_sessions — but on failure it REDIRECTS the browser into the web
|
|
712
|
+
// sign-in flow (302 → /api/gds/auth/web/start) instead of returning a JSON 401,
|
|
713
|
+
// because these requests are page navigations and asset loads, not API calls.
|
|
714
|
+
//
|
|
715
|
+
// This is the *actual* access control for the citizen primer + system-maps:
|
|
716
|
+
// server.js mounts it ahead of the static handlers so an unauthenticated
|
|
717
|
+
// request is bounced before the file is ever served (curl-proof, JS-disabled-
|
|
718
|
+
// proof). The client-side link-hiding in builders.js is only cosmetic UX.
|
|
719
|
+
// Build a `?return=<absolute current URL>` for a page-gate sign-in redirect
|
|
720
|
+
// (#734) so a builder bounced into sign-in from the builders subdomain lands
|
|
721
|
+
// back on the page they wanted. The web flow validates it (open-redirect guard)
|
|
722
|
+
// and also falls back to Referer, so this is belt-and-braces for navigations
|
|
723
|
+
// where Referer may be stripped. Returns '' if the host can't be determined.
|
|
724
|
+
function signInWithReturn(req) {
|
|
725
|
+
const base = '/api/gds/auth/web/start';
|
|
726
|
+
const proto = req.headers['x-forwarded-proto'] || (req.secure ? 'https' : 'http');
|
|
727
|
+
const host = req.headers['x-forwarded-host'] || req.headers['host'];
|
|
728
|
+
if (!host) return base;
|
|
729
|
+
return `${base}?return=${encodeURIComponent(`${proto}://${host}${req.originalUrl}`)}`;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
async function requireBuilderPage(req, res, next) {
|
|
733
|
+
try {
|
|
734
|
+
const session = await resolveSession(req);
|
|
735
|
+
if (!session) {
|
|
736
|
+
res.redirect(302, signInWithReturn(req));
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
next();
|
|
740
|
+
} catch (err) {
|
|
741
|
+
// Fail CLOSED: if the session lookup itself errors (e.g. DB blip), send the
|
|
742
|
+
// visitor to sign-in rather than leaking the gated page. The content the
|
|
743
|
+
// gate protects is the priority here, not availability.
|
|
744
|
+
console.error('[gds] requireBuilderPage lookup failed', err);
|
|
745
|
+
res.redirect(302, signInWithReturn(req));
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
// requireArchonPage — page-level Archon gate for the /watch monitoring surface
|
|
750
|
+
// (#726). Same identity resolution as requireBuilderPage, but additionally
|
|
751
|
+
// checks the LIVE rank from the session lookup:
|
|
752
|
+
// - no session → 302 into the web sign-in flow (page navigation)
|
|
753
|
+
// - signed in, not Archon → 302 back to the hall (/builders) — they ARE a
|
|
754
|
+
// citizen, just not the Ruling Hand; bouncing to
|
|
755
|
+
// sign-in would loop, so send them home instead.
|
|
756
|
+
// - Archon → next()
|
|
757
|
+
// This is defense-in-depth: every data endpoint the page calls is ALSO
|
|
758
|
+
// requireRank('archon') server-side, so no cross-builder data can leak even if
|
|
759
|
+
// this gate were bypassed. But it keeps a non-Archon from ever seeing the page
|
|
760
|
+
// shell, honoring "only Archons have access".
|
|
761
|
+
async function requireArchonPage(req, res, next) {
|
|
762
|
+
try {
|
|
763
|
+
const session = await resolveSession(req);
|
|
764
|
+
if (!session) {
|
|
765
|
+
res.redirect(302, signInWithReturn(req));
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
if (session.rank !== 'archon') {
|
|
769
|
+
res.redirect(302, '/builders');
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
next();
|
|
773
|
+
} catch (err) {
|
|
774
|
+
// Fail CLOSED — a lookup error sends the visitor to sign-in rather than
|
|
775
|
+
// leaking the gated page shell.
|
|
776
|
+
console.error('[gds] requireArchonPage lookup failed', err);
|
|
777
|
+
res.redirect(302, signInWithReturn(req));
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
// requireNonXenosPage — page-level gate for the orientation reading rooms (the
|
|
782
|
+
// citizen's primer, the system-maps/diagrams) that #766 closed to newcomers
|
|
783
|
+
// and #770 enforces at the URL. A Xenos's path is action-only — find work,
|
|
784
|
+
// claim, ship three — so these pages are reserved for graduated citizens
|
|
785
|
+
// (Thetes+). Same identity resolution as requireBuilderPage, plus a live-rank
|
|
786
|
+
// check:
|
|
787
|
+
// - no session → 302 into the web sign-in flow (page navigation)
|
|
788
|
+
// - Xenos → 302 to /builders/not-ready — they ARE a citizen, just not
|
|
789
|
+
// ready for the reading rooms; the not-ready page offers the
|
|
790
|
+
// rank ladder + a way back to the hall. Bouncing to sign-in
|
|
791
|
+
// would loop, so send them to the friendly notice.
|
|
792
|
+
// - Thetes+ → next()
|
|
793
|
+
// Defense-in-depth: this is the curl-proof / no-JS-proof gate. The link-hiding
|
|
794
|
+
// in builders.js is only cosmetic.
|
|
795
|
+
async function requireNonXenosPage(req, res, next) {
|
|
796
|
+
try {
|
|
797
|
+
const session = await resolveSession(req);
|
|
798
|
+
if (!session) {
|
|
799
|
+
res.redirect(302, signInWithReturn(req));
|
|
800
|
+
return;
|
|
801
|
+
}
|
|
802
|
+
if (session.rank === 'xenos') {
|
|
803
|
+
res.redirect(302, '/builders/not-ready');
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
806
|
+
next();
|
|
807
|
+
} catch (err) {
|
|
808
|
+
// Fail CLOSED — a lookup error sends the visitor to sign-in rather than
|
|
809
|
+
// leaking the gated page shell.
|
|
810
|
+
console.error('[gds] requireNonXenosPage lookup failed', err);
|
|
811
|
+
res.redirect(302, signInWithReturn(req));
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
// ---------------------------------------------------------------------------
|
|
816
|
+
// Route-gate inventory (V3.R06 #231) — the canonical "who can call what" list.
|
|
817
|
+
// Derived from ADR 0016's fail-closed rule: privileged routes that mutate
|
|
818
|
+
// SHARED plan/scope state default to archon; review/triage routes go to
|
|
819
|
+
// metic+archon (#360); routes scoped to a builder's OWN work/contributions
|
|
820
|
+
// stay open to any authenticated builder (a Xenos additionally limited to
|
|
821
|
+
// xenos_claimable tasks at claim time). Reads + auth/identity are never gated.
|
|
822
|
+
//
|
|
823
|
+
// archon-only (requireRank('archon')) — ratification / scope / infra:
|
|
824
|
+
// POST /tasks create work
|
|
825
|
+
// PATCH /tasks/:id edit work (incl. xenos_claimable)
|
|
826
|
+
// POST /tasks/:id/promote backlog→ready
|
|
827
|
+
// POST/PUT/DELETE /tasks/:id/dependencies reshape the dep graph
|
|
828
|
+
// POST /tasks/:id/confirm grader-bypass override
|
|
829
|
+
// POST /done-when/:id/{satisfy,unsatisfy} version scope
|
|
830
|
+
// PATCH /builders/:id/rank, /builders/:id/status rank/exit (Archon-only)
|
|
831
|
+
//
|
|
832
|
+
// metic+archon (requireRank('metic','archon')) — the approved working rank's
|
|
833
|
+
// review/triage powers, per criterion C2 (#360 / ADR 0018):
|
|
834
|
+
// PATCH /inbox/:id idea triage (promote/discard/merge)
|
|
835
|
+
// POST /blockers/:id/resolve unblocks tasks (blocker review)
|
|
836
|
+
// POST /blockers/:id/link blocker review
|
|
837
|
+
//
|
|
838
|
+
// open to any AUTHENTICATED builder (requireBuilder only) — own work, BUT a
|
|
839
|
+
// Xenos can only CLAIM tasks flagged xenos_claimable (gated in db.claimTask /
|
|
840
|
+
// claimTasksBatch, #360). Contributions:
|
|
841
|
+
// POST /claims, /claims/batch, /claims/:id/resolve claim + ship own work
|
|
842
|
+
// POST /tasks/:id/ship, /tasks/:id/grade own ship flow
|
|
843
|
+
// POST /cost log cost on own claim
|
|
844
|
+
// POST /learnings capture a learning
|
|
845
|
+
// POST /blockers FILE a blocker
|
|
846
|
+
// POST /inbox FILE an idea
|
|
847
|
+
// POST /session/optimize pick own next work
|
|
848
|
+
// POST /sessions upload own session digest (6D.1)
|
|
849
|
+
// POST /auth/logout own session
|
|
850
|
+
//
|
|
851
|
+
// reads / public / auth: GET *, /healthz, /public/*, /auth/device|web/*.
|
|
852
|
+
//
|
|
853
|
+
// Three ranks are LIVE (#360 / ADR 0018): archon, metic, xenos (default).
|
|
854
|
+
// Fail-closed rule stands: a NEW privileged mutating route defaults to
|
|
855
|
+
// requireRank('archon') until a lower rank is explicitly granted. The periodic
|
|
856
|
+
// auditor (#265 / V3.R45) mechanically asserts no privileged mutating route is
|
|
857
|
+
// left without a rank gate.
|
|
858
|
+
// ---------------------------------------------------------------------------
|
|
859
|
+
|
|
860
|
+
// requireRank(...allowed) — authorization middleware (V3.R05 #106; made
|
|
861
|
+
// threshold-based in #591).
|
|
862
|
+
//
|
|
863
|
+
// Runs AFTER requireBuilder, so req.builder.rank is the live DB value. 403s
|
|
864
|
+
// with a structured body if the builder's rank is below the gate. This is the
|
|
865
|
+
// enforcement point of the trust boundary documented in ADR 0016: the server
|
|
866
|
+
// checks a value the builder's machine cannot write.
|
|
867
|
+
//
|
|
868
|
+
// THRESHOLD, not exact-match (#591). The rank ladder (db.RANKS) is hierarchical
|
|
869
|
+
// — lower index = higher authority (olympian … archon … metic … helot). The old
|
|
870
|
+
// gate authorized by exact set membership (`allowedSet.has(actual)`), which
|
|
871
|
+
// silently denied every rank ABOVE the listed one: a builder promoted to a
|
|
872
|
+
// "divine" rank (olympian/hermes/oracle/demigod — documented as above-the-system,
|
|
873
|
+
// strictly above archon) FAILED requireRank('archon') and lost all archon-gated
|
|
874
|
+
// authority instead of inheriting it. We now admit any rank AT-OR-ABOVE the
|
|
875
|
+
// lowest-authority rank named in `allowed`. So requireRank('archon') admits
|
|
876
|
+
// archon and every divine rank above it; requireRank('metic','archon') admits
|
|
877
|
+
// metic, archon, and everything above — and still blocks thetes/xenos. The
|
|
878
|
+
// `required` field in the 403 body keeps reporting the literal allow-list so the
|
|
879
|
+
// route's intent (and existing clients) read unchanged.
|
|
880
|
+
//
|
|
881
|
+
// Fail-closed (matches the posture of xenosClaimAllowed/#510): an actual rank
|
|
882
|
+
// not in RANKS (unknown / typo / legacy / null) is denied, and a misconfigured
|
|
883
|
+
// gate whose allowed ranks are ALL unknown denies everyone.
|
|
884
|
+
//
|
|
885
|
+
// Usage: router.post('/tasks', auth.requireBuilder, auth.requireRank('archon'), handler)
|
|
886
|
+
function rankMeetsThreshold(actual, allowed) {
|
|
887
|
+
const actualIdx = RANKS.indexOf(actual);
|
|
888
|
+
if (actualIdx === -1) return false; // unknown/missing actual rank → fail closed
|
|
889
|
+
// The gate's floor is the LOWEST-authority (largest-index) rank in `allowed`;
|
|
890
|
+
// unknown allowed ranks are ignored so a typo can't silently widen the gate.
|
|
891
|
+
const floorIdx = allowed.reduce((max, r) => {
|
|
892
|
+
const i = RANKS.indexOf(r);
|
|
893
|
+
return i === -1 ? max : Math.max(max, i);
|
|
894
|
+
}, -1);
|
|
895
|
+
if (floorIdx === -1) return false; // no valid allowed rank → fail closed
|
|
896
|
+
return actualIdx <= floorIdx; // at or above the floor
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
function requireRank(...allowed) {
|
|
900
|
+
return function rankGate(req, res, next) {
|
|
901
|
+
const actual = req.builder && req.builder.rank;
|
|
902
|
+
if (!rankMeetsThreshold(actual, allowed)) {
|
|
903
|
+
res.fail('rank_forbidden', 403, { required: [...allowed], actual: actual ?? null });
|
|
904
|
+
return;
|
|
905
|
+
}
|
|
906
|
+
next();
|
|
907
|
+
};
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
// requireBfgPrincipal — the identity gate for the cross-builder memory WRITE
|
|
911
|
+
// path (ADR 0026 §6C / task #446). Runs AFTER requireBuilder. This is
|
|
912
|
+
// DELIBERATELY NOT a rank gate: the trust-boundary exception is bound to ONE
|
|
913
|
+
// service principal, so the gate checks the EXACT identity (system_role='bfg'),
|
|
914
|
+
// which only the BFG principal's `builders` row carries. An Archon token — or
|
|
915
|
+
// any rank-escalated / stolen token — has system_role=NULL and is rejected with
|
|
916
|
+
// 403. This is the enforcement the ADR demands ("gate on the exact principal
|
|
917
|
+
// identity, not on rank... an Archon token must be rejected"), not prose intent.
|
|
918
|
+
// Pure + exported so the gate behavior is unit-tested directly.
|
|
919
|
+
function requireBfgPrincipal(req, res, next) {
|
|
920
|
+
const role = req.builder && req.builder.system_role;
|
|
921
|
+
if (role !== 'bfg') {
|
|
922
|
+
res.fail('not_bfg_principal', 403);
|
|
923
|
+
return;
|
|
924
|
+
}
|
|
925
|
+
next();
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
// requireDiscordBot — the identity gate for the Discord bot service principal
|
|
929
|
+
// (Discord Phase 3, ADR 0033 §1). Like requireBfgPrincipal, this is an IDENTITY
|
|
930
|
+
// gate, NOT a rank gate: it keys on the EXACT system_role='discord_bot', which
|
|
931
|
+
// only the bot principal's `builders` row carries. An Archon token — or any
|
|
932
|
+
// rank-escalated / stolen human token — has system_role=NULL and is rejected
|
|
933
|
+
// (403); so is a DIFFERENT system principal (the BFG). The bot is trusted ONLY
|
|
934
|
+
// to assert a self-linked Discord→builder mapping; every endpoint behind this
|
|
935
|
+
// gate still re-checks the linked builder's LIVE rank before any write, so a
|
|
936
|
+
// Discord reaction can never escalate past what that builder may already do.
|
|
937
|
+
function requireDiscordBot(req, res, next) {
|
|
938
|
+
const role = req.builder && req.builder.system_role;
|
|
939
|
+
if (role !== 'discord_bot') {
|
|
940
|
+
res.fail('not_discord_bot_principal', 403);
|
|
941
|
+
return;
|
|
942
|
+
}
|
|
943
|
+
next();
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
module.exports = {
|
|
947
|
+
authConfigured,
|
|
948
|
+
openEnrollmentEnabled,
|
|
949
|
+
firstTimeAdmissionBlocked,
|
|
950
|
+
firstAdminLogin,
|
|
951
|
+
maybeSeatFirstAdmin,
|
|
952
|
+
startDeviceFlow,
|
|
953
|
+
pollDeviceFlow,
|
|
954
|
+
buildAuthorizeUrl,
|
|
955
|
+
completeWebFlow,
|
|
956
|
+
extractTokens,
|
|
957
|
+
resolveSession,
|
|
958
|
+
allowBoxScope,
|
|
959
|
+
requireBuilder,
|
|
960
|
+
requireBuilderPage,
|
|
961
|
+
requireArchonPage,
|
|
962
|
+
requireNonXenosPage,
|
|
963
|
+
requireRank,
|
|
964
|
+
rankMeetsThreshold,
|
|
965
|
+
requireBfgPrincipal,
|
|
966
|
+
requireDiscordBot,
|
|
967
|
+
// Discord OAuth (account-linking — Phase 3 F2, ADR 0033)
|
|
968
|
+
loadDiscordCreds,
|
|
969
|
+
discordConfigured,
|
|
970
|
+
loadDiscordBotToken,
|
|
971
|
+
discordBotConfigured,
|
|
972
|
+
discordAuthorizeUrl,
|
|
973
|
+
buildDiscordAuthorizeUrl,
|
|
974
|
+
exchangeDiscordCode,
|
|
975
|
+
fetchDiscordUser,
|
|
976
|
+
isValidDiscordUserId,
|
|
977
|
+
addDiscordGuildMember,
|
|
978
|
+
removeDiscordGuildMember,
|
|
979
|
+
};
|