@bongos/core 1.18.3 → 1.18.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.bongos-core.json +1575 -285
- package/.claude/skills/builder-box/SKILL.md +6 -0
- package/.claude/skills/builder-sequence/SKILL.md +97 -0
- package/.claude/skills/builder-ship/SKILL.md +8 -2
- package/.claude/skills/builder-start/SKILL.md +11 -0
- package/.claude/skills/builder-sync/SKILL.md +41 -0
- package/.claude/skills/fix-task/SKILL.md +64 -0
- package/.claude/skills/goal-create/SKILL.md +158 -0
- package/.claude/skills/goal-review/SKILL.md +3 -1
- package/.claude/skills/idea-triage/SKILL.md +14 -8
- package/.claude/skills/merge-mode/SKILL.md +2 -2
- package/.claude/skills/new-project/SKILL.md +1 -0
- package/.claude/skills/planning-session/SKILL.md +45 -18
- package/.env.local.example +4 -3
- package/README.md +4 -2
- package/bin/bongos.js +1 -0
- package/clients/bongos-client/README.md +2 -2
- package/clients/bongos-client/bongos-client.global.js +82 -18
- package/clients/bongos-client/index.cjs +82 -18
- package/clients/bongos-client/index.d.ts +131 -29
- package/clients/bongos-client/index.mjs +82 -18
- package/config/branding.neutral.json +6 -1
- package/config/governance.neutral.json +21 -0
- package/docs/adr/0043-git-ssh-trust-boundary-and-rank-floor-on-permission-paths.md +24 -0
- package/docs/adr/0090-metic-task-authoring.md +1 -1
- package/docs/adr/0144-devbox-rehome-onto-cloudbongos-plane.md +41 -0
- package/docs/adr/0144-federated-single-logout-backchannel.md +42 -0
- package/docs/adr/0145-devbox-app-branding-driven-module.md +44 -0
- package/docs/adr/0145-free-hosted-project-tier-isolation-and-domain-separation.md +62 -0
- package/docs/adr/0146-configurable-reward-mode-cost-plus-only.md +53 -0
- package/docs/adr/0147-standalone-live-docs-regen.md +39 -0
- package/docs/adr/0148-task-scoped-box-source-access.md +48 -0
- package/docs/adr/0149-bongos-dev-local-launcher.md +57 -0
- package/docs/adr/0150-box-first-boot-bringup-vendored-instances.md +68 -0
- package/docs/adr/0151-governance-permissions-as-atom-ranks-as-roles.md +229 -0
- package/docs/adr/0152-landing-gate-served-by-the-core-app.md +92 -0
- package/docs/adr/0152-metic-task-abandonment.md +67 -0
- package/docs/adr/0153-sequential-chain-runner.md +51 -0
- package/docs/adr/0154-goal-owner-criterion-authoring.md +86 -0
- package/docs/adr/README.md +10 -0
- package/docs/api/openapi.json +843 -17
- package/docs/api-reference.md +23 -10
- package/docs/architecture.md +4 -2
- package/docs/branding-contract.md +3 -1
- package/docs/canonical-permissions.md +21 -8
- package/docs/file-map.md +8 -0
- package/docs/onboarding/diagrams/01-task-lifecycle.mmd +2 -2
- package/docs/onboarding/diagrams/02-rank-ladder.mmd +3 -1
- package/docs/onboarding/diagrams/05-versioning.mmd +1 -3
- package/docs/onboarding/diagrams/assertions.json +4 -7
- package/docs/onboarding/drachmae-and-karma.md +3 -3
- package/docs/onboarding/primer.md +17 -16
- package/docs/onboarding/slash-commands.md +124 -0
- package/docs/recipes/builder-box-lifecycle.md +23 -0
- package/docs/recipes/devbox-app-release.md +14 -4
- package/docs/recipes/instance-first-run.md +16 -0
- package/docs/recipes/local-dev.md +81 -0
- package/docs/recipes/ops-gotchas.md +19 -1
- package/docs/recipes/overnight-builder.md +1 -1
- package/docs/recipes/parallel-goal-waves.md +174 -0
- package/docs/recipes/ship-and-regrade.md +2 -2
- package/docs/recipes/standalone-live-docs.md +48 -0
- package/docs/recipes/work-on-a-task-from-your-dev-box.md +108 -0
- package/migrations/003_pms.sql +6 -7
- package/migrations/006_pms_v2.sql +3 -31
- package/migrations/core_193_task_needs_rebase.sql +28 -0
- package/migrations/core_194_confirmed_backlog_clawback.sql +63 -0
- package/migrations/core_195_idea_triage_provenance.sql +57 -0
- package/migrations/core_196_session_reward_topup.sql +69 -0
- package/modules/autonomy/pollers/routine-timers.js +9 -8
- package/modules/builder-settings/wandering-prefs.js +61 -18
- package/modules/dev-box/app/README.md +73 -0
- package/modules/dev-box/app/assets/tray/mac-alertTemplate.png +0 -0
- package/modules/dev-box/app/assets/tray/mac-alertTemplate@2x.png +0 -0
- package/modules/dev-box/app/assets/tray/mac-busyTemplate.png +0 -0
- package/modules/dev-box/app/assets/tray/mac-busyTemplate@2x.png +0 -0
- package/modules/dev-box/app/assets/tray/mac-offTemplate.png +0 -0
- package/modules/dev-box/app/assets/tray/mac-offTemplate@2x.png +0 -0
- package/modules/dev-box/app/assets/tray/mac-onTemplate.png +0 -0
- package/modules/dev-box/app/assets/tray/mac-onTemplate@2x.png +0 -0
- package/modules/dev-box/app/assets/tray/win-alert.png +0 -0
- package/modules/dev-box/app/assets/tray/win-busy.png +0 -0
- package/modules/dev-box/app/assets/tray/win-off.png +0 -0
- package/modules/dev-box/app/assets/tray/win-on.png +0 -0
- package/modules/dev-box/app/build-resources/dmg-background.png +0 -0
- package/modules/dev-box/app/build-resources/dmg-background@2x.png +0 -0
- package/modules/dev-box/app/build-resources/icon.png +0 -0
- package/modules/dev-box/app/electron-builder.tmpl.yml +95 -0
- package/modules/dev-box/app/electron-builder.yml +95 -0
- package/modules/dev-box/app/package-lock.json +5291 -0
- package/modules/dev-box/app/package.json +30 -0
- package/modules/dev-box/app/renderer/app.js +319 -0
- package/modules/dev-box/app/renderer/index.html +45 -0
- package/modules/dev-box/app/renderer/styles.css +246 -0
- package/modules/dev-box/app/renderer/theme.generated.css +16 -0
- package/modules/dev-box/app/scripts/apply-dmg-backdrop.sh +82 -0
- package/modules/dev-box/app/scripts/dmgbuild-settings.py +39 -0
- package/modules/dev-box/app/scripts/gen-brand.js +138 -0
- package/modules/dev-box/app/scripts/gen-dmg-backdrop.py +323 -0
- package/modules/dev-box/app/scripts/gen-icons.js +286 -0
- package/modules/dev-box/app/scripts/vendor-client.js +23 -0
- package/modules/dev-box/app/src/box-controller.js +287 -0
- package/modules/dev-box/app/src/brand.generated.json +13 -0
- package/modules/dev-box/app/src/brand.js +16 -0
- package/modules/dev-box/app/src/config.js +19 -0
- package/modules/dev-box/app/src/gds-client.js +90 -0
- package/modules/dev-box/app/src/main.js +610 -0
- package/modules/dev-box/app/src/pairing.js +57 -0
- package/modules/dev-box/app/src/preload.js +29 -0
- package/modules/dev-box/app/src/session-store.js +58 -0
- package/modules/dev-box/app/src/ssh-setup.js +569 -0
- package/modules/dev-box/app/src/task-picker.js +70 -0
- package/modules/dev-box/app/src/tray.js +99 -0
- package/modules/dev-box/app/src/updater.js +55 -0
- package/modules/dev-box/app/src/vendor/bongos-client.cjs +698 -0
- package/modules/dev-box/app/test/unit.mjs +526 -0
- package/modules/dev-box/box-access.js +121 -4
- package/modules/dev-box/boxes.js +20 -0
- package/modules/dev-box/module.json +1 -1
- package/modules/dev-box/routes/box.js +87 -3
- package/modules/economy/credits.js +116 -25
- package/modules/economy/reward.js +8 -0
- package/modules/economy/streak.js +2 -1
- package/modules/governance/CLAUDE.md +15 -0
- package/modules/governance/catalog.js +419 -0
- package/modules/governance/config.js +174 -0
- package/modules/governance/db.js +125 -0
- package/modules/governance/migrations/governance_001_tables.sql +81 -0
- package/modules/governance/migrations/governance_002_seed_and_backfill.sql +103 -0
- package/modules/governance/migrations/governance_003_task_abandon_metic.sql +28 -0
- package/modules/governance/migrations/governance_004_criterion_create.sql +39 -0
- package/modules/governance/migrations/governance_005_resync_builder_rank_roles.sql +56 -0
- package/modules/governance/module.json +10 -0
- package/modules/governance/protected-surfaces.js +88 -0
- package/modules/governance/protected-surfaces.json +80 -0
- package/modules/governance/rank-roles.js +83 -0
- package/modules/governance/resolver.js +221 -0
- package/modules/governance/routes/governance.js +89 -0
- package/modules/governance/routes/role-sync.js +49 -0
- package/modules/governance/session-scopes.js +127 -0
- package/modules/governance/session-scopes.json +38 -0
- package/modules/hall-ui/public/atlas.css +117 -0
- package/modules/hall-ui/public/atlas.html +3 -114
- package/modules/hall-ui/public/brand-apply.js +34 -6
- package/modules/hall-ui/public/builders.js +187 -219
- package/modules/hall-ui/public/diagrams.html +2 -0
- package/modules/hall-ui/public/dom-utils.js +27 -0
- package/modules/hall-ui/public/drachmae.html +2 -0
- package/modules/hall-ui/public/drachmae.js +54 -1
- package/modules/hall-ui/public/gate.css +98 -0
- package/modules/hall-ui/public/gate.html +3 -95
- package/modules/hall-ui/public/goal-inbox.js +4 -1
- package/modules/hall-ui/public/goals-page.js +626 -114
- package/modules/hall-ui/public/goals.css +79 -1
- package/modules/hall-ui/public/goals.html +38 -11
- package/modules/hall-ui/public/hall-widgets.js +62 -52
- package/modules/hall-ui/public/harbor.css +123 -0
- package/modules/hall-ui/public/harbor.html +3 -120
- package/modules/hall-ui/public/index.html +11 -4
- package/modules/hall-ui/public/instances-panel.js +4 -1
- package/modules/hall-ui/public/modules.css +63 -0
- package/modules/hall-ui/public/modules.html +3 -60
- package/modules/hall-ui/public/new-project-wizard.js +7 -3
- package/modules/hall-ui/public/not-ready.html +2 -0
- package/modules/hall-ui/public/pair.css +73 -0
- package/modules/hall-ui/public/pair.html +3 -70
- package/modules/hall-ui/public/palette.js +302 -0
- package/modules/hall-ui/public/primer.css +13 -0
- package/modules/hall-ui/public/primer.html +2 -0
- package/modules/hall-ui/public/profile.html +2 -0
- package/modules/hall-ui/public/ranks.html +2 -0
- package/modules/hall-ui/public/roadmap.css +179 -0
- package/modules/hall-ui/public/roadmap.html +9 -172
- package/modules/hall-ui/public/roadmap.js +329 -316
- package/modules/hall-ui/public/sessions.css +16 -0
- package/modules/hall-ui/public/sessions.html +3 -13
- package/modules/hall-ui/public/settings.html +2 -0
- package/modules/hall-ui/public/settings.js +4 -4
- package/modules/hall-ui/public/shell.js +143 -18
- package/modules/hall-ui/public/style.css +231 -39
- package/modules/hall-ui/public/task-detail.js +347 -26
- package/modules/hall-ui/public/task.html +6 -1
- package/modules/hall-ui/public/watch.html +2 -0
- package/modules/hall-ui/public/work.html +8 -6
- package/modules/hall-ui/public/work.js +1 -1
- package/modules/ideas/inbox.js +61 -11
- package/modules/ideas/routes/inbox.js +23 -0
- package/modules/lifecycle/claim-eligibility.js +72 -0
- package/modules/lifecycle/db.js +519 -139
- package/modules/lifecycle/done-when.js +18 -3
- package/modules/lifecycle/github-push.js +19 -2
- package/modules/lifecycle/goal-advisory.js +114 -0
- package/modules/lifecycle/goal-authz.js +62 -0
- package/modules/lifecycle/lifecycle.js +9 -0
- package/modules/lifecycle/publish-reconciler.js +109 -6
- package/modules/lifecycle/routes/claims.js +68 -23
- package/modules/lifecycle/routes/done-when.js +110 -15
- package/modules/lifecycle/routes/goals.js +8 -38
- package/modules/lifecycle/routes/tasks.js +38 -6
- package/modules/lifecycle/ship-card.js +6 -0
- package/modules/onboarding/onboarding-state.js +1 -1
- package/modules/platform-identity/CLAUDE.md +15 -0
- package/modules/platform-identity/hub-device.js +147 -0
- package/modules/platform-identity/hub-keys.js +151 -0
- package/modules/platform-identity/migrations/platform_identity_001_tables.sql +114 -0
- package/modules/platform-identity/migrations/platform_identity_002_client_origin_index.sql +18 -0
- package/modules/platform-identity/migrations/platform_identity_003_project_memberships.sql +42 -0
- package/modules/platform-identity/migrations/platform_identity_004_projects.sql +44 -0
- package/modules/platform-identity/migrations/platform_identity_005_builder_activity.sql +42 -0
- package/modules/platform-identity/module.json +18 -0
- package/modules/platform-identity/platform-identity.js +531 -0
- package/modules/platform-identity/pollers/code-sweep.js +43 -0
- package/modules/platform-identity/routes/my-projects.js +140 -0
- package/modules/platform-identity/routes/projects.js +107 -0
- package/modules/platform-identity/routes/scouting.js +35 -0
- package/modules/platform-identity/routes/sso.js +402 -0
- package/modules/platform-identity/tests/platform-identity.mjs +388 -0
- package/modules/platform-identity/ui/.gitkeep +0 -0
- package/modules/security/routes/reports.js +10 -0
- package/modules/sessions/db.js +166 -21
- package/modules/sessions/module.json +1 -1
- package/modules/sessions/pollers/session-reward-reconcile.js +78 -0
- package/modules/sessions/routes/sessions.js +33 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/public-landing/assets/anim/idle/frame_00.webp +0 -0
- package/public-landing/assets/anim/idle/frame_01.webp +0 -0
- package/public-landing/assets/anim/idle/frame_02.webp +0 -0
- package/public-landing/assets/anim/idle/frame_03.webp +0 -0
- package/public-landing/assets/anim/idle/frame_04.webp +0 -0
- package/public-landing/assets/anim/idle/frame_05.webp +0 -0
- package/public-landing/assets/anim/idle/frame_06.webp +0 -0
- package/public-landing/assets/anim/idle/frame_07.webp +0 -0
- package/public-landing/assets/anim/idle/frame_08.webp +0 -0
- package/public-landing/assets/anim/idle/frame_09.webp +0 -0
- package/public-landing/assets/anim/idle/frame_10.webp +0 -0
- package/public-landing/assets/anim/idle/frame_11.webp +0 -0
- package/public-landing/assets/anim/idle/frame_12.webp +0 -0
- package/public-landing/assets/anim/idle/frame_13.webp +0 -0
- package/public-landing/assets/anim/idle/frame_14.webp +0 -0
- package/public-landing/assets/anim/idle/frame_15.webp +0 -0
- package/public-landing/assets/anim/idle/frame_16.webp +0 -0
- package/public-landing/assets/anim/idle/frame_17.webp +0 -0
- package/public-landing/assets/anim/idle/frame_18.webp +0 -0
- package/public-landing/assets/anim/idle/frame_19.webp +0 -0
- package/public-landing/assets/anim/idle/frame_20.webp +0 -0
- package/public-landing/assets/anim/idle/frame_21.webp +0 -0
- package/public-landing/assets/anim/idle/frame_22.webp +0 -0
- package/public-landing/assets/anim/idle/frame_23.webp +0 -0
- package/public-landing/assets/anim/idle/frame_24.webp +0 -0
- package/public-landing/assets/anim/idle/frame_25.webp +0 -0
- package/public-landing/assets/anim/idle/frame_26.webp +0 -0
- package/public-landing/assets/anim/idle/frame_27.webp +0 -0
- package/public-landing/assets/anim/idle/frame_28.webp +0 -0
- package/public-landing/assets/anim/idle/frame_29.webp +0 -0
- package/public-landing/assets/anim/idle/frame_30.webp +0 -0
- package/public-landing/assets/anim/idle/frame_31.webp +0 -0
- package/public-landing/assets/anim/idle/frame_32.webp +0 -0
- package/public-landing/assets/anim/idle/frame_33.webp +0 -0
- package/public-landing/assets/anim/idle/frame_34.webp +0 -0
- package/public-landing/assets/anim/idle/frame_35.webp +0 -0
- package/public-landing/assets/anim/idle/frame_36.webp +0 -0
- package/public-landing/assets/anim/idle/frame_37.webp +0 -0
- package/public-landing/assets/anim/idle/frame_38.webp +0 -0
- package/public-landing/assets/anim/idle/frame_39.webp +0 -0
- package/public-landing/assets/anim/idle/frame_40.webp +0 -0
- package/public-landing/assets/anim/idle/frame_41.webp +0 -0
- package/public-landing/assets/anim/idle/frame_42.webp +0 -0
- package/public-landing/assets/anim/idle/frame_43.webp +0 -0
- package/public-landing/create.html +51 -58
- package/public-landing/gate/README.md +137 -0
- package/public-landing/gate/dispmaps/bar.png +0 -0
- package/public-landing/gate/dispmaps/flat.png +0 -0
- package/public-landing/gate/dispmaps/key.png +0 -0
- package/public-landing/gate/index.html +587 -0
- package/public-landing/gate/previews/cloud-bongos.jpg +0 -0
- package/public-landing/gate/previews/off-the-boats.jpg +0 -0
- package/public-landing/gate/space.css +799 -0
- package/public-landing/gate/space.js +1826 -0
- package/public-landing/gate/sprites/enter.png +0 -0
- package/public-landing/gate/sprites/search.png +0 -0
- package/public-landing/gate/sprites/terminal-blank.png +0 -0
- package/public-landing/gate/sprites/tile.png +0 -0
- package/public-landing/world/assets/clouds/cloud-island-large.png +0 -0
- package/public-landing/world/assets/clouds/cloud-puff.png +0 -0
- package/public-landing/world/assets/creatures/creature-whale-cargo.png +0 -0
- package/public-landing/world/assets/creatures/creature-whale-glide.png +0 -0
- package/public-landing/world/assets/plates/plate-clouds-near.png +0 -0
- package/public-landing/world/assets/plates/scene-establishing.png +0 -0
- package/scripts/gds/abandon.js +6 -4
- package/scripts/gds/api.js +10 -5
- package/scripts/gds/api.ps1 +26 -0
- package/scripts/gds/api.sh +6 -0
- package/scripts/gds/audit-ships.js +7 -1
- package/scripts/gds/backfill-sessions.js +78 -18
- package/scripts/gds/backup.js +23 -1
- package/scripts/gds/box-connect-lib.js +124 -31
- package/scripts/gds/box-sync.js +66 -0
- package/scripts/gds/box.js +225 -53
- package/scripts/gds/cf-tunnel.js +145 -6
- package/scripts/gds/claim.js +34 -3
- package/scripts/gds/cli-lib.js +161 -27
- package/scripts/gds/code.js +6 -2
- package/scripts/gds/deploy-divergence.js +209 -0
- package/scripts/gds/dev-lib.js +309 -0
- package/scripts/gds/dev.js +322 -0
- package/scripts/gds/doctor.js +90 -61
- package/scripts/gds/fetch-art-key.js +1 -1
- package/scripts/gds/fetch-sound-prefs.js +2 -2
- package/scripts/gds/fitness.js +228 -12
- package/scripts/gds/gds-db-backup.service +16 -1
- package/scripts/gds/gds-db-offsite.service +9 -1
- package/scripts/gds/gen-api-client.js +1 -1
- package/scripts/gds/gen-api-docs.js +36 -14
- package/scripts/gds/gen-diagrams.js +45 -28
- package/scripts/gds/gen-file-map.js +2 -1
- package/scripts/gds/gen-freshness.js +32 -0
- package/scripts/gds/gen-repo-map.js +3 -2
- package/scripts/gds/gen-session-index.js +14 -5
- package/scripts/gds/hooks/pre-push.js +2 -2
- package/scripts/gds/init.js +60 -3
- package/scripts/gds/install-git-hooks.js +1 -1
- package/scripts/gds/key-card.js +15 -6
- package/scripts/gds/key-status.js +1 -1
- package/scripts/gds/linkify-refs.js +35 -9
- package/scripts/gds/login.js +16 -1
- package/scripts/gds/paste-token.js +7 -1
- package/scripts/gds/preflight.js +126 -0
- package/scripts/gds/provision.js +123 -12
- package/scripts/gds/publish-manifest.js +7 -1
- package/scripts/gds/reauth.js +7 -3
- package/scripts/gds/regen-instance-docs.js +47 -0
- package/scripts/gds/run-unit-tests.js +4 -2
- package/scripts/gds/seed-bongos-v1-seo-tasks.js +104 -0
- package/scripts/gds/seed-bv1-profiles-tasks.js +126 -0
- package/scripts/gds/sequence.js +425 -0
- package/scripts/gds/session-cost.js +10 -1
- package/scripts/gds/setup.js +31 -5
- package/scripts/gds/shell.js +5 -3
- package/scripts/gds/ship.js +319 -29
- package/scripts/gds/smoke-gds.js +139 -8
- package/scripts/gds/start.js +171 -22
- package/scripts/gds/status.js +1 -1
- package/scripts/gds/triage.js +10 -9
- package/scripts/gds/upgrade.js +124 -2
- package/scripts/status-mirror-sync.js +114 -84
- package/src/bongos/api-errors.js +1 -0
- package/src/bongos/auth.js +447 -102
- package/src/bongos/db-kernel.js +27 -0
- package/src/bongos/db.js +39 -0
- package/src/bongos/module-scope-map.js +27 -1
- package/src/bongos/permission-path-check.js +136 -102
- package/src/bongos/repo-info.js +47 -11
- package/src/bongos/route-rank-check.js +429 -46
- package/src/bongos/routes/_helpers.js +12 -1
- package/src/bongos/routes/auth.js +219 -2
- package/src/bongos/routes/backup.js +58 -2
- package/src/bongos/routes/me.js +35 -1
- package/src/bongos/serve-internal.js +84 -15
- package/src/branding.js +32 -0
- package/src/instance-config.js +30 -0
- package/src/module-api.js +112 -1
- package/src/platform-server.js +86 -9
- package/tests/abandon_task.mjs +59 -3
- package/tests/api_cli_flags.mjs +53 -0
- package/tests/auth_page_gate.mjs +13 -0
- package/tests/backfill_sessions.mjs +127 -1
- package/tests/backup_cli.mjs +54 -0
- package/tests/bongos_box_connect.mjs +77 -0
- package/tests/box_access.mjs +130 -1
- package/tests/box_credential.mjs +125 -0
- package/tests/box_host_keys.mjs +24 -0
- package/tests/box_infra_resolve.mjs +149 -0
- package/tests/box_scope_session.mjs +6 -2
- package/tests/box_sync.mjs +55 -0
- package/tests/box_task_scope.mjs +99 -0
- package/tests/branding.mjs +12 -0
- package/tests/cf_ruleset_preflight.mjs +142 -0
- package/tests/cf_tunnel.mjs +37 -0
- package/tests/claim_gate_rebase.mjs +98 -0
- package/tests/clawback_confirmed_backlog.mjs +103 -0
- package/tests/cli_lib_formatters.mjs +20 -0
- package/tests/confirm_attribution.mjs +30 -46
- package/tests/credit_grant.mjs +198 -10
- package/tests/criterion_authoring_gate.mjs +272 -0
- package/tests/criterion_progress.mjs +33 -1
- package/tests/dev_lib.mjs +253 -0
- package/tests/devbox_artifact_gate.mjs +75 -0
- package/tests/devbox_dmg_backdrop.mjs +137 -0
- package/tests/devbox_icons.mjs +304 -0
- package/tests/devbox_installer_brand.mjs +232 -0
- package/tests/devbox_key_register.mjs +234 -0
- package/tests/docs_openapi_route.mjs +110 -0
- package/tests/doctor_hooks.mjs +141 -0
- package/tests/extract_json.mjs +140 -0
- package/tests/fitness.mjs +89 -0
- package/tests/gen_freshness.mjs +96 -0
- package/tests/goal_advisory.mjs +251 -0
- package/tests/goal_criterion_confirm_ui.mjs +269 -0
- package/tests/goal_routes.mjs +59 -4
- package/tests/goal_tier.mjs +12 -3
- package/tests/governance_catalog.mjs +129 -0
- package/tests/governance_claim_eligibility.mjs +262 -0
- package/tests/governance_config.mjs +187 -0
- package/tests/governance_ownership_scope.mjs +410 -0
- package/tests/governance_page_gates.mjs +324 -0
- package/tests/governance_parity.mjs +336 -0
- package/tests/governance_principals.mjs +232 -0
- package/tests/governance_protected_surfaces.mjs +208 -0
- package/tests/governance_require_permission.mjs +223 -0
- package/tests/governance_resolver.mjs +84 -0
- package/tests/governance_role_sync.mjs +231 -0
- package/tests/governance_seed.mjs +127 -0
- package/tests/governance_session_scope.mjs +434 -0
- package/tests/governance_vanilla_boot.mjs +203 -0
- package/tests/grade_attribution.mjs +7 -7
- package/tests/hall_audit.mjs +223 -0
- package/tests/hall_nav.mjs +191 -0
- package/tests/hall_palette.mjs +206 -0
- package/tests/hall_signout.mjs +124 -0
- package/tests/hook_user_agents.mjs +10 -0
- package/tests/idea_triage.mjs +162 -0
- package/tests/idp_device_exchange.mjs +93 -0
- package/tests/init.mjs +51 -0
- package/tests/instance_config.mjs +65 -0
- package/tests/instance_mismatch_guard.mjs +23 -6
- package/tests/instance_release_config.mjs +104 -0
- package/tests/linkify_refs.mjs +15 -2
- package/tests/module_contributions.mjs +55 -0
- package/tests/module_loader.mjs +10 -5
- package/tests/module_scope_active_claims.mjs +94 -0
- package/tests/needs_rebase.mjs +92 -0
- package/tests/one_active_claim_per_session.mjs +76 -0
- package/tests/override_attribution.mjs +10 -8
- package/tests/override_request_ownership.mjs +8 -4
- package/tests/parse_cookie_decode.mjs +42 -0
- package/tests/pay_on_land.mjs +129 -0
- package/tests/platform_boot.mjs +100 -1
- package/tests/post_login_landing.mjs +42 -0
- package/tests/preflight_checks.mjs +72 -0
- package/tests/provision.mjs +132 -7
- package/tests/publish_branch_route.mjs +53 -0
- package/tests/publish_reconciler.mjs +187 -2
- package/tests/repo_info_binding.mjs +248 -0
- package/tests/reward_gate.mjs +11 -2
- package/tests/reward_mode_cost_plus_only.mjs +116 -0
- package/tests/reward_mode_surfacing.mjs +168 -0
- package/tests/route_permission_check.mjs +301 -0
- package/tests/route_rank_check.mjs +254 -12
- package/tests/route_validation.mjs +30 -0
- package/tests/sequence.mjs +267 -0
- package/tests/session_digest.mjs +44 -0
- package/tests/session_index.mjs +18 -6
- package/tests/session_records.mjs +39 -1
- package/tests/session_reward_on_land.mjs +381 -0
- package/tests/session_reward_reconcile.mjs +229 -0
- package/tests/ship_api_client_pathspec.mjs +100 -0
- package/tests/ship_base_flag.mjs +93 -0
- package/tests/ship_ci_deploy.mjs +80 -3
- package/tests/ship_upstream_deletion.mjs +57 -0
- package/tests/smoke_target.mjs +233 -0
- package/tests/start_env_guard.mjs +87 -0
- package/tests/start_rebase_warning.mjs +103 -0
- package/tests/status_mirror_sync.mjs +122 -0
- package/tests/stop_refcheck.mjs +19 -1
- package/tests/task_goal_link.mjs +15 -6
- package/tests/upgrade.mjs +139 -0
- package/tests/web_admission_status.mjs +162 -0
- package/tests/write_validation_gaps.mjs +110 -0
|
@@ -80,6 +80,78 @@ function renderAuthErrorPage(heading, detail, opts = {}) {
|
|
|
80
80
|
</body></html>`;
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
// renderAccessPendingPage — the calm "hang tight" page shown instead of the red
|
|
84
|
+
// dead-end when admission is plausibly imminent (task 1002097): the configured
|
|
85
|
+
// firstAdmin during the bootstrap window, or a login with a pending request.
|
|
86
|
+
//
|
|
87
|
+
// It polls GET /auth/web/admission-status and, the moment the answer flips to
|
|
88
|
+
// admitted, sends the visitor back through /auth/web/start for a FRESH sign-in.
|
|
89
|
+
// A fresh start is required, not optional: the OAuth code that brought them
|
|
90
|
+
// here is already spent, so re-running the callback would fail on state
|
|
91
|
+
// mismatch. The owner chose auto-walk-in, hence no button to click.
|
|
92
|
+
//
|
|
93
|
+
// Branding resolves defensively, exactly as renderAuthErrorPage does — a
|
|
94
|
+
// branding hiccup must degrade to neutral copy, never crash the page.
|
|
95
|
+
function renderAccessPendingPage(login) {
|
|
96
|
+
const worldName = (() => { try { return branding().identity?.worldName || 'The city'; } catch { return 'The city'; } })();
|
|
97
|
+
const epigraph = (() => { try { return branding().copy?.authError?.epigraph || '— access —'; } catch { return '— access —'; } })();
|
|
98
|
+
// The probe takes the login as a query param; everything interpolated into
|
|
99
|
+
// the page is escaped, and the JS reads it from a data- attribute rather than
|
|
100
|
+
// being built by string concatenation into a script body.
|
|
101
|
+
const safeLogin = escapeHtml(String(login || ''));
|
|
102
|
+
return `<!doctype html>
|
|
103
|
+
<html lang="en"><head>
|
|
104
|
+
<meta charset="utf-8">
|
|
105
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
106
|
+
<title>${escapeHtml(worldName)} — confirming access</title>
|
|
107
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
108
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
109
|
+
<link href="https://fonts.googleapis.com/css2?family=Cinzel:wght@500;700&family=EB+Garamond:ital,wght@0,400;0,500&display=swap" rel="stylesheet">
|
|
110
|
+
<link rel="stylesheet" href="/builders/style.css">
|
|
111
|
+
</head><body>
|
|
112
|
+
<header class="hall">
|
|
113
|
+
<p class="hall__epigraph">${escapeHtml(epigraph)}</p>
|
|
114
|
+
<h1>Confirming your access</h1>
|
|
115
|
+
<p class="hall__sub">Hang tight — this usually takes a few seconds.</p>
|
|
116
|
+
</header>
|
|
117
|
+
<main>
|
|
118
|
+
<section class="scroll">
|
|
119
|
+
<div class="landing" id="pending" data-login="${safeLogin}">
|
|
120
|
+
<p>We're checking your access now. You'll be signed in automatically as soon as it clears — no need to refresh.</p>
|
|
121
|
+
<p class="landing__note" id="pending-note">Checking…</p>
|
|
122
|
+
</div>
|
|
123
|
+
</section>
|
|
124
|
+
</main>
|
|
125
|
+
<script>
|
|
126
|
+
(function () {
|
|
127
|
+
var el = document.getElementById('pending');
|
|
128
|
+
var note = document.getElementById('pending-note');
|
|
129
|
+
var login = el ? el.getAttribute('data-login') : '';
|
|
130
|
+
var EVERY_MS = 3000;
|
|
131
|
+
var GIVE_UP_MS = 5 * 60 * 1000;
|
|
132
|
+
var started = Date.now();
|
|
133
|
+
|
|
134
|
+
function land() { window.location.href = '/api/gds/auth/web/start'; }
|
|
135
|
+
|
|
136
|
+
function tick() {
|
|
137
|
+
if (Date.now() - started > GIVE_UP_MS) {
|
|
138
|
+
note.textContent = 'Still waiting on approval. You can leave this page open, or try signing in again later.';
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
fetch('/api/gds/auth/web/admission-status?login=' + encodeURIComponent(login), { credentials: 'omit' })
|
|
142
|
+
.then(function (r) { return r.ok ? r.json() : null; })
|
|
143
|
+
.then(function (j) {
|
|
144
|
+
if (j && j.admitted) { land(); return; }
|
|
145
|
+
setTimeout(tick, EVERY_MS);
|
|
146
|
+
})
|
|
147
|
+
.catch(function () { setTimeout(tick, EVERY_MS); });
|
|
148
|
+
}
|
|
149
|
+
setTimeout(tick, EVERY_MS);
|
|
150
|
+
})();
|
|
151
|
+
</script>
|
|
152
|
+
</body></html>`;
|
|
153
|
+
}
|
|
154
|
+
|
|
83
155
|
// Cookie names. The web flow writes the new `gds_session` (+ `gds_oauth_state`)
|
|
84
156
|
// names; both flows accept either the new or the legacy `pms_session` /
|
|
85
157
|
// `pms_oauth_state` cookies on read so a builder mid-flow doesn't have to
|
|
@@ -189,6 +261,27 @@ function configuredOAuthOrigin() {
|
|
|
189
261
|
}
|
|
190
262
|
}
|
|
191
263
|
|
|
264
|
+
// The post-sign-in landing used when no explicit ?return= / same-apex Referer
|
|
265
|
+
// was stashed for this sign-in. Configurable per instance via
|
|
266
|
+
// branding.landing.postLogin (env override <PREFIX>_POST_LOGIN_LANDING) so a
|
|
267
|
+
// platform-home instance can land visitors on its OWN front door —
|
|
268
|
+
// cloudbongos.com sets it to /gate/ (the orbs) so a visitor who signs in from
|
|
269
|
+
// the orbs returns to the orbs, never auto-redirected into the builders' hall
|
|
270
|
+
// (task 1002240). Every other instance keeps the portable default '/builders':
|
|
271
|
+
// the hall always exists, a /gate/ orbs site does not. The configured value is
|
|
272
|
+
// run through the SAME open-redirect allowlist as ?return= (safeReturnTo), so a
|
|
273
|
+
// misconfigured landing can never become an open redirect; anything unsafe or
|
|
274
|
+
// absent falls back to '/builders'. Takes an optional resolved pack so the pure
|
|
275
|
+
// path is unit-testable without disk I/O (mirrors clientBranding()).
|
|
276
|
+
function postLoginLanding(brand) {
|
|
277
|
+
try {
|
|
278
|
+
const b = brand || branding();
|
|
279
|
+
return safeReturnTo(b.landing?.postLogin) || '/builders';
|
|
280
|
+
} catch {
|
|
281
|
+
return '/builders';
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
192
285
|
// The OAuth redirect_uri must be a FIXED, registered origin — never derived from
|
|
193
286
|
// the request's x-forwarded-host (spoofable; F2 Hacker finding). Configured via
|
|
194
287
|
// env, falling back to the configured canonical public origin, so an injected host
|
|
@@ -348,6 +441,49 @@ module.exports = function buildAuthRouter() {
|
|
|
348
441
|
: auth.buildAuthorizeUrl(state, redirectUri, { repos: wantsRepos }));
|
|
349
442
|
});
|
|
350
443
|
|
|
444
|
+
// GET /auth/web/admission-status?login=X — the probe renderAccessPendingPage
|
|
445
|
+
// polls (task 1002097). Answers ONE question: would this login be admitted
|
|
446
|
+
// right now? It mirrors the real gate via auth.webAdmissionStatus so the page
|
|
447
|
+
// can never invite someone in the gate would refuse.
|
|
448
|
+
//
|
|
449
|
+
// rank: public — it must be reachable by a visitor with no session (that is
|
|
450
|
+
// the entire situation it exists for). It grants nothing: admission still
|
|
451
|
+
// runs through the normal sign-in, and this only reads state.
|
|
452
|
+
//
|
|
453
|
+
// WHAT IT LEAKS, stated plainly (task 1002430 — an earlier version of this
|
|
454
|
+
// comment claimed the opposite and was wrong). Returning only a boolean does
|
|
455
|
+
// NOT make it non-enumerable: with open enrollment off, and for any login
|
|
456
|
+
// that is not the configured firstAdmin, `admitted:true` means that login is
|
|
457
|
+
// a member or has been invited. That reveals account existence to an
|
|
458
|
+
// unauthenticated caller. It is also NOT throttled — rateLimitMiddleware
|
|
459
|
+
// returns early for non-write methods, so GETs bypass it entirely; the page's
|
|
460
|
+
// 3s poll bounds the honest client, not an attacker.
|
|
461
|
+
//
|
|
462
|
+
// Why it is nonetheless not a new exposure: the onboarding module's
|
|
463
|
+
// GET /access-requests/status?github_login=X is ALREADY public and returns
|
|
464
|
+
// the strictly richer member|invited|pending|dismissed|none plus its own
|
|
465
|
+
// `admitted`. Anything derivable here is already derivable there, so this
|
|
466
|
+
// adds no net-new surface — and hardening only this route would accomplish
|
|
467
|
+
// nothing while that one stays open. Whether unauthenticated login-existence
|
|
468
|
+
// probing is acceptable at all is a real question, but it belongs to both
|
|
469
|
+
// routes together, not to a comment here.
|
|
470
|
+
//
|
|
471
|
+
// That endpoint is also why this one exists: it models neither the firstAdmin
|
|
472
|
+
// bypass nor open enrollment, so it reports the OWNER as 'none' forever —
|
|
473
|
+
// the exact case this page has to resolve.
|
|
474
|
+
router.get('/auth/web/admission-status', async (req, res) => {
|
|
475
|
+
const login = typeof req.query.login === 'string' ? req.query.login.slice(0, 100) : '';
|
|
476
|
+
if (!login) return res.json({ admitted: false });
|
|
477
|
+
try {
|
|
478
|
+
const { admitted } = await auth.webAdmissionStatus(login);
|
|
479
|
+
return res.json({ admitted: !!admitted });
|
|
480
|
+
} catch (err) {
|
|
481
|
+
// Fail closed and quietly — the page just keeps polling.
|
|
482
|
+
console.error('[gds] auth/web/admission-status', err);
|
|
483
|
+
return res.json({ admitted: false });
|
|
484
|
+
}
|
|
485
|
+
});
|
|
486
|
+
|
|
351
487
|
// rank: public — OAuth callback; the state cookie + code are the bearer.
|
|
352
488
|
router.get('/auth/web/callback', async (req, res) => {
|
|
353
489
|
if (!auth.authConfigured() && !auth.idpConfigured()) {
|
|
@@ -415,15 +551,37 @@ module.exports = function buildAuthRouter() {
|
|
|
415
551
|
res.clearCookie(REPOS_COOKIE, domainOpts);
|
|
416
552
|
}
|
|
417
553
|
// Land back where sign-in began if a validated return target was stashed
|
|
418
|
-
// (e.g. the builders subdomain bounced through here); else the
|
|
554
|
+
// (e.g. the builders subdomain bounced through here); else the instance's
|
|
555
|
+
// configured post-login landing (default '/builders'; cloudbongos.com →
|
|
556
|
+
// /gate/, the orbs — task 1002240).
|
|
419
557
|
const returnTo = safeReturnTo(parseCookie(req, RETURN_COOKIE));
|
|
420
558
|
res.clearCookie(RETURN_COOKIE, domainOpts);
|
|
421
|
-
res.redirect(returnTo ||
|
|
559
|
+
res.redirect(returnTo || postLoginLanding());
|
|
422
560
|
} catch (err) {
|
|
423
561
|
// Approval gate (2026-05-30): a first-time GitHub account that hasn't been
|
|
424
562
|
// approved by an Archon can't enter. Send them to the hall to request
|
|
425
563
|
// access rather than dead-ending on a generic error.
|
|
426
564
|
if (err && err.code === 'NOT_APPROVED') {
|
|
565
|
+
// task 1002097: a blocked sign-in is not always a rejection. On a fresh
|
|
566
|
+
// or just-restarted instance the configured firstAdmin lands here for a
|
|
567
|
+
// few seconds — before FIRST_ADMIN is loaded / the bootstrap finishes —
|
|
568
|
+
// and so does anyone whose access_request an Archon is about to mark
|
|
569
|
+
// invited. The red dead-end looked broken, and could not be retried:
|
|
570
|
+
// the OAuth code is single-use, so a reload throws "State mismatch"
|
|
571
|
+
// instead. For those two, show a calm waiting page that polls and walks
|
|
572
|
+
// them in by itself. Genuine strangers still get the request-access
|
|
573
|
+
// page below.
|
|
574
|
+
let expectedSoon = false;
|
|
575
|
+
try {
|
|
576
|
+
({ expectedSoon } = await auth.webAdmissionStatus(err.login));
|
|
577
|
+
} catch (probeErr) {
|
|
578
|
+
// Never let the classifier turn a clean 403 into a 500 — fall through
|
|
579
|
+
// to the existing page, which is the safe answer for a stranger.
|
|
580
|
+
console.error('[gds] web/callback admission classify', probeErr);
|
|
581
|
+
}
|
|
582
|
+
if (expectedSoon) {
|
|
583
|
+
return res.status(202).type('text/html').send(renderAccessPendingPage(err.login));
|
|
584
|
+
}
|
|
427
585
|
return res.status(403).type('text/html').send(renderAuthErrorPage(
|
|
428
586
|
'This account is not approved yet',
|
|
429
587
|
'Signing in with GitHub is only for builders an Archon has already approved. If you are new, request access at the builders’ hall — an Archon will approve you, then this sign-in will let you in. If you were just approved, the Archon may still need to mark you invited; give it a moment and try again.',
|
|
@@ -617,9 +775,67 @@ module.exports = function buildAuthRouter() {
|
|
|
617
775
|
res.clearCookie(SESSION_COOKIE, domainOpts);
|
|
618
776
|
res.clearCookie(SESSION_COOKIE_LEGACY, domainOpts);
|
|
619
777
|
}
|
|
778
|
+
// Federated single-logout (ADR 0144, task 1002238): announce the logout so the
|
|
779
|
+
// hub (platform-identity, enabled only on cloudbongos.com) can fan a signed
|
|
780
|
+
// back-channel logout out to every federated project this builder entered. A
|
|
781
|
+
// fire-and-forget seam EMIT — zero listeners on a non-hub instance, so it's a
|
|
782
|
+
// no-op there; the fan-out is best-effort server-side and never blocks or fails
|
|
783
|
+
// this logout (the hub session is already revoked above).
|
|
784
|
+
seams.emit('builder.logout', {
|
|
785
|
+
builderId: req.builder.id,
|
|
786
|
+
githubId: req.builder.github_id,
|
|
787
|
+
githubLogin: req.builder.github_login,
|
|
788
|
+
});
|
|
620
789
|
res.json({ ok: true });
|
|
621
790
|
});
|
|
622
791
|
|
|
792
|
+
// POST /auth/backchannel-logout — the INSTANCE side of federated single-logout
|
|
793
|
+
// (ADR 0144, task 1002238). The hub POSTs a hub-signed logout token here when a
|
|
794
|
+
// builder signs out of the hub; we verify it and revoke ALL of that builder's
|
|
795
|
+
// local sessions, so one hub logout ends the session on every federated project.
|
|
796
|
+
//
|
|
797
|
+
// rank: public — the hub-signed EdDSA logout token IS the credential (like the
|
|
798
|
+
// OAuth callback), NOT a builder session. Inert (404) on an instance that does
|
|
799
|
+
// not federate through a hub (idp unset), so a self-hosted instance exposes no
|
|
800
|
+
// new surface. Idempotent: an unknown/replayed subject just revokes nothing.
|
|
801
|
+
router.post('/auth/backchannel-logout', async (req, res) => {
|
|
802
|
+
const idp = auth.loadIdpConfig();
|
|
803
|
+
if (!idp) {
|
|
804
|
+
// Not a federated instance — indistinguishable from "no such route".
|
|
805
|
+
return res.status(404).type('text/plain').send('not found');
|
|
806
|
+
}
|
|
807
|
+
if (validateOrRespond(req, res, {
|
|
808
|
+
logout_token: { required: true, type: 'string', maxLength: 4096 },
|
|
809
|
+
})) return;
|
|
810
|
+
const publicKey = auth.loadHubPublicKey();
|
|
811
|
+
if (!publicKey) return res.fail('idp_unconfigured', { status: 503, message: 'hub public key not configured' });
|
|
812
|
+
let payload;
|
|
813
|
+
try {
|
|
814
|
+
payload = auth.verifyHubLogoutToken(req.body.logout_token, { publicKey, clientId: idp.clientId });
|
|
815
|
+
} catch (err) {
|
|
816
|
+
// A bad / expired / forged / wrong-audience token is an auth failure (401),
|
|
817
|
+
// never a server error — and never revokes anything.
|
|
818
|
+
return res.fail('invalid_logout_token', { status: 401, message: String(err.message || 'invalid logout token').slice(0, 160) });
|
|
819
|
+
}
|
|
820
|
+
// Anti-replay (defense-in-depth, symmetric with the CLI assertion path): honor
|
|
821
|
+
// a logout token's jti ONCE. A replay within the ~120s TTL is an idempotent
|
|
822
|
+
// no-op, so a captured token can't keep killing sessions the builder creates
|
|
823
|
+
// AFTER it was minted.
|
|
824
|
+
if (auth.logoutTokenJtiSeenAndRecord(payload.jti, payload.exp)) {
|
|
825
|
+
return res.json({ ok: true });
|
|
826
|
+
}
|
|
827
|
+
try {
|
|
828
|
+
const builder = await db.getBuilderByGithubId(payload.sub);
|
|
829
|
+
// Revoke ALL of this builder's local sessions (web + CLI) — the hub said sign
|
|
830
|
+
// them out. No-op + 200 if we don't know this builder (never leak existence).
|
|
831
|
+
if (builder) await db.revokeAllSessionsForBuilder(builder.id);
|
|
832
|
+
res.json({ ok: true });
|
|
833
|
+
} catch (err) {
|
|
834
|
+
console.error('[gds] /auth/backchannel-logout revoke', err);
|
|
835
|
+
res.fail('server_error', { status: 500, message: 'logout failed' });
|
|
836
|
+
}
|
|
837
|
+
});
|
|
838
|
+
|
|
623
839
|
// POST /auth/cli-token/issue — V3.R91 / #401. Issue a fresh CLI bearer for
|
|
624
840
|
// the requesting builder, gated on a WEB COOKIE session (not a bearer). The
|
|
625
841
|
// browser already proves identity via the gds_session cookie; this endpoint
|
|
@@ -835,6 +1051,7 @@ module.exports = function buildAuthRouter() {
|
|
|
835
1051
|
// open-redirect guard on the post-sign-in return target and the cross-subdomain
|
|
836
1052
|
// cookie-domain decision — worth locking down independently of a running server.
|
|
837
1053
|
module.exports.safeReturnTo = safeReturnTo;
|
|
1054
|
+
module.exports.postLoginLanding = postLoginLanding;
|
|
838
1055
|
module.exports.sessionCookieDomainOpts = sessionCookieDomainOpts;
|
|
839
1056
|
module.exports.reqHost = reqHost;
|
|
840
1057
|
// The auth-error page renderer (task 1439): pure over the branding pack, so a
|
|
@@ -37,7 +37,39 @@ const BACKUP_SCRIPT = process.env.GDS_BACKUP_SCRIPT
|
|
|
37
37
|
// In-memory guard: true while a trigger-initiated dump is running.
|
|
38
38
|
let triggerRunning = false;
|
|
39
39
|
|
|
40
|
-
|
|
40
|
+
// A dump older than this counts as STALE. The nightly timer fires daily, so 36h
|
|
41
|
+
// allows one missed run plus slack before we call it broken.
|
|
42
|
+
const STALE_AFTER_HOURS = Number(process.env.GDS_BACKUP_STALE_HOURS) || 36;
|
|
43
|
+
|
|
44
|
+
// parseDumpTimestamp — the dump filename stamp `YYYY-MM-DDTHHMMSSZ` is ALMOST
|
|
45
|
+
// ISO-8601 but omits the time separators, so Date.parse rejects it. Re-insert
|
|
46
|
+
// the colons and parse. Returns epoch ms, or null if it doesn't match.
|
|
47
|
+
// Pure + exported so tests can pin it without touching the filesystem.
|
|
48
|
+
function parseDumpTimestamp(stamp) {
|
|
49
|
+
const m = /^(\d{4}-\d{2}-\d{2})T(\d{2})(\d{2})(\d{2})Z$/.exec(String(stamp || ''));
|
|
50
|
+
if (!m) return null;
|
|
51
|
+
const ms = Date.parse(`${m[1]}T${m[2]}:${m[3]}:${m[4]}Z`);
|
|
52
|
+
return Number.isFinite(ms) ? ms : null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// dumpAgeHours — hours between a dump stamp and now. null when unparseable, so
|
|
56
|
+
// callers can tell "cannot tell" apart from "fresh". Pure (now is injectable).
|
|
57
|
+
function dumpAgeHours(stamp, nowMs = Date.now()) {
|
|
58
|
+
const ms = parseDumpTimestamp(stamp);
|
|
59
|
+
if (ms === null) return null;
|
|
60
|
+
return Math.max(0, (nowMs - ms) / 3_600_000);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// isStaleDump — fail LOUD on an unreadable/absent stamp. A dump we cannot date
|
|
64
|
+
// must not read as healthy: that silence is exactly how the cloudbongos nightly
|
|
65
|
+
// sat failed for 13 days (2026-07-15 → 07-28) with nothing noticing (1002479).
|
|
66
|
+
function isStaleDump(stamp, nowMs = Date.now(), staleAfterHours = STALE_AFTER_HOURS) {
|
|
67
|
+
const age = dumpAgeHours(stamp, nowMs);
|
|
68
|
+
if (age === null) return true;
|
|
69
|
+
return age > staleAfterHours;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function latestDump(nowMs = Date.now()) {
|
|
41
73
|
let files;
|
|
42
74
|
try {
|
|
43
75
|
files = fs.readdirSync(BACKUP_DIR);
|
|
@@ -54,7 +86,18 @@ function latestDump() {
|
|
|
54
86
|
try { size_bytes = fs.statSync(full).size; } catch { /* ok */ }
|
|
55
87
|
// Filename: <db>-<YYYY-MM-DDTHHMMSSZ>.sql.gz — extract timestamp from it.
|
|
56
88
|
const ts = name.replace(`${DB_NAME}-`, '').replace('.sql.gz', '');
|
|
57
|
-
|
|
89
|
+
const age = dumpAgeHours(ts, nowMs);
|
|
90
|
+
return {
|
|
91
|
+
filename: name,
|
|
92
|
+
timestamp: ts,
|
|
93
|
+
size_bytes,
|
|
94
|
+
count: dumps.length,
|
|
95
|
+
// Staleness is reported, not just implied by the timestamp, so a consumer
|
|
96
|
+
// (CLI, dashboard, alert) can act on one boolean instead of re-deriving it.
|
|
97
|
+
age_hours: age === null ? null : Math.round(age * 10) / 10,
|
|
98
|
+
stale: isStaleDump(ts, nowMs),
|
|
99
|
+
stale_after_hours: STALE_AFTER_HOURS,
|
|
100
|
+
};
|
|
58
101
|
}
|
|
59
102
|
|
|
60
103
|
module.exports = function buildBackupRouter() {
|
|
@@ -68,6 +111,11 @@ module.exports = function buildBackupRouter() {
|
|
|
68
111
|
backup_dir: BACKUP_DIR,
|
|
69
112
|
db_name: DB_NAME,
|
|
70
113
|
latest_dump: dump,
|
|
114
|
+
// Top-level too, so a consumer gets a health signal WITHOUT having to
|
|
115
|
+
// special-case latest_dump===null. No dump at all is the worst state,
|
|
116
|
+
// not a neutral one, so it reports stale.
|
|
117
|
+
stale: dump ? dump.stale : true,
|
|
118
|
+
age_hours: dump ? dump.age_hours : null,
|
|
71
119
|
trigger_running: triggerRunning,
|
|
72
120
|
});
|
|
73
121
|
}));
|
|
@@ -102,3 +150,11 @@ module.exports = function buildBackupRouter() {
|
|
|
102
150
|
|
|
103
151
|
return router;
|
|
104
152
|
};
|
|
153
|
+
|
|
154
|
+
// Pure helpers exported for tests (tests/backup_cli.mjs) — importing the real
|
|
155
|
+
// implementations beats re-implementing them in the test, which is how a test
|
|
156
|
+
// can pass while the shipped code is wrong.
|
|
157
|
+
module.exports.parseDumpTimestamp = parseDumpTimestamp;
|
|
158
|
+
module.exports.dumpAgeHours = dumpAgeHours;
|
|
159
|
+
module.exports.isStaleDump = isStaleDump;
|
|
160
|
+
module.exports.STALE_AFTER_HOURS = STALE_AFTER_HOURS;
|
package/src/bongos/routes/me.js
CHANGED
|
@@ -83,7 +83,7 @@ module.exports = function buildMeRouter() {
|
|
|
83
83
|
const [
|
|
84
84
|
builder, activeClaims, estVsActual, unlocked, lockedWithProgress, streakInfo,
|
|
85
85
|
recentGrades, builderAvg, onboardingRow, shipCount, recentKarma, karma30d, discordLink, boxConnected, cliAuthed,
|
|
86
|
-
behaviorPrefs, geminiKeyRow,
|
|
86
|
+
behaviorPrefs, geminiKeyRow, needsRebaseTasks,
|
|
87
87
|
] = await Promise.all([
|
|
88
88
|
db.getBuilderById(req.builder.id),
|
|
89
89
|
lc().getActiveClaimsForBuilder(req.builder.id),
|
|
@@ -136,6 +136,14 @@ module.exports = function buildMeRouter() {
|
|
|
136
136
|
// Own Gemini-key status via the art-pipeline port (ADR 0102) — the storage
|
|
137
137
|
// is art-pipeline domain now; degrades to a no-key shape on a vanilla instance.
|
|
138
138
|
seams.resolveOptional('art.getBuilderGeminiKeyRow', async () => ({ enc: null, last4: null, set_at: null }))(req.builder.id),
|
|
139
|
+
// ADR 0120 part 5: the confirmed tasks this builder owes a rebase on —
|
|
140
|
+
// un-landed confirmed work the publish-reconciler flagged needs_rebase
|
|
141
|
+
// (part 2). It GATES new claims (part 3: claimTask refuses REBASE_REQUIRED)
|
|
142
|
+
// and its per-task reward now lands only on ship (part 1), so the CLI
|
|
143
|
+
// (/builder-start) + the hall surface an owed-rebase warning from this
|
|
144
|
+
// without a follow-up call. Through the `lifecycle` port; [] on a
|
|
145
|
+
// lifecycle-less instance (guarded — the port is resolveOptional).
|
|
146
|
+
lc() ? lc().needsRebaseTasksForBuilder(req.builder.id) : [],
|
|
139
147
|
]);
|
|
140
148
|
// V3.R84 (#302), criterion C3: the builder's month-to-date spend vs their
|
|
141
149
|
// monthly budget. Surfaced here so the CLI (session-start ping) and the hall
|
|
@@ -148,6 +156,18 @@ module.exports = function buildMeRouter() {
|
|
|
148
156
|
} catch (err) {
|
|
149
157
|
console.error('[gds] GET /me cost status', err);
|
|
150
158
|
}
|
|
159
|
+
// Best-effort read of the resolved reward policy (see the payload note).
|
|
160
|
+
// Never break the profile over a display concern — null degrades the CLI to
|
|
161
|
+
// its own paying default rather than a wrong "not paid" claim.
|
|
162
|
+
const rewardPolicySafe = () => {
|
|
163
|
+
try {
|
|
164
|
+
const p = typeof reward.rewardConfig === 'function' ? reward.rewardConfig() : null;
|
|
165
|
+
return p && typeof p.mode === 'string' ? { mode: p.mode, perTaskCredit: p.perTaskCredit !== false } : null;
|
|
166
|
+
} catch (err) {
|
|
167
|
+
console.error('[gds] GET /me reward policy', err);
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
};
|
|
151
171
|
// For each active claim, compute the streak preview (cheap; pool query).
|
|
152
172
|
// Lets the CLI render "shipping this would extend streak to N days" hints.
|
|
153
173
|
const activeClaimsWithPreview = await Promise.all(
|
|
@@ -161,6 +181,20 @@ module.exports = function buildMeRouter() {
|
|
|
161
181
|
active_claims: activeClaimsWithPreview,
|
|
162
182
|
// Legacy field — first claim only. Prefer active_claims (plural).
|
|
163
183
|
active_claim: activeClaimsWithPreview[0] ?? null,
|
|
184
|
+
// ADR 0120 part 5: un-landed confirmed tasks this builder owes a rebase on.
|
|
185
|
+
// Each row: { id, title, needs_rebase_reason, needs_rebase_at }, oldest
|
|
186
|
+
// first. Empty when none owed (or on a lifecycle-less instance). These GATE
|
|
187
|
+
// new claims (part 3) and their reward is pending until they land on ship
|
|
188
|
+
// (part 1); the CLI + hall render a warning off this array.
|
|
189
|
+
needs_rebase_tasks: needsRebaseTasks,
|
|
190
|
+
// ADR 0146 / task 1002469: the instance's RESOLVED reward policy, so the
|
|
191
|
+
// CLI can label a task's advertised `credits_reward` honestly. On a
|
|
192
|
+
// `cost-plus-only` instance that estimate is never paid, and a builder who
|
|
193
|
+
// isn't told reads a correct `+0` at ship as a broken payout. It must come
|
|
194
|
+
// from the SERVER — the instance's branding.json is not in the builder's
|
|
195
|
+
// checkout, so a laptop reading local config always sees the default.
|
|
196
|
+
// Resolved by economy (never re-derived); null if economy is disabled.
|
|
197
|
+
reward: rewardPolicySafe(),
|
|
164
198
|
est_vs_actual: estVsActual,
|
|
165
199
|
unlocked,
|
|
166
200
|
locked_with_progress: lockedWithProgress,
|
|
@@ -47,7 +47,7 @@ const { buildBongosDownloads } = require('../bongos-downloads');
|
|
|
47
47
|
const { clientBranding, branding } = require('../branding');
|
|
48
48
|
const { clientModules, isModuleEnabled } = require('../modules');
|
|
49
49
|
const { hallWidgetScripts, moduleWebSurfaces } = require('../module-loader/loader');
|
|
50
|
-
const { resolveCoreRoot } = require('../instance-config');
|
|
50
|
+
const { resolveCoreRoot, resolveDocsRoot, resolveInstanceRoot } = require('../instance-config');
|
|
51
51
|
const { API_PREFIX, LEGACY_API_PREFIXES, API_VERSION, VERSIONED_API_PREFIX, ALL_API_PREFIXES } = require('./api-prefix');
|
|
52
52
|
|
|
53
53
|
// ADR 0108 §1: every use of ROOT below (infra/ installers, modules/, docs/) is
|
|
@@ -56,15 +56,28 @@ const { API_PREFIX, LEGACY_API_PREFIXES, API_VERSION, VERSIONED_API_PREFIX, ALL_
|
|
|
56
56
|
const ROOT = resolveCoreRoot();
|
|
57
57
|
|
|
58
58
|
// Instance-only RELEASE config (config/devbox-app.json, config/bongos-cli.json) is
|
|
59
|
-
// boundary-excluded from the portable core (publish-manifest INSTANCE_EXCLUDE), so
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
59
|
+
// boundary-excluded from the portable core (publish-manifest INSTANCE_EXCLUDE), so it is
|
|
60
|
+
// HOST content: it lives at the INSTANCE root, which on a vendored instance is NOT the
|
|
61
|
+
// package dir this file sits under. A core-relative require therefore looked in
|
|
62
|
+
// node_modules/@bongos/core/config/ and never found it, so /downloads/* silently failed
|
|
63
|
+
// to mount (task 1002371 — same class as ADR 0144's vendored infra/ path bug; ADR 0145's
|
|
64
|
+
// copy-the-config-into-node_modules workaround is lost on the next `npm ci`). So resolve
|
|
65
|
+
// the instance root FIRST — the branding/modules resolution (src/branding.js INSTANCE_PATH)
|
|
66
|
+
// — then the core package dir, which keeps a single checkout (and that copied-in
|
|
67
|
+
// workaround) byte-identical.
|
|
68
|
+
//
|
|
69
|
+
// Fail-soft is preserved verbatim: an ABSENT config makes its download surface simply not
|
|
70
|
+
// mount (fail-to-vanilla, like branding/modules) rather than crash the boot as the original
|
|
71
|
+
// hard require did (task 1824); a MALFORMED config still throws — only a MISSING file is
|
|
72
|
+
// tolerated. `roots` is injected so the ladder is unit-testable with zero ambient state.
|
|
73
|
+
function tryLoadInstanceConfig(name, roots = [resolveInstanceRoot(), ROOT]) {
|
|
74
|
+
for (const root of new Set(roots)) {
|
|
75
|
+
let raw;
|
|
76
|
+
try { raw = fs.readFileSync(path.join(root, 'config', name), 'utf8'); }
|
|
77
|
+
catch (e) { if (e.code === 'ENOENT') continue; throw e; }
|
|
78
|
+
return JSON.parse(raw);
|
|
79
|
+
}
|
|
80
|
+
return null;
|
|
68
81
|
}
|
|
69
82
|
|
|
70
83
|
// Map the pack's camelCase theme color keys → the kebab-case CSS custom
|
|
@@ -193,6 +206,60 @@ function docsIndexHtml() {
|
|
|
193
206
|
return _docsHtml;
|
|
194
207
|
}
|
|
195
208
|
|
|
209
|
+
// The OpenAPI 3.1 spec served at /docs/openapi.json — SELF-HEALING (task 1002200).
|
|
210
|
+
// The Redoc page (public-docs/) ships as static assets, but the SPEC is a generated
|
|
211
|
+
// artifact whose on-disk copy is NOT guaranteed at every deploy topology, so a bare
|
|
212
|
+
// res.sendFile(one fixed path) 404s exactly when the file is missing:
|
|
213
|
+
// • Standalone instance (ADR 0108/0140): the route resolves from the CORE package
|
|
214
|
+
// (resolveCoreRoot → node_modules/@bongos/core) while the generator writes the
|
|
215
|
+
// INSTANCE's own spec to resolveDocsRoot (the instance repo) — a path mismatch.
|
|
216
|
+
// • A pinned @bongos/core that predates the spec's packaging ships NO copy at all
|
|
217
|
+
// (the observed cloudbongos.com 404: public-docs/ shipped, docs/api/ didn't).
|
|
218
|
+
// • A fresh instance, before its first generate, has neither.
|
|
219
|
+
// So resolve the spec in tiers: (1) the committed spec at resolveDocsRoot() (the
|
|
220
|
+
// instance's own — carries its module routes), then (2) at resolveCoreRoot() (the
|
|
221
|
+
// core-package copy), then (3) GENERATE it in-process from the live route source.
|
|
222
|
+
// gen-api-docs.build() reads routes from resolveCoreRoot() (always present — it IS
|
|
223
|
+
// the running code), is DB-free + express-free (a pure line+regex extractor that
|
|
224
|
+
// never executes a route or opens a connection), and yields the identical spec.
|
|
225
|
+
// resolveOpenapiJson is the pure, injectable core so the fallback ladder is unit-
|
|
226
|
+
// testable (tests/docs_openapi_route.mjs).
|
|
227
|
+
function resolveOpenapiJson({ candidates = [], generate } = {}) {
|
|
228
|
+
for (const p of candidates) {
|
|
229
|
+
let raw;
|
|
230
|
+
try { raw = fs.readFileSync(p, 'utf8'); } catch { continue; } // absent → next tier
|
|
231
|
+
try { JSON.parse(raw); } catch { continue; } // corrupt → next tier
|
|
232
|
+
return raw;
|
|
233
|
+
}
|
|
234
|
+
if (typeof generate === 'function') {
|
|
235
|
+
try {
|
|
236
|
+
const out = generate();
|
|
237
|
+
if (out) return out;
|
|
238
|
+
} catch (e) {
|
|
239
|
+
console.error('[docs] in-process OpenAPI generation failed:', e && e.message);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return '';
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Memoized like docsIndexHtml: one resolve per process; a redeploy restarts and
|
|
246
|
+
// re-resolves. '' (→ the route falls through to 404) only if every tier fails.
|
|
247
|
+
let _openapiJson;
|
|
248
|
+
function openapiJson() {
|
|
249
|
+
if (_openapiJson !== undefined) return _openapiJson;
|
|
250
|
+
const roots = [...new Set([resolveDocsRoot(), ROOT])]; // instance repo first, then core pkg
|
|
251
|
+
_openapiJson = resolveOpenapiJson({
|
|
252
|
+
candidates: roots.map((r) => path.join(r, 'docs/api/openapi.json')),
|
|
253
|
+
generate() {
|
|
254
|
+
// Lazy require: boot never pays for it, and a generator hiccup is contained
|
|
255
|
+
// to this one endpoint rather than taking down server start.
|
|
256
|
+
const gen = require('../../scripts/gds/gen-api-docs.js');
|
|
257
|
+
return JSON.stringify(gen.build().spec, null, 2) + '\n';
|
|
258
|
+
},
|
|
259
|
+
});
|
|
260
|
+
return _openapiJson;
|
|
261
|
+
}
|
|
262
|
+
|
|
196
263
|
// The browser-global build of the generated typed client (task 1996 / R09),
|
|
197
264
|
// served at /lib/bongos-client.js so classic <script> web pages can consume the
|
|
198
265
|
// same generated client the CLI uses (window.BongosClient). Read once at boot
|
|
@@ -408,7 +475,7 @@ function mountInternalSurfaces(app) {
|
|
|
408
475
|
// builders.<apex> (the builders rewrite shim below only prefixes non-/api
|
|
409
476
|
// paths, so without this placement /downloads/* would be rewritten to
|
|
410
477
|
// /builders/downloads/* and 404).
|
|
411
|
-
const devboxCfg =
|
|
478
|
+
const devboxCfg = tryLoadInstanceConfig('devbox-app.json');
|
|
412
479
|
if (devboxCfg) {
|
|
413
480
|
const devboxDownloads = buildDevboxDownloads(devboxCfg);
|
|
414
481
|
app.get('/downloads/devbox/latest.json', (_req, res) => {
|
|
@@ -439,7 +506,7 @@ function mountInternalSurfaces(app) {
|
|
|
439
506
|
// every user-facing URL on the apex. The installer scripts are served with the
|
|
440
507
|
// server's PINNED origin substituted for __GDS_API_BASE__ — never a request
|
|
441
508
|
// header, so a spoofed Host can't redirect a `curl | sh` (the box-connect.sh rule).
|
|
442
|
-
const bongosCfg =
|
|
509
|
+
const bongosCfg = tryLoadInstanceConfig('bongos-cli.json');
|
|
443
510
|
if (bongosCfg) {
|
|
444
511
|
const bongosDownloads = buildBongosDownloads(bongosCfg);
|
|
445
512
|
// Pinned public origin for the served installer scripts (never a request header —
|
|
@@ -847,7 +914,7 @@ function mountInternalSurfaces(app) {
|
|
|
847
914
|
// external fetch (matches the /vendor/* self-hosting convention; CSP-safe).
|
|
848
915
|
// GET /docs → the Redoc host page (branding-injected)
|
|
849
916
|
// GET /docs/redoc.standalone.js → the vendored renderer (long-cached)
|
|
850
|
-
// GET /docs/openapi.json → the
|
|
917
|
+
// GET /docs/openapi.json → the spec, SELF-HEALING (committed → generated), CORS-open so Postman/Swagger/AI can fetch it; see openapiJson()
|
|
851
918
|
app.get('/docs', (req, res, next) => {
|
|
852
919
|
if (isModuleSurfaceHost(req)) return next();
|
|
853
920
|
const html = docsIndexHtml();
|
|
@@ -864,10 +931,12 @@ function mountInternalSurfaces(app) {
|
|
|
864
931
|
});
|
|
865
932
|
app.get('/docs/openapi.json', (req, res, next) => {
|
|
866
933
|
if (isModuleSurfaceHost(req)) return next();
|
|
934
|
+
const json = openapiJson();
|
|
935
|
+
if (!json) return next(); // no committed spec + generation failed → fall through to 404
|
|
867
936
|
res.set('Access-Control-Allow-Origin', '*'); // read-only spec — tools fetch it cross-origin
|
|
868
937
|
res.set('Cache-Control', 'public, max-age=60');
|
|
869
938
|
res.type('application/json');
|
|
870
|
-
res.
|
|
939
|
+
res.send(json);
|
|
871
940
|
});
|
|
872
941
|
|
|
873
942
|
app.use((req, res, next) => {
|
|
@@ -895,4 +964,4 @@ function mountInternalSurfaces(app) {
|
|
|
895
964
|
return app;
|
|
896
965
|
}
|
|
897
966
|
|
|
898
|
-
module.exports = { mountInternalSurfaces, isStatusHost, isBuildersHost, injectBrandingGlobal, injectBrandingInto, injectModuleHallWidgets, themeMarkup, applyBrandTokens };
|
|
967
|
+
module.exports = { mountInternalSurfaces, isStatusHost, isBuildersHost, injectBrandingGlobal, injectBrandingInto, injectModuleHallWidgets, themeMarkup, applyBrandTokens, resolveOpenapiJson, openapiJson, tryLoadInstanceConfig };
|
package/src/branding.js
CHANGED
|
@@ -49,6 +49,11 @@ const ENV_OVERRIDES = [
|
|
|
49
49
|
// idp:false), so a co-tenant instance sharing the neutral pack still federates
|
|
50
50
|
// individually. 'bool'-coerced below — auth.js idpEnabled() reads it as strict === true.
|
|
51
51
|
['AUTH_IDP', ['auth', 'idp'], 'bool'],
|
|
52
|
+
// Post-sign-in landing when no ?return=/Referer was carried (auth.js
|
|
53
|
+
// postLoginLanding). Env-overridable so a platform-home instance can flip its
|
|
54
|
+
// front door with one env var and no instance branding.json — cloudbongos.com
|
|
55
|
+
// sets <PREFIX>_POST_LOGIN_LANDING=/gate/ (the orbs). Task 1002240.
|
|
56
|
+
['POST_LOGIN_LANDING', ['landing', 'postLogin']],
|
|
52
57
|
];
|
|
53
58
|
|
|
54
59
|
const isObj = (v) => v && typeof v === 'object' && !Array.isArray(v);
|
|
@@ -192,7 +197,34 @@ function clientBranding(b = branding()) {
|
|
|
192
197
|
notFound: b.copy?.notFound || 'Page not found.',
|
|
193
198
|
},
|
|
194
199
|
rankLabels: b.rankLabels || {},
|
|
200
|
+
reward: clientReward(),
|
|
195
201
|
};
|
|
196
202
|
}
|
|
197
203
|
|
|
204
|
+
// The RESOLVED reward policy, projected client-safe (ADR 0146 follow-up, task
|
|
205
|
+
// 1002469). Display surfaces need to know which streams this instance actually
|
|
206
|
+
// pays — on a `cost-plus-only` instance the per-task "Est." is a guide, never a
|
|
207
|
+
// payout, and a builder who is not told that reads a correct `+0` as a bug.
|
|
208
|
+
//
|
|
209
|
+
// Resolved through the economy module's `reward` port, NOT re-derived here: the
|
|
210
|
+
// mode→perTaskCredit mapping is economy's to own (ADR 0146 put the knob inside
|
|
211
|
+
// the module deliberately). `module-seams` is a dependency-free leaf, so this
|
|
212
|
+
// adds no boot-order coupling to the config layer.
|
|
213
|
+
//
|
|
214
|
+
// Fail-safe: an instance with economy disabled, or a resolve that throws, falls
|
|
215
|
+
// back to the ADR 0146 DEFAULT (pay both streams) — the same direction the pure
|
|
216
|
+
// resolver takes an unknown mode, so a hiccup can never silently tell builders
|
|
217
|
+
// their per-task reward is withheld.
|
|
218
|
+
function clientReward() {
|
|
219
|
+
const fallback = { mode: 'cost-plus-and-estimate', perTaskCredit: true };
|
|
220
|
+
try {
|
|
221
|
+
const reward = require('./module-seams').resolveOptional('reward');
|
|
222
|
+
const p = reward && typeof reward.rewardConfig === 'function' ? reward.rewardConfig() : null;
|
|
223
|
+
if (!p || typeof p.mode !== 'string') return fallback;
|
|
224
|
+
return { mode: p.mode, perTaskCredit: p.perTaskCredit !== false };
|
|
225
|
+
} catch (_) {
|
|
226
|
+
return fallback;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
198
230
|
module.exports = { resolveBranding, loadBranding, deepMerge, branding, clientBranding, userAgent, NEUTRAL_PATH, INSTANCE_PATH };
|