@bongos/core 1.19.1073 → 1.19.1074
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 +320 -465
- package/.claude/skills/feedback/SKILL.md +2 -2
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +4 -60
- package/clients/bongos-client/index.cjs +4 -60
- package/clients/bongos-client/index.d.ts +5 -90
- package/clients/bongos-client/index.mjs +4 -60
- package/config/branding.neutral.json +1 -5
- package/config/modules.neutral.json +2 -3
- package/config/scheduled-routines.json +0 -2
- package/docs/adr/0145-free-hosted-project-tier-isolation-and-domain-separation.md +1 -0
- package/docs/adr/0285-a-shared-box-holds-about-twelve-projects-per-gb-and-memory-is-the-wall.md +1 -0
- package/docs/adr/0323-hosting-is-three-shapes-and-we-are-not-the-landlord.md +1 -1
- package/docs/adr/0327-a-cloud-host-runs-on-the-owners-account-and-the-key-is-borrowed.md +1 -1
- package/docs/adr/0341-the-page-is-the-unit-of-tweak-mode.md +2 -0
- package/docs/adr/0345-we-host-every-projects-hall-and-its-app-deploys-where-the-owner-chooses.md +94 -0
- package/docs/adr/README.md +1 -0
- package/docs/api/openapi.json +171 -1661
- package/docs/api-reference.md +9 -41
- package/docs/architecture.md +16 -1
- package/docs/branding-contract.md +0 -3
- package/docs/copy-inventory.md +454 -479
- package/docs/copy-registry.json +746 -978
- package/docs/file-map.md +5 -34
- package/docs/module-api-changelog.md +2 -0
- package/docs/modules-contract.md +13 -1
- package/docs/onboarding/diagrams/04-architecture.mmd +11 -22
- package/docs/onboarding/diagrams/README.md +3 -3
- package/docs/onboarding/diagrams/assertions.json +2 -22
- package/docs/onboarding/primer.md +9 -28
- package/docs/page-inventory.json +4 -1
- package/docs/page-readings.json +1032 -1077
- package/docs/recipes/render-pilot.md +29 -10
- package/migrations/core_257_module_store_registry.sql +87 -0
- package/migrations/core_258_drop_dev_box_tables.sql +55 -0
- package/modules/agents/module.json +1 -0
- package/modules/autonomy/module.json +1 -0
- package/modules/builder-settings/module.json +1 -0
- package/modules/builder-settings/render-prefs.js +4 -5
- package/modules/copy-desk/docx.js +78 -5
- package/modules/copy-desk/module.json +1 -0
- package/modules/copy-desk/routes/copy-desk.js +9 -9
- package/modules/copy-desk/tests/copy_docx.mjs +107 -1
- package/modules/copy-desk/tests/fixtures/word-docx.mjs +30 -0
- package/modules/discord/module.json +1 -0
- package/modules/economy/module.json +1 -0
- package/modules/economy/routes/credits.js +1 -2
- package/modules/government/catalog.js +13 -17
- package/modules/government/migrations/government_019_retire_box_permissions.sql +33 -0
- package/modules/government/module.json +1 -0
- package/modules/government/protected-surfaces.json +3 -11
- package/modules/government/resolver.js +3 -46
- package/modules/government/routes/government.js +0 -2
- package/modules/government/session-scopes.js +43 -83
- package/modules/government/session-scopes.json +8 -18
- package/modules/grading/module.json +1 -0
- package/modules/hall-ui/module.json +1 -0
- package/modules/hall-ui/public/board-lib.js +2 -2
- package/modules/hall-ui/public/builders.js +29 -40
- package/modules/hall-ui/public/collab.html +1 -1
- package/modules/hall-ui/public/collab.js +1 -1
- package/modules/hall-ui/public/diagrams.html +3 -3
- package/modules/hall-ui/public/dom-utils.js +1 -1
- package/modules/hall-ui/public/gate.js +2 -2
- package/modules/hall-ui/public/gate.states.json +1 -1
- package/modules/hall-ui/public/hall-render.js +43 -79
- package/modules/hall-ui/public/index.html +5 -1
- package/modules/hall-ui/public/modules.js +1 -0
- package/modules/hall-ui/public/oversight.css +5 -5
- package/modules/hall-ui/public/palette.js +0 -11
- package/modules/hall-ui/public/primer.js +1 -1
- package/modules/hall-ui/public/sessions.html +1 -1
- package/modules/hall-ui/public/settings-sessions.js +1 -1
- package/modules/hall-ui/public/settings.css +0 -1
- package/modules/hall-ui/public/settings.html +4 -33
- package/modules/hall-ui/public/settings.js +3 -33
- package/modules/hall-ui/public/settings.states.json +2 -2
- package/modules/hall-ui/public/studio.html +1 -1
- package/modules/hall-ui/public/style.css +10 -13
- package/modules/hall-ui/public/task.html +4 -1
- package/modules/hall-ui/public/task.js +36 -1
- package/modules/hall-ui/public/tweak-editor-lib.js +18 -1
- package/modules/hall-ui/public/tweak-editor.css +16 -0
- package/modules/hall-ui/public/tweak-editor.html +26 -3
- package/modules/hall-ui/public/tweak-editor.js +32 -1
- package/modules/hall-ui/public/work.js +1 -1
- package/modules/hall-ui/records/tweak-editor.md +1 -0
- package/modules/ideas/module.json +1 -0
- package/modules/lifecycle/db-claim-reads.js +1 -58
- package/modules/lifecycle/db-ship.js +0 -5
- package/modules/lifecycle/db.js +0 -4
- package/modules/lifecycle/kickoff-checklist.js +0 -4
- package/modules/lifecycle/lifecycle.js +0 -8
- package/modules/lifecycle/module.json +1 -0
- package/modules/lifecycle/routes/tasks.js +5 -7
- package/modules/memory/module.json +1 -0
- package/modules/npm-release/module.json +1 -0
- package/modules/onboarding/module.json +1 -0
- package/modules/onboarding/onboarding-state.js +17 -36
- package/modules/onboarding/routes/access-requests.js +3 -3
- package/modules/platform-identity/module.json +1 -0
- package/modules/platform-identity/platform-identity.js +13 -5
- package/modules/platform-identity/routes/sso.js +2 -2
- package/modules/platform-identity/tests/platform-identity.mjs +17 -3
- package/modules/provisioning/module.json +1 -0
- package/modules/provisioning/provisioning.js +2 -2
- package/modules/provisioning/starter-bundles.js +5 -25
- package/modules/public-landing/module.json +1 -0
- package/modules/public-landing/public/assets/cosmos.css +1 -1
- package/modules/public-landing/public/contact.html +1 -1
- package/modules/public-landing/public/privacy.html +1 -1
- package/modules/public-landing/public/projects.html +101 -32
- package/modules/public-landing/public/projects.probes.json +2 -2
- package/modules/public-landing/public/projects.states.json +4 -3
- package/modules/public-landing/public/terms.html +1 -1
- package/modules/security/module.json +1 -0
- package/modules/sessions/module.json +1 -0
- package/modules/specialities/module.json +1 -0
- package/modules/status-ui/module.json +1 -0
- package/modules/ui-design/module.json +1 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +78 -0
- package/scripts/gds/artifact-format.js +203 -0
- package/scripts/gds/audit-authorship.sh +1 -13
- package/scripts/gds/claim.js +0 -15
- package/scripts/gds/cli-lib.js +8 -8
- package/scripts/gds/diagram-facts.js +12 -23
- package/scripts/gds/do-api.js +13 -80
- package/scripts/gds/doc-cli-guard.js +2 -0
- package/scripts/gds/doctor.js +1 -1
- package/scripts/gds/feedback-latest.js +139 -9
- package/scripts/gds/fitness-checks-identity.js +2 -7
- package/scripts/gds/fitness-checks-write-validation.js +4 -0
- package/scripts/gds/fitness-ratchets.js +4 -4
- package/scripts/gds/fitness.js +10 -10
- package/scripts/gds/gen-diagrams.js +49 -71
- package/scripts/gds/http-api-client.js +12 -19
- package/scripts/gds/init.js +1 -1
- package/scripts/gds/lint-infra-exec.js +1 -1
- package/scripts/gds/local-preview-lib.js +8 -39
- package/scripts/gds/local-preview.js +2 -15
- package/scripts/gds/migration-namespace.js +3 -3
- package/scripts/gds/module-artifact.js +212 -0
- package/scripts/gds/module.js +95 -6
- package/scripts/gds/package-core.js +5 -116
- package/scripts/gds/provision-config.js +2 -2
- package/scripts/gds/provision-net.js +2 -3
- package/scripts/gds/provision.js +9 -9
- package/scripts/gds/publish-manifest.js +1 -2
- package/scripts/gds/regen-instance-docs.js +1 -1
- package/scripts/gds/run-unit-tests.js +2 -16
- package/scripts/gds/sandbox-stage.js +39 -341
- package/scripts/gds/ship-flow.js +6 -6
- package/scripts/gds/ship-land.js +3 -4
- package/scripts/gds/ship-regen.js +10 -41
- package/scripts/gds/ship.js +4 -24
- package/scripts/gds/start.js +0 -15
- package/scripts/hall-preview/README.md +5 -5
- package/scripts/hall-preview/server.js +6 -6
- package/scripts/render-diagrams.sh +1 -1
- package/src/bongos/api-errors.js +1 -1
- package/src/bongos/auth-github.js +2 -3
- package/src/bongos/auth.js +51 -126
- package/src/bongos/db-kernel.js +2 -3
- package/src/bongos/db.js +5 -28
- package/src/bongos/module-scope-map.js +14 -29
- package/src/bongos/module-store.js +141 -0
- package/src/bongos/route-rank-check.js +3 -16
- package/src/bongos/routes/auth.js +0 -116
- package/src/bongos/routes/builders.js +5 -15
- package/src/bongos/routes/instance.js +8 -14
- package/src/bongos/routes/me.js +6 -22
- package/src/bongos/routes/modules.js +83 -0
- package/src/bongos/routes.js +5 -0
- package/src/module-api.js +13 -6
- package/src/module-loader/catalog.js +3 -0
- package/src/module-loader/loader.js +4 -4
- package/src/module-loader/manifest-schema.js +15 -7
- package/src/module-seams.js +2 -3
- package/src/modules.js +42 -14
- package/tests/agents_spend_guard.mjs +1 -1
- package/tests/api_client.mjs +0 -15
- package/tests/auth_page_gate.mjs +10 -10
- package/tests/canonical_profile_url.mjs +1 -3
- package/tests/claim_action.mjs +4 -1
- package/tests/claim_from_task_and_home.mjs +127 -0
- package/tests/consumer_layout_boot.mjs +2 -0
- package/tests/copy_desk_page_docx.mjs +33 -2
- package/tests/core_upgrade_runner.mjs +6 -6
- package/tests/deploy_divergence_line.mjs +11 -11
- package/tests/diagram_facts_offset.mjs +1 -1
- package/tests/do_api.mjs +106 -0
- package/tests/feedback_latest.mjs +122 -0
- package/tests/fitness.mjs +44 -42
- package/tests/gate_approvals.mjs +0 -1
- package/tests/go_live.mjs +3 -3
- package/tests/government_abuse_matrix.mjs +20 -19
- package/tests/government_ownership_scope.mjs +33 -35
- package/tests/government_parity.mjs +2 -2
- package/tests/government_protected_surfaces.mjs +56 -37
- package/tests/government_require_permission.mjs +3 -3
- package/tests/government_seed.mjs +37 -4
- package/tests/government_session_scope.mjs +92 -366
- package/tests/hall_error_envelope.mjs +4 -0
- package/tests/hall_landing_boot.mjs +4 -1
- package/tests/hall_palette.mjs +1 -16
- package/tests/hall_record_world.mjs +6 -0
- package/tests/hall_tweak_editor.mjs +52 -1
- package/tests/host_topology_skips.mjs +1 -2
- package/tests/html_comment_nesting.mjs +137 -0
- package/tests/infra_exec_paths.mjs +9 -10
- package/tests/init.mjs +4 -5
- package/tests/instance_manifest.mjs +18 -25
- package/tests/kickoff_checklist.mjs +1 -1
- package/tests/lib_sh_resolution.mjs +0 -221
- package/tests/local_preview.mjs +0 -12
- package/tests/module-scope-map.mjs +9 -9
- package/tests/module_catalog.mjs +1 -1
- package/tests/module_cli.mjs +15 -15
- package/tests/module_contributions.mjs +6 -6
- package/tests/module_loader.mjs +6 -6
- package/tests/module_manifest.mjs +13 -4
- package/tests/module_store_publish.mjs +307 -0
- package/tests/module_store_publish_route.mjs +121 -0
- package/tests/module_store_registry_migration.mjs +70 -0
- package/tests/modules.mjs +56 -26
- package/tests/onboarding_route_signals.mjs +21 -31
- package/tests/onboarding_state.mjs +35 -49
- package/tests/permission_path.mjs +0 -3
- package/tests/platform_boot.mjs +1 -2
- package/tests/profile_route.mjs +1 -6
- package/tests/projects_hub.mjs +5 -5
- package/tests/projects_hub_app_step.mjs +138 -0
- package/tests/projects_hub_module_picker.mjs +2 -2
- package/tests/projects_hub_pre_uat.mjs +4 -5
- package/tests/provision.mjs +7 -7
- package/tests/provisioning_starter_bundles.mjs +0 -22
- package/tests/publish_branch_route.mjs +10 -18
- package/tests/publish_manifest.mjs +1 -2
- package/tests/rank_tier_single_source.mjs +1 -13
- package/tests/repo_map.mjs +3 -4
- package/tests/runner_drift.mjs +3 -21
- package/tests/sandbox_stage.mjs +57 -390
- package/tests/session_rename_fallback.mjs +0 -21
- package/tests/ship_error_shape.mjs +1 -1
- package/tests/task_visual_slots.mjs +31 -0
- package/tests/terms_acceptance.mjs +1 -1
- package/tests/upgrade.mjs +4 -4
- package/tests/watch_sealed_floor.mjs +0 -1
- package/tests/wizard_draft_resume.mjs +2 -2
- package/tests/wizard_intent_resume.mjs +21 -10
- package/tests/wizard_preselect_why.mjs +18 -18
- package/docs/onboarding/browser-terminal-guide.md +0 -73
- package/docs/recipes/managed-settings-remote-control.md +0 -416
- package/modules/dev-box/CLAUDE.md +0 -15
- package/modules/dev-box/box-access.js +0 -623
- package/modules/dev-box/box-credential.js +0 -94
- package/modules/dev-box/box-onboard.js +0 -486
- package/modules/dev-box/boxes.js +0 -1038
- package/modules/dev-box/db.js +0 -39
- package/modules/dev-box/module.json +0 -19
- package/modules/dev-box/routes/box.js +0 -1223
- package/scripts/gds/box-auth-check.js +0 -160
- package/scripts/gds/box-infra.js +0 -321
- package/scripts/gds/box-sync.js +0 -216
- package/scripts/gds/box.js +0 -1332
- package/scripts/gds/cf-tunnel.js +0 -558
- package/scripts/gds/smoke-box.sh +0 -150
- package/scripts/gds/tree-preflight.js +0 -241
- package/src/bongos/app-pair.js +0 -172
- package/tests/app_pair.mjs +0 -168
- package/tests/box_access.mjs +0 -578
- package/tests/box_auth_check.mjs +0 -114
- package/tests/box_code_staleness.mjs +0 -129
- package/tests/box_connect_e2e.mjs +0 -196
- package/tests/box_cost.mjs +0 -70
- package/tests/box_credential.mjs +0 -125
- package/tests/box_dns_repoint.mjs +0 -153
- package/tests/box_env.mjs +0 -64
- package/tests/box_host_keys.mjs +0 -126
- package/tests/box_infra_resolve.mjs +0 -246
- package/tests/box_onboard.mjs +0 -317
- package/tests/box_scope_session.mjs +0 -119
- package/tests/box_sweep_docs.mjs +0 -119
- package/tests/box_sync.mjs +0 -55
- package/tests/box_sync_scope_report.mjs +0 -126
- package/tests/box_task_scope.mjs +0 -169
- package/tests/box_task_scope_pause.mjs +0 -66
- package/tests/box_terminal_ssrf.mjs +0 -96
- package/tests/boxes.mjs +0 -1585
- package/tests/cf_ruleset_preflight.mjs +0 -142
- package/tests/cf_tunnel.mjs +0 -496
- package/tests/docs_scrubber_damage.mjs +0 -111
- package/tests/module_scope_active_claims.mjs +0 -94
- package/tests/ship_api_client_pathspec.mjs +0 -100
- package/tests/tree_preflight.mjs +0 -243
package/src/bongos/db.js
CHANGED
|
@@ -26,10 +26,9 @@
|
|
|
26
26
|
|
|
27
27
|
const { pool } = require('./pool');
|
|
28
28
|
const secretBox = require('./secret-box');
|
|
29
|
-
// Module seam (ADR 0083
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
// of the module.
|
|
29
|
+
// Module seam (ADR 0083). The builder-exit path EMITS `builder.secrets-scrub-needed`
|
|
30
|
+
// AFTER COMMIT (best-effort) so a module that stores a builder-keyed secret can
|
|
31
|
+
// clear its own store — never a direct require of the module.
|
|
33
32
|
const seams = require('../module-seams');
|
|
34
33
|
|
|
35
34
|
// Kernel db slice (BV1.R69 / ADR 0091 §2): builder IDENTITY (getBuilderById),
|
|
@@ -123,11 +122,6 @@ async function linkAnthropicAccount(builderId, { email, userId }) {
|
|
|
123
122
|
// setBuilderMonthlyBudget() carved into modules/economy/cost.js (BV1.R77 / ADR
|
|
124
123
|
// 0093 §1-2). routes/builders.js resolves it via the `reward` kernel port.
|
|
125
124
|
|
|
126
|
-
// setBuilderBoxBlocked + its box_blocked UPDATE RELOCATED to modules/dev-box/db.js
|
|
127
|
-
// (BV1.R68 / ADR 0091 §3): box-blocked is cleanly dev-box-owned — the module is
|
|
128
|
-
// its only writer AND reader (core never reads box_blocked) — so the query left
|
|
129
|
-
// the core monolith with the doorway re-export when the slice pattern landed.
|
|
130
|
-
|
|
131
125
|
// Approval-gated entry (2026-05-30). The web hall sign-in no longer auto-creates
|
|
132
126
|
// an account for any GitHub user — a first-time visitor must be approved first
|
|
133
127
|
// (an Archon marks their access_request 'invited'). These three small lookups
|
|
@@ -446,14 +440,6 @@ async function setBuilderRank({ builderId, newRank, actorBuilderId }) {
|
|
|
446
440
|
const result = await withTx(async (client) => {
|
|
447
441
|
return _applyRankChangeTx(client, { builderId, newRank, actorBuilderId, enforceSingleRungPromotion: true });
|
|
448
442
|
});
|
|
449
|
-
// Source-access clawback (ADR 0031 §6 / #600). Post-commit, own connection,
|
|
450
|
-
// never throws — records the box reconcile this rank change implies (a demotion
|
|
451
|
-
// below the floor → deprovision; a graduation up → rescope). The CREDENTIAL
|
|
452
|
-
// layer is already cut off live on the next /box/source-access fetch; this
|
|
453
|
-
// just signals + audits the slower droplet-level reconcile.
|
|
454
|
-
await seams.emitAsync('builder.box-reconcile-needed', {
|
|
455
|
-
builderId, rank: newRank, status: result.status, actor: `rank-change:builder#${actorBuilderId}`,
|
|
456
|
-
});
|
|
457
443
|
return {
|
|
458
444
|
builder: result.builder,
|
|
459
445
|
previous_rank: result.previous_rank,
|
|
@@ -613,20 +599,11 @@ async function deactivateBuilder({ builderId, reason, actorBuilderId }) {
|
|
|
613
599
|
await deleteGithubToken({ builderId, p: client });
|
|
614
600
|
return { updated, activeClaims };
|
|
615
601
|
});
|
|
616
|
-
// Source-access clawback (ADR 0031 §6 / #600): a deactivated builder loses
|
|
617
|
-
// their box → loses source. Post-commit, own connection, never throws. The
|
|
618
|
-
// builder is now status='inactive' (rank forced to xenos above), so the
|
|
619
|
-
// reconcile resolves to 'deprovision'. Their next /box/source-access fetch is
|
|
620
|
-
// already 403'd (inactive) — this signals the droplet teardown for `box.js
|
|
621
|
-
// reconcile` / the dormant sweep.
|
|
622
|
-
await seams.emitAsync('builder.box-reconcile-needed', {
|
|
623
|
-
builderId, rank: 'xenos', status: 'inactive', actor: `builder-exit:builder#${actorBuilderId}`,
|
|
624
|
-
});
|
|
625
602
|
// PRIVACY (task 1003338 / ADR 0332): teardown port for a MODULE-owned
|
|
626
603
|
// builder-keyed secret (today: art-pipeline's Gemini BYOK key,
|
|
627
604
|
// gemini_key_enc/last4/set_at — CRUD moved out of core under ADR 0102, so
|
|
628
|
-
// core does not touch those columns directly). Best-effort,
|
|
629
|
-
//
|
|
605
|
+
// core does not touch those columns directly). Best-effort, post-commit:
|
|
606
|
+
// a module that stores such a secret listens
|
|
630
607
|
// for this event and clears its own store; on a vanilla instance (no
|
|
631
608
|
// listener registered) it is a no-op.
|
|
632
609
|
await seams.emitAsync('builder.secrets-scrub-needed', {
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
// regex, so wildcards would never match. Every PROTECTED_GLOBS entry is likewise
|
|
21
21
|
// an exact path or dir-prefix, so the overlap is exact.
|
|
22
22
|
//
|
|
23
|
-
// FIRST CUT, auto-tightening. We are mid-strangler (ADR 0083):
|
|
23
|
+
// FIRST CUT, auto-tightening. We are mid-strangler (ADR 0083): some domains have
|
|
24
24
|
// moved into modules/<key>/; the rest still live fused in src/bongos/. So a module's
|
|
25
25
|
// globs list WHERE ITS CODE LIVES TODAY plus its modules/<key>/ home, and the
|
|
26
26
|
// `protected` result reflects the current tree (most core domains are protected
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
// modules + kernel from docs/design/modular-architecture/02-module-map.md §6.
|
|
31
31
|
//
|
|
32
32
|
// RELATION to src/modules.js: that registry tracks only the OPTIONAL feature
|
|
33
|
-
// modules (game/art-pipeline/discord
|
|
33
|
+
// modules (game/art-pipeline/discord) an instance toggles. This map is a
|
|
34
34
|
// SUPERSET — every module including the always-on core domains — because the goal
|
|
35
35
|
// scope wall must place ALL work, not just the optional features.
|
|
36
36
|
|
|
@@ -316,7 +316,7 @@ const MODULE_GLOBS = {
|
|
|
316
316
|
// (files-are-source — the committed definitions ARE the input agents-sync
|
|
317
317
|
// upserts), listed alongside it on the autonomy precedent, which likewise
|
|
318
318
|
// owns its '.claude/scheduled-tasks/' definition dir. The sync CLI travels
|
|
319
|
-
// with the domain but stays under scripts/gds/ (the
|
|
319
|
+
// with the domain but stays under scripts/gds/ (the provisioning
|
|
320
320
|
// precedent). Without this key the three Agents goals (1000038/39/40) could
|
|
321
321
|
// declare no wall at all (task 1002736).
|
|
322
322
|
agents: [
|
|
@@ -342,7 +342,7 @@ const MODULE_GLOBS = {
|
|
|
342
342
|
'npm-release': ['modules/npm-release/', 'tests/npm_release_*.mjs'],
|
|
343
343
|
// --- instance provisioning: the web-tier REQUEST + STATE surface (module home)
|
|
344
344
|
// plus the control-plane runner CLI — a command travels with its domain (the
|
|
345
|
-
// lifecycle
|
|
345
|
+
// lifecycle precedent). Covers the projects-surface work (goal
|
|
346
346
|
// 1000059); previously NO key resolved these paths, so no goal could wall
|
|
347
347
|
// them (task 1002690). ---
|
|
348
348
|
provisioning: [
|
|
@@ -355,32 +355,17 @@ const MODULE_GLOBS = {
|
|
|
355
355
|
// core/CLI-side under scripts/discord/.
|
|
356
356
|
'scripts/discord/', 'modules/discord/',
|
|
357
357
|
],
|
|
358
|
-
// ---
|
|
359
|
-
//
|
|
360
|
-
// (
|
|
361
|
-
//
|
|
362
|
-
//
|
|
363
|
-
//
|
|
364
|
-
//
|
|
365
|
-
//
|
|
366
|
-
|
|
367
|
-
// listed here yet — they come under this key when de-vendored. (ADR 0150 /
|
|
368
|
-
// task 1002386 made box.js RESOLVE infra/ from the instance root and fail
|
|
369
|
-
// loudly when it is missing; de-vendoring infra/ into core is still open.)
|
|
370
|
-
'dev-box': [
|
|
371
|
-
'modules/dev-box/', 'devbox-app/',
|
|
372
|
-
'scripts/gds/box.js', 'scripts/gds/box-auth-check.js', 'scripts/gds/box-connect-lib.js',
|
|
373
|
-
'scripts/gds/smoke-box.sh',
|
|
374
|
-
'scripts/gds/box-sync.js', // task 1004024: split out of box.js, now protected
|
|
375
|
-
|
|
376
|
-
// ADOPTED (task 1004111): the box BRING-UP surface. box.js (already here)
|
|
377
|
-
// orchestrates these five provisioning CLIs, and infra/ is the shell-script
|
|
378
|
-
// tree it resolves from the instance root — the comment above has described
|
|
379
|
-
// infra/ as dev-box's since task 1002386 without the glob saying so.
|
|
358
|
+
// --- the operator CONTROL PLANE: the provisioning CLIs that hold the DigitalOcean /
|
|
359
|
+
// Cloudflare credentials, and an instance's host topology under infra/. Not a
|
|
360
|
+
// module (nothing under modules/ carries it) — a scope like kernel or devsecops.
|
|
361
|
+
// Its own key rather than provisioning's, deliberately: every path here is a
|
|
362
|
+
// PROTECTED surface, so listing them under provisioning would make that module
|
|
363
|
+
// derive protected:true and put its open goals (1000106) behind an Archon join —
|
|
364
|
+
// the effect tests/government_protected_surfaces.mjs pins against. Given this key
|
|
365
|
+
// by task 1003892 (their previous owner was retired).
|
|
366
|
+
'control-plane': [
|
|
380
367
|
'scripts/gds/provision-config.js',
|
|
381
368
|
'scripts/gds/provision-net.js',
|
|
382
|
-
'scripts/gds/box-infra.js',
|
|
383
|
-
'scripts/gds/cf-tunnel.js',
|
|
384
369
|
'scripts/gds/do-api.js',
|
|
385
370
|
'infra/',
|
|
386
371
|
],
|
|
@@ -709,7 +694,7 @@ function scopeIncludesProtected(keys) {
|
|
|
709
694
|
//
|
|
710
695
|
// WHAT THIS IS AND IS NOT FOR. This narrows ONE consumer: the ship-time module-
|
|
711
696
|
// drift advisory (moduleDriftFiles below), whose question is "you said game, did
|
|
712
|
-
// you touch
|
|
697
|
+
// you touch discord?" — module-to-module confusion. It does NOT touch the goal
|
|
713
698
|
// scope wall, claim collisions (touchesOverlap), or the protected-path wall.
|
|
714
699
|
// Those keep counting methodology's territory as methodology's.
|
|
715
700
|
//
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// src/bongos/module-store.js — the module store's write side: keep a published
|
|
2
|
+
// module version's tarball and record it in the registry (task 1004271, ADR 0338 D1).
|
|
3
|
+
//
|
|
4
|
+
// The route (POST /store/modules/:key/versions in routes/modules.js) verifies the
|
|
5
|
+
// upload with scripts/gds/module-artifact.js first; this file only persists what has
|
|
6
|
+
// already passed. Two halves, in this order:
|
|
7
|
+
//
|
|
8
|
+
// 1. stageArtifact — write the tarball under the store dir (a temp name first;
|
|
9
|
+
// commitArtifact moves it into place inside step 2's transaction). ADR 0338 lets the
|
|
10
|
+
// store keep tarballs on the control plane's disk while volume is small, so it
|
|
11
|
+
// does: <MODULE_STORE_DIR or var/module-store>/<key>/<key>-<version>.tgz. Outside
|
|
12
|
+
// the git tree, like var/task-visuals, so a deploy's `git reset --hard` never
|
|
13
|
+
// wipes it.
|
|
14
|
+
// 2. publishVersion — one transaction over the registry tables (core_257): create
|
|
15
|
+
// the store_modules row on a key's first publish (the publisher becomes its
|
|
16
|
+
// author), refuse anyone else's key, refuse a delisted key (ADR 0338 D2: delisting
|
|
17
|
+
// stops what comes next), refuse a version that already exists or is not newer
|
|
18
|
+
// than the latest, then INSERT the version row. Rows are only ever INSERTed.
|
|
19
|
+
//
|
|
20
|
+
// If step 2 refuses, the route removes the temp file step 1 wrote, so a refused
|
|
21
|
+
// publish leaves nothing behind.
|
|
22
|
+
|
|
23
|
+
const fs = require('node:fs');
|
|
24
|
+
const path = require('node:path');
|
|
25
|
+
const crypto = require('node:crypto');
|
|
26
|
+
const { parseVersion, cmp } = require('../module-loader/semver');
|
|
27
|
+
const { KEY_RE } = require('../module-loader/manifest-schema');
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
function storeDir() {
|
|
31
|
+
return process.env.MODULE_STORE_DIR || path.join(__dirname, '..', '..', 'var', 'module-store');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Where one version's tarball lives. Key and version are re-validated here so this
|
|
35
|
+
// is the path-traversal defense on its own, whatever the caller checked.
|
|
36
|
+
function artifactPath(key, version, { dir = storeDir() } = {}) {
|
|
37
|
+
if (!KEY_RE.test(String(key)) || !parseVersion(String(version))) {
|
|
38
|
+
throw new Error('artifactPath: a bad module key or version');
|
|
39
|
+
}
|
|
40
|
+
return path.join(dir, key, `${key}-${version}.tgz`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Write the tarball to a private temp name beside its final place and return both
|
|
44
|
+
// paths. It becomes the real file only through commitArtifact, which publishVersion
|
|
45
|
+
// runs just before COMMIT — so a crash anywhere leaves at most a temp file (or a
|
|
46
|
+
// final file with no row, which the next publish of that version simply replaces),
|
|
47
|
+
// never a row pointing at nothing and never a stray file that blocks a retry.
|
|
48
|
+
// Async throughout so a multi-MB write never holds the web process's event loop.
|
|
49
|
+
async function stageArtifact(buf, { key, version, dir = storeDir() } = {}) {
|
|
50
|
+
const final = artifactPath(key, version, { dir });
|
|
51
|
+
await fs.promises.mkdir(path.dirname(final), { recursive: true });
|
|
52
|
+
const tmp = `${final}.${process.pid}.${Date.now()}.${crypto.randomBytes(4).toString('hex')}.partial`;
|
|
53
|
+
await fs.promises.writeFile(tmp, buf, { flag: 'wx', mode: 0o640 });
|
|
54
|
+
return { tmp, final };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function commitArtifact({ tmp, final }) {
|
|
58
|
+
await fs.promises.rename(tmp, final);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function removeArtifact(file) {
|
|
62
|
+
try { await fs.promises.unlink(file); } catch { /* already gone — nothing to undo */ }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// The store keeps artifact_path relative to the store dir, so moving the dir (or
|
|
66
|
+
// moving to object storage later) is a config change, not a data migration.
|
|
67
|
+
function relativeArtifactPath(file, { dir = storeDir() } = {}) {
|
|
68
|
+
return path.relative(dir, file).split(path.sep).join('/');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// One transaction. `db` is a pg Pool (anything with connect()). Returns
|
|
72
|
+
// { ok: true, version: row, created: bool } or { ok: false, code, message, status }.
|
|
73
|
+
async function publishVersion(input, { db, beforeCommit } = {}) {
|
|
74
|
+
const {
|
|
75
|
+
key, title, description, version, coreVersion, manifest,
|
|
76
|
+
treeSha256, tarballSha256, bytes, artifactPath: relPath, builderId,
|
|
77
|
+
} = input;
|
|
78
|
+
const pool = db || require('./pool').pool;
|
|
79
|
+
const client = await pool.connect();
|
|
80
|
+
const refuse = async (status, code, message) => {
|
|
81
|
+
await client.query('ROLLBACK');
|
|
82
|
+
return { ok: false, status, code, message };
|
|
83
|
+
};
|
|
84
|
+
try {
|
|
85
|
+
await client.query('BEGIN');
|
|
86
|
+
const { rows: [mod] } = await client.query(
|
|
87
|
+
'SELECT module_key, author_id, status FROM store_modules WHERE module_key = $1 FOR UPDATE', [key]);
|
|
88
|
+
let created = false;
|
|
89
|
+
if (!mod) {
|
|
90
|
+
await client.query(
|
|
91
|
+
'INSERT INTO store_modules (module_key, title, description, author_id) VALUES ($1, $2, $3, $4)',
|
|
92
|
+
[key, title, description || '', builderId]);
|
|
93
|
+
created = true;
|
|
94
|
+
} else {
|
|
95
|
+
if (String(mod.author_id) !== String(builderId)) {
|
|
96
|
+
return refuse(403, 'not_module_author', `"${key}" belongs to another author — only its author can publish new versions`);
|
|
97
|
+
}
|
|
98
|
+
if (mod.status === 'delisted') {
|
|
99
|
+
return refuse(409, 'module_delisted', `"${key}" is delisted — it takes no new versions (ADR 0338 D2)`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Every version of the key, compared in JS: semver order is numeric per part, so
|
|
104
|
+
// a text ORDER BY would rank 1.10.0 below 1.9.0. A module's version count is small.
|
|
105
|
+
const { rows: existing } = await client.query(
|
|
106
|
+
'SELECT version FROM store_module_versions WHERE module_key = $1', [key]);
|
|
107
|
+
const wanted = parseVersion(version);
|
|
108
|
+
if (existing.some((r) => r.version === version)) {
|
|
109
|
+
return refuse(409, 'version_exists', `${key} ${version} is already published — a version never changes; bump module.json's version`);
|
|
110
|
+
}
|
|
111
|
+
const newest = existing.map((r) => parseVersion(r.version)).filter(Boolean).sort(cmp).pop();
|
|
112
|
+
if (newest && cmp(wanted, newest) <= 0) {
|
|
113
|
+
return refuse(409, 'version_not_newer', `${key} ${version} is not newer than the latest published ${newest.join('.')}`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const { rows: [row] } = await client.query(
|
|
117
|
+
`INSERT INTO store_module_versions
|
|
118
|
+
(module_key, version, core_version, manifest, tree_sha256, tarball_sha256,
|
|
119
|
+
tarball_bytes, artifact_path, published_by)
|
|
120
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
|
121
|
+
RETURNING id, module_key, version, core_version, tree_sha256, tarball_sha256,
|
|
122
|
+
tarball_bytes, published_by, published_at`,
|
|
123
|
+
[key, version, coreVersion, manifest, treeSha256, tarballSha256, bytes, relPath, builderId]);
|
|
124
|
+
// Put the tarball in place while the row is still uncommitted: if this throws,
|
|
125
|
+
// the catch below rolls the row back, so the two can never disagree.
|
|
126
|
+
if (beforeCommit) await beforeCommit();
|
|
127
|
+
await client.query('COMMIT');
|
|
128
|
+
return { ok: true, created, version: row };
|
|
129
|
+
} catch (e) {
|
|
130
|
+
try { await client.query('ROLLBACK'); } catch { /* the connection may be gone */ }
|
|
131
|
+
// Two publishes of the same version racing past the check: the UNIQUE wins.
|
|
132
|
+
if (e && e.code === '23505') return { ok: false, status: 409, code: 'version_exists', message: `${key} ${version} is already published` };
|
|
133
|
+
throw e;
|
|
134
|
+
} finally {
|
|
135
|
+
client.release();
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
module.exports = {
|
|
140
|
+
stageArtifact, commitArtifact, removeArtifact, relativeArtifactPath, publishVersion,
|
|
141
|
+
};
|
|
@@ -221,9 +221,7 @@ const EXPECTED_RANKS = {
|
|
|
221
221
|
// criterion confirmation, and gate-approvals — moved to MODULE_EXPECTED_RANKS
|
|
222
222
|
// ['lifecycle'] below in BV1.R86. They live in the lifecycle module now
|
|
223
223
|
// (modules/lifecycle/routes/…), so they're verified only when that module is in
|
|
224
|
-
// scope, not unconditionally from this core list — the pins follow the routes
|
|
225
|
-
// the dev-box precedent.)
|
|
226
|
-
// (dev-box fleet pins likewise live in MODULE_EXPECTED_RANKS below — BV1.R43.)
|
|
224
|
+
// scope, not unconditionally from this core list — the pins follow the routes.)
|
|
227
225
|
// security engagement docs — exploit-detail report store (task 991). Pinned
|
|
228
226
|
// because a silent downgrade here leaks the attacker cookbook below the
|
|
229
227
|
// trust line. (security-the-PROPERTY stays kernel — routes/security.js.)
|
|
@@ -277,10 +275,8 @@ const GOVERNMENT_PINS = {
|
|
|
277
275
|
// when that module is in scope (its routes were discovered in this scan). So an
|
|
278
276
|
// instance that doesn't ship the module — or a hermetic test isolating a
|
|
279
277
|
// different module — never trips a false "vanished" alarm, while a real rank
|
|
280
|
-
// downgrade or a removal IS still caught wherever the module is present
|
|
281
|
-
//
|
|
282
|
-
// follow the routes; the intent is unchanged: closing/blocking another builder's
|
|
283
|
-
// paid infra, and reading the fleet/cost roster, stay archon-only.)
|
|
278
|
+
// downgrade or a removal IS still caught wherever the module is present — the pins
|
|
279
|
+
// follow the routes (BV1.R43 / task 1412 moved the first set here).
|
|
284
280
|
//
|
|
285
281
|
// The `ideas` / `memory` / `security` blocks were added in BV1.R109b: their routes are
|
|
286
282
|
// declared multi-line and so were invisible to this auditor until it learned that form
|
|
@@ -293,15 +289,6 @@ const GOVERNMENT_PINS = {
|
|
|
293
289
|
// (metic), `security.report.adjudicate` / `idea.ratify` / `memory.read.any` (archon) —
|
|
294
290
|
// so tightening these to `perm:` pins is a clean follow-up once R104-R107 have landed.
|
|
295
291
|
const MODULE_EXPECTED_RANKS = {
|
|
296
|
-
'dev-box': {
|
|
297
|
-
// ADR 0157 lowered `box.fleet.manage` archon→metic (operating paid infra is admin
|
|
298
|
-
// work, not an authority grant). Re-pinned by ATOM so the guarded invariant is
|
|
299
|
-
// "these gate on the fleet permission", independent of its floor.
|
|
300
|
-
'GET /boxes': 'perm:box.fleet.manage',
|
|
301
|
-
'GET /boxes/cost-ledger': 'perm:box.fleet.manage',
|
|
302
|
-
'POST /boxes/:builderId/close': 'perm:box.fleet.manage',
|
|
303
|
-
'PATCH /boxes/:builderId/block': 'perm:box.fleet.manage',
|
|
304
|
-
},
|
|
305
292
|
// The lifecycle module's privileged routes (carved BV1.R86 — the pins follow the
|
|
306
293
|
// routes from EXPECTED_RANKS). Verified only when the lifecycle module is in scope.
|
|
307
294
|
lifecycle: {
|
|
@@ -13,7 +13,6 @@
|
|
|
13
13
|
// - /auth/discord/* LINKING an existing builder to a Discord id for role sync.
|
|
14
14
|
// It is NOT a way to sign in — Discord sits OUTSIDE the trust
|
|
15
15
|
// boundary (ADR 0032), so it can never mint a session.
|
|
16
|
-
// - /auth/app-pair/* the Dev Box desktop app pairing to its builder (ADR 0045).
|
|
17
16
|
//
|
|
18
17
|
// TWO INVARIANTS worth keeping in view before editing:
|
|
19
18
|
// - `/auth/cli-token/issue` mints a CLI bearer from a browser COOKIE, never from a
|
|
@@ -36,7 +35,6 @@ const { reportActivityRollupForShippedTask } = require('../auth-admission.js');
|
|
|
36
35
|
// endpoint refuses. Same composed answer the gate itself reads.
|
|
37
36
|
const { effectiveJoinDoor } = require('../project-door.js');
|
|
38
37
|
const db = require('../db');
|
|
39
|
-
const { pairStore } = require('../app-pair');
|
|
40
38
|
const { publicOrigin, parseCookie, validateOrRespond } = require('./_helpers');
|
|
41
39
|
const { accountExistenceReadRateLimit } = require('../middleware/rate-limit');
|
|
42
40
|
const { branding } = require('../../branding');
|
|
@@ -1162,120 +1160,6 @@ module.exports = function buildAuthRouter() {
|
|
|
1162
1160
|
}
|
|
1163
1161
|
});
|
|
1164
1162
|
|
|
1165
|
-
// /auth/app-pair — browser-clicks-only sign-in for the Dev Box desktop app
|
|
1166
|
-
// (#904, ADR 0045). The app starts a pairing, opens the browser at
|
|
1167
|
-
// /builders/pair?code=…, the signed-in builder clicks ONE Approve button,
|
|
1168
|
-
// and the app collects a freshly-minted source='cli' session over its own
|
|
1169
|
-
// secret poll channel. Design + threat model in src/bongos/app-pair.js.
|
|
1170
|
-
//
|
|
1171
|
-
// Route order note: the two PUBLIC routes here sit so no requireBuilder/
|
|
1172
|
-
// requireRank from a NEIGHBORING declaration falls inside route-rank-check's
|
|
1173
|
-
// 20-line look-ahead — the deterministic auditor must classify them from
|
|
1174
|
-
// their own `rank:` annotations, not a false middleware hit.
|
|
1175
|
-
|
|
1176
|
-
// rank: public — starts a pairing; returns opaque codes only, no identity.
|
|
1177
|
-
router.post('/auth/app-pair/start', (req, res) => {
|
|
1178
|
-
if (validateOrRespond(req, res, {
|
|
1179
|
-
device_name: { type: 'string', maxLength: 80 },
|
|
1180
|
-
platform: { type: 'string', maxLength: 20 },
|
|
1181
|
-
})) return;
|
|
1182
|
-
const body = req.body || {};
|
|
1183
|
-
const out = pairStore.start({ deviceName: body.device_name, platform: body.platform });
|
|
1184
|
-
if (out.error === 'busy') {
|
|
1185
|
-
return res.fail('pairing_busy', { status: 429, message: 'Too many pairings in flight. Try again in a few minutes.' });
|
|
1186
|
-
}
|
|
1187
|
-
// The approve-page URL is PINNED to the canonical public origin (same
|
|
1188
|
-
// posture as the OAuth redirect_uris above) — never derived from the
|
|
1189
|
-
// request host, so a spoofed Host header can't point the app's browser
|
|
1190
|
-
// hand-off at a look-alike approve page.
|
|
1191
|
-
const verifyPath = `/builders/pair?code=${out.pairCode}`;
|
|
1192
|
-
res.status(201).json({
|
|
1193
|
-
pair_code: out.pairCode,
|
|
1194
|
-
poll_token: out.pollToken,
|
|
1195
|
-
verify_path: verifyPath,
|
|
1196
|
-
verify_url: `${WEB_REDIRECT_ORIGIN}${verifyPath}`,
|
|
1197
|
-
expires_in: out.expiresIn,
|
|
1198
|
-
interval: out.interval,
|
|
1199
|
-
});
|
|
1200
|
-
});
|
|
1201
|
-
|
|
1202
|
-
// rank: any-builder — read-only: what the approve page shows BEFORE the
|
|
1203
|
-
// click (device name/platform for the given code). Unknown and expired are
|
|
1204
|
-
// indistinguishable (404) so the code space can't be probed for liveness.
|
|
1205
|
-
router.get('/auth/app-pair/info', auth.requireBuilder, (req, res) => {
|
|
1206
|
-
const info = pairStore.info(req.query.code);
|
|
1207
|
-
if (!info) return res.fail('unknown_or_expired', 404);
|
|
1208
|
-
res.json({
|
|
1209
|
-
pair_code: info.pairCode,
|
|
1210
|
-
device_name: info.deviceName,
|
|
1211
|
-
platform: info.platform,
|
|
1212
|
-
status: info.status,
|
|
1213
|
-
});
|
|
1214
|
-
});
|
|
1215
|
-
|
|
1216
|
-
// rank: any-builder (own identity only) — approving signs the app in AS THE
|
|
1217
|
-
// APPROVER. Cookie-gated exactly like /auth/cli-token/issue above: a leaked
|
|
1218
|
-
// CLI bearer must not be able to approve pairings and mint itself siblings;
|
|
1219
|
-
// only the httpOnly browser cookie — the click in the hall — can.
|
|
1220
|
-
router.post('/auth/app-pair/approve', auth.requireBuilder, async (req, res) => {
|
|
1221
|
-
const cookieToken = sessionCookieToken(req);
|
|
1222
|
-
if (!cookieToken) {
|
|
1223
|
-
return res.fail('cookie_required', { status: 403, message: 'Pairings are approved from an authenticated browser session in the builders’ hall, not from a bearer token.' });
|
|
1224
|
-
}
|
|
1225
|
-
if (validateOrRespond(req, res, {
|
|
1226
|
-
pair_code: { required: true, type: 'string', maxLength: 32 },
|
|
1227
|
-
})) return;
|
|
1228
|
-
try {
|
|
1229
|
-
const token = crypto.randomBytes(32).toString('hex');
|
|
1230
|
-
const row = await db.createSession({
|
|
1231
|
-
builderId: req.builder.id,
|
|
1232
|
-
token,
|
|
1233
|
-
source: 'cli', // 003_pms.sql CHECK; the app is a CLI-class bearer client
|
|
1234
|
-
userAgent: `devbox-app pair (${req.headers['user-agent'] ?? 'unknown'})`,
|
|
1235
|
-
expiresAt: null, // no expiry — a CLI bearer lasts until revoked (ADR 0294)
|
|
1236
|
-
});
|
|
1237
|
-
const out = pairStore.approve((req.body || {}).pair_code, {
|
|
1238
|
-
token,
|
|
1239
|
-
expires_at: row.expires_at,
|
|
1240
|
-
builder: {
|
|
1241
|
-
id: req.builder.id,
|
|
1242
|
-
github_login: req.builder.github_login,
|
|
1243
|
-
display_name: req.builder.display_name,
|
|
1244
|
-
rank: req.builder.rank,
|
|
1245
|
-
},
|
|
1246
|
-
});
|
|
1247
|
-
if (!out.ok) {
|
|
1248
|
-
// The pairing was gone or already approved — revoke the session we
|
|
1249
|
-
// just minted so an orphaned token never outlives this branch.
|
|
1250
|
-
await db.revokeSession(token);
|
|
1251
|
-
return out.error === 'already_approved'
|
|
1252
|
-
? res.fail('already_approved', 409)
|
|
1253
|
-
: res.fail('unknown_or_expired', 404);
|
|
1254
|
-
}
|
|
1255
|
-
res.json({ ok: true });
|
|
1256
|
-
} catch (err) {
|
|
1257
|
-
log.error('[gds] POST /auth/app-pair/approve', err);
|
|
1258
|
-
res.fail('approve_failed', { status: 500, message: 'internal error' });
|
|
1259
|
-
}
|
|
1260
|
-
});
|
|
1261
|
-
|
|
1262
|
-
// rank: public — poll with the secret poll_token; the token IS the bearer
|
|
1263
|
-
// (mirrors /auth/device/poll, where the device_code is the bearer). Kept
|
|
1264
|
-
// LAST in this router so route-rank-check's look-ahead window stays clear
|
|
1265
|
-
// of any neighboring gated declaration (see the block comment above).
|
|
1266
|
-
router.post('/auth/app-pair/poll', (req, res) => {
|
|
1267
|
-
if (validateOrRespond(req, res, {
|
|
1268
|
-
poll_token: { required: true, type: 'string', maxLength: 128 },
|
|
1269
|
-
})) return;
|
|
1270
|
-
const out = pairStore.poll((req.body || {}).poll_token);
|
|
1271
|
-
if (out.status === 'approved') {
|
|
1272
|
-
return res.json({ status: 'approved', ...out.grant });
|
|
1273
|
-
}
|
|
1274
|
-
res.json(out.status === 'pending'
|
|
1275
|
-
? { status: 'pending', interval: out.interval }
|
|
1276
|
-
: { status: 'expired' });
|
|
1277
|
-
});
|
|
1278
|
-
|
|
1279
1163
|
return router;
|
|
1280
1164
|
};
|
|
1281
1165
|
|
|
@@ -82,14 +82,6 @@ function staleHoursFor(req, res) {
|
|
|
82
82
|
// kernel PORT (lc()). The rank/identity mutations (setBuilderRank/deactivate/
|
|
83
83
|
// reactivate) stay core db (this domain is builder identity, not the lifecycle).
|
|
84
84
|
const lc = () => seams.resolveOptional('lifecycle');
|
|
85
|
-
// Dev-box "has this builder ever connected a box?" read through the kernel seam
|
|
86
|
-
// instead of a direct `require('../boxes')` (a forbidden core→module edge once
|
|
87
|
-
// dev-box moved to modules/dev-box/, ADR 0083 / BV1.R43). The dev-box module
|
|
88
|
-
// provides the `box.hasEverConnected` port when enabled; degrades to false on a
|
|
89
|
-
// box-less instance (the box_connect onboarding step just never auto-clears).
|
|
90
|
-
const boxHasEverConnected = (builderId) =>
|
|
91
|
-
seams.resolveOptional('box.hasEverConnected', async () => false)(builderId);
|
|
92
|
-
|
|
93
85
|
// The discord "has this builder linked their Discord account?" signal, read
|
|
94
86
|
// through the kernel seam instead of a direct `db.getDiscordLink` (a forbidden
|
|
95
87
|
// core→module edge once discord moved to modules/discord/, ADR 0083 / BV1.R47).
|
|
@@ -470,22 +462,20 @@ module.exports = function buildBuildersRouter() {
|
|
|
470
462
|
// never a direct require of a module. Default-on (present in prod); degrades
|
|
471
463
|
// to a null state row (shapeForApi synthesizes the "nothing cleared" view).
|
|
472
464
|
const onboarding = seams.resolveOptional('onboarding');
|
|
473
|
-
const [state, shipCount, discordLink,
|
|
465
|
+
const [state, shipCount, discordLink, cliAuthed] = await Promise.all([
|
|
474
466
|
onboarding ? onboarding.getState(pool, targetId) : null,
|
|
475
467
|
lc().countShippedTasksForBuilder(targetId),
|
|
476
468
|
discordIsLinked(targetId),
|
|
477
|
-
boxHasEverConnected(targetId),
|
|
478
469
|
db.hasCliSession(targetId),
|
|
479
470
|
]);
|
|
480
471
|
// shipCount drives the terminal 'ship_three' stage's "N of 3" counter.
|
|
481
|
-
// The
|
|
472
|
+
// The discord/cli steps clear from LIVE signals (not the onboarding_state
|
|
482
473
|
// row), so they must be passed in exactly as GET /me does — otherwise they
|
|
483
|
-
// default to false and this route renders
|
|
474
|
+
// default to false and this route renders join_discord/cli_setup
|
|
484
475
|
// perpetually unchecked even after the builder cleared them (task 1444).
|
|
485
476
|
const shaped = onboarding ? onboarding.shapeForApi(state, {
|
|
486
477
|
shipCount,
|
|
487
478
|
discordLinked: !!discordLink,
|
|
488
|
-
boxConnected: !!boxConnected,
|
|
489
479
|
cliAuthed: !!cliAuthed,
|
|
490
480
|
}) : null;
|
|
491
481
|
// Light cache — onboarding state changes a few times per builder, ever.
|
|
@@ -510,7 +500,7 @@ module.exports = function buildBuildersRouter() {
|
|
|
510
500
|
// shareable without a sign-in wall (that IS the feature). It deliberately
|
|
511
501
|
// WHITELISTS the public column subset off the full builder row — the private
|
|
512
502
|
// columns getBuilderById also returns (anthropic_email / anthropic_user_id /
|
|
513
|
-
// monthly_budget_usd / system_role
|
|
503
|
+
// monthly_budget_usd / system_role) are NEVER spread into the
|
|
514
504
|
// JSON. A leak-prevention unit test (tests/profile_route.mjs) asserts their
|
|
515
505
|
// absence + the 404-on-unknown-id behaviour.
|
|
516
506
|
// THROTTLED since task 1002804. The global limiter meters WRITES only
|
|
@@ -554,7 +544,7 @@ module.exports = function buildBuildersRouter() {
|
|
|
554
544
|
]);
|
|
555
545
|
// WHITELIST — only the leaderboard-equivalent public field set. Never spread
|
|
556
546
|
// the raw row: it carries anthropic_email / anthropic_user_id /
|
|
557
|
-
// monthly_budget_usd / system_role /
|
|
547
|
+
// monthly_budget_usd / system_role / deactivated_reason,
|
|
558
548
|
// none of which are public.
|
|
559
549
|
const builder = {
|
|
560
550
|
id: b.id,
|
|
@@ -9,14 +9,12 @@
|
|
|
9
9
|
// * modules + disciplines — the R54 feature-module flags
|
|
10
10
|
// * rank ladder — the canonical live tiers + the instance's labels
|
|
11
11
|
// * repo binding — the GitHub owner/repo for `--local` clones
|
|
12
|
-
// * auth methods — how a stranger signs in (device-flow / web
|
|
12
|
+
// * auth methods — how a stranger signs in (device-flow / web)
|
|
13
13
|
// * admission policy — invite-only vs open self-enrollment
|
|
14
|
-
// * dev-box availability — whether the managed cloud-box backend is on
|
|
15
14
|
//
|
|
16
15
|
// WHY THIS EXISTS: GET /healthz returns only { ok, auth_configured } — too thin
|
|
17
|
-
// for a CLI to brand + feature-gate itself
|
|
18
|
-
//
|
|
19
|
-
// that gap calls for.
|
|
16
|
+
// for a CLI to brand + feature-gate itself against an instance's modules. This is
|
|
17
|
+
// the richer public descriptor that gap calls for.
|
|
20
18
|
//
|
|
21
19
|
// SECURITY (no secrets / no topology): the manifest is assembled ONLY from
|
|
22
20
|
// already-client-safe projections — clientBranding() (the documented allow-list
|
|
@@ -35,7 +33,7 @@
|
|
|
35
33
|
const express = require('express');
|
|
36
34
|
const { corsPublicGet, asyncHandler } = require('./_helpers');
|
|
37
35
|
const { clientBranding } = require('../../branding');
|
|
38
|
-
const { clientModules
|
|
36
|
+
const { clientModules } = require('../../modules');
|
|
39
37
|
const { loadRepoInfo } = require('../repo-info');
|
|
40
38
|
const { authConfigured, openEnrollmentEnabled, loadIdpConfig, resolveSession } = require('../auth');
|
|
41
39
|
const { RANK_TIER } = require('../permission-path-check');
|
|
@@ -49,15 +47,14 @@ const MANIFEST_SCHEMA = 'instance-manifest/v1';
|
|
|
49
47
|
|
|
50
48
|
// Supported sign-in methods. All require GitHub auth to be configured (the
|
|
51
49
|
// server holds the OAuth client creds): device-flow = the CLI Device Flow;
|
|
52
|
-
// web = the browsers' hall sign-in (completeWebFlow)
|
|
53
|
-
//
|
|
54
|
-
|
|
55
|
-
const AUTH_METHODS = ['device-flow', 'web', 'app-pair'];
|
|
50
|
+
// web = the browsers' hall sign-in (completeWebFlow). Both mount unconditionally
|
|
51
|
+
// in routes/auth.js, so configured-ness is the only gate.
|
|
52
|
+
const AUTH_METHODS = ['device-flow', 'web'];
|
|
56
53
|
|
|
57
54
|
// buildInstanceManifest — PURE assembler. Every input is injected so this is
|
|
58
55
|
// unit-testable without disk, DB, or git. Inputs are the CLIENT-SAFE projections
|
|
59
56
|
// (branding = clientBranding() output, modules = clientModules() output), the
|
|
60
|
-
// canonical rank-tier map, loadRepoInfo()'s result, and
|
|
57
|
+
// canonical rank-tier map, loadRepoInfo()'s result, and two booleans. Returns
|
|
61
58
|
// the JSON body verbatim.
|
|
62
59
|
function buildInstanceManifest({
|
|
63
60
|
branding,
|
|
@@ -66,7 +63,6 @@ function buildInstanceManifest({
|
|
|
66
63
|
repo,
|
|
67
64
|
authConfigured: authOk,
|
|
68
65
|
openEnrollment,
|
|
69
|
-
devBoxEnabled,
|
|
70
66
|
idp = null,
|
|
71
67
|
} = {}) {
|
|
72
68
|
const b = branding || {};
|
|
@@ -109,7 +105,6 @@ function buildInstanceManifest({
|
|
|
109
105
|
idp: idp || null,
|
|
110
106
|
},
|
|
111
107
|
admission: { policy: openEnrollment ? 'open' : 'invite-only' },
|
|
112
|
-
devBox: { available: !!devBoxEnabled },
|
|
113
108
|
};
|
|
114
109
|
}
|
|
115
110
|
|
|
@@ -144,7 +139,6 @@ function currentManifest() {
|
|
|
144
139
|
// what the gate will do — and 'invite-only' already covers every non-open
|
|
145
140
|
// door, so narrowing it names no state (ADR 0247 D3).
|
|
146
141
|
openEnrollment: openEnrollmentEnabled(),
|
|
147
|
-
devBoxEnabled: isModuleEnabled('dev-box'),
|
|
148
142
|
// Project OUT the client secret — the manifest is public; only origin +
|
|
149
143
|
// client_id (both public) advertise federation.
|
|
150
144
|
idp: idpCfg ? { origin: idpCfg.origin, client_id: idpCfg.clientId } : null,
|