@bongos/core 1.19.1073 → 1.19.1075
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 +404 -549
- package/.claude/skills/feedback/SKILL.md +2 -2
- package/bin/bongos.js +1 -3
- 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 +175 -1665
- 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 +4 -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 +10 -10
- 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/discord/ship-broadcast.js +3 -3
- package/modules/economy/credits.js +2 -2
- 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/ideas/projection.js +1 -1
- package/modules/lifecycle/db-claim-reads.js +1 -58
- package/modules/lifecycle/db-ship.js +4 -9
- package/modules/lifecycle/db.js +0 -4
- package/modules/lifecycle/github-push.js +12 -14
- 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/publish-reconciler.js +10 -10
- package/modules/lifecycle/routes/tasks.js +13 -15
- package/modules/lifecycle/ship-card.js +1 -1
- package/modules/lifecycle/task-visuals.js +2 -2
- package/modules/memory/module.json +1 -0
- package/modules/memory/routes/memory.js +2 -2
- 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 +5 -4
- package/modules/platform-identity/tests/platform-identity.mjs +17 -3
- package/modules/provisioning/capacity.js +1 -1
- package/modules/provisioning/module.json +1 -0
- package/modules/provisioning/provisioning.js +6 -5
- package/modules/provisioning/routes/provisioning.js +4 -4
- 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/db.js +1 -1
- 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 +84 -0
- package/scripts/gds/artifact-format.js +203 -0
- package/scripts/gds/artifact-staleness.js +2 -2
- package/scripts/gds/audit-authorship.sh +1 -13
- package/scripts/gds/backfill-task-rewards.js +5 -5
- package/scripts/gds/claim.js +0 -15
- package/scripts/gds/claude-materialize.js +2 -2
- package/scripts/gds/cli-lib.js +15 -15
- package/scripts/gds/codemod-rename-src-gds.js +1 -1
- package/scripts/gds/context-pack.js +5 -7
- 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 +4 -4
- package/scripts/gds/feedback-latest.js +139 -9
- package/scripts/gds/fitness-checks-identity.js +2 -7
- package/scripts/gds/fitness-checks-packaging.js +4 -4
- package/scripts/gds/fitness-checks-write-validation.js +4 -0
- package/scripts/gds/fitness-ratchets.js +5 -5
- package/scripts/gds/fitness.js +10 -10
- package/scripts/gds/gen-api-client.js +4 -4
- package/scripts/gds/gen-api-docs.js +1 -1
- package/scripts/gds/gen-diagrams.js +49 -71
- package/scripts/gds/gen-repo-map.js +13 -14
- package/scripts/gds/gen-session-index.js +8 -8
- package/scripts/gds/http-api-client.js +12 -19
- package/scripts/gds/init.js +1 -1
- package/scripts/gds/leak-scan-allowlist.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/plain-cards.js +1 -1
- 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/push-path-brief.js +10 -10
- package/scripts/gds/regen-instance-docs.js +1 -1
- package/scripts/gds/rename-history-check.js +1 -1
- package/scripts/gds/run-unit-tests.js +2 -16
- package/scripts/gds/sandbox-stage.js +39 -341
- package/scripts/gds/session-digest-build.js +2 -2
- package/scripts/gds/ship-deploy-target.js +13 -13
- package/scripts/gds/ship-flow.js +7 -7
- package/scripts/gds/ship-land.js +9 -10
- package/scripts/gds/ship-merge.js +4 -4
- package/scripts/gds/ship-preflight-steps.js +4 -4
- package/scripts/gds/ship-regen.js +15 -49
- package/scripts/gds/ship.js +13 -33
- package/scripts/gds/skill-preflight.js +9 -8
- 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/api-prefix.js +4 -4
- 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/platform-visibility-gate.js +1 -1
- 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 +7 -23
- package/src/bongos/routes/modules.js +83 -0
- package/src/bongos/routes/security.js +3 -3
- package/src/bongos/routes.js +7 -2
- package/src/bongos/serve-internal.js +5 -6
- package/src/bongos-downloads.js +1 -6
- package/src/branding.js +2 -2
- package/src/build-info.js +4 -6
- package/src/instance-config.js +2 -2
- 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 +16 -8
- package/src/module-seams.js +2 -3
- package/src/modules.js +42 -14
- package/tests/agents_spend_guard.mjs +1 -1
- package/tests/api_alias_caller_ratchet.mjs +1 -1
- package/tests/api_alias_exceptions.mjs +1 -1
- package/tests/api_client.mjs +1 -16
- package/tests/api_path_404.mjs +1 -1
- package/tests/auth_page_gate.mjs +10 -10
- package/tests/backfill_task_rewards.mjs +1 -1
- 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/{box_ship_permissions.mjs → cli_allowlist_permissions.mjs} +5 -5
- package/tests/cli_sessions.mjs +1 -1
- package/tests/consumer_layout_boot.mjs +2 -0
- package/tests/context_pack.mjs +13 -13
- package/tests/copy_desk_page_docx.mjs +33 -2
- package/tests/copy_inventory.mjs +7 -7
- package/tests/core_upgrade_runner.mjs +6 -6
- package/tests/credit_grant.mjs +2 -2
- package/tests/criterion_suggest.mjs +1 -1
- package/tests/deploy_divergence_line.mjs +11 -11
- package/tests/design_tokens_sync.mjs +3 -2
- 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/fitness_ratchets.mjs +2 -2
- package/tests/gate_approvals.mjs +0 -1
- package/tests/github_push_land_proof.mjs +1 -1
- package/tests/go_live.mjs +3 -3
- package/tests/goal_advisory.mjs +3 -3
- package/tests/goal_suggest.mjs +3 -3
- package/tests/government_abuse_matrix.mjs +21 -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_audit.mjs +1 -1
- 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/helpers.mjs +1 -1
- package/tests/host_topology_skips.mjs +3 -4
- 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/migration_namespace.mjs +6 -6
- package/tests/module-scope-map.mjs +9 -9
- package/tests/module_api.mjs +2 -2
- package/tests/module_catalog.mjs +1 -1
- package/tests/module_cli.mjs +20 -20
- package/tests/module_contributions.mjs +6 -6
- package/tests/module_loader.mjs +6 -6
- package/tests/module_manifest.mjs +26 -17
- package/tests/module_route_rank.mjs +2 -2
- 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/modules_route_wiring.mjs +1 -1
- package/tests/onboarding_route_signals.mjs +21 -31
- package/tests/onboarding_state.mjs +35 -49
- package/tests/paste_token_session_store.mjs +1 -1
- package/tests/permission_path.mjs +0 -3
- package/tests/platform_boot.mjs +1 -2
- package/tests/platform_visibility_gate.mjs +1 -1
- package/tests/profile_route.mjs +1 -6
- package/tests/project_modules_ui.mjs +17 -17
- package/tests/projects_hub.mjs +5 -5
- package/tests/projects_hub_app_step.mjs +138 -0
- package/tests/projects_hub_module_picker.mjs +77 -76
- package/tests/projects_hub_pre_uat.mjs +4 -5
- package/tests/provision.mjs +7 -7
- package/tests/provisioning_capacity.mjs +1 -1
- package/tests/provisioning_recommendations.mjs +2 -1
- package/tests/provisioning_starter_bundles.mjs +0 -22
- package/tests/publish_branch_route.mjs +11 -19
- package/tests/publish_manifest.mjs +2 -3
- package/tests/publish_reconciler.mjs +8 -8
- package/tests/push_path_brief.mjs +10 -9
- package/tests/rank_tier_single_source.mjs +1 -13
- package/tests/regrade_eligibility.mjs +1 -1
- package/tests/rename_history_restraint.mjs +1 -1
- package/tests/repo_map.mjs +12 -13
- package/tests/runner_drift.mjs +3 -21
- package/tests/sandbox_stage.mjs +57 -390
- package/tests/seam_wiring_guard.mjs +11 -11
- package/tests/search_isolation.mjs +3 -3
- package/tests/session_records.mjs +1 -1
- package/tests/session_rename_fallback.mjs +1 -22
- package/tests/session_start_freshness.mjs +3 -3
- package/tests/session_token_hash_db.mjs +1 -1
- package/tests/ship_card.mjs +1 -1
- package/tests/ship_ci_deploy.mjs +17 -17
- package/tests/ship_error_shape.mjs +1 -1
- package/tests/ship_premerge.mjs +1 -1
- package/tests/ship_resume.mjs +1 -1
- package/tests/skill_preflight.mjs +2 -2
- package/tests/skip_is_not_pass.mjs +1 -1
- package/tests/task_visual_slots.mjs +31 -0
- package/tests/terms_acceptance.mjs +2 -2
- 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
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
// scripts/gds/module-artifact.js — pack and verify ONE module version for the
|
|
2
|
+
// Bongos-hosted module store (task 1004271, ADR 0338 D1).
|
|
3
|
+
//
|
|
4
|
+
// ADR 0338 settled "no second packaging format": a module version is a tarball plus a
|
|
5
|
+
// hash manifest in exactly the shape package-core.js emits for the core — per-file
|
|
6
|
+
// sha256, a compression-independent tree_sha256, a deterministic ustar+gzip — so the
|
|
7
|
+
// tar writer, reader and tree pin all come from artifact-format.js, the file the core
|
|
8
|
+
// release builds with. What differs is only WHAT is packed and what the manifest names:
|
|
9
|
+
//
|
|
10
|
+
// tarball package/<path inside modules/<key>/> (module.json at package/module.json)
|
|
11
|
+
// package/.bongos-module.json the inner manifest (below)
|
|
12
|
+
// manifest { artifact: 'bongos-module', manifest_schema, generator, module_key,
|
|
13
|
+
// module_version, core_version, built_at, file_count, tree_sha256,
|
|
14
|
+
// files: [{ path, mode, sha256 }] }
|
|
15
|
+
// side-car the same manifest + { tarball: { name, sha256, bytes } } (withTarballInfo)
|
|
16
|
+
//
|
|
17
|
+
// THE GATE. The core's isPublishable() is default-deny: a path ships only if the
|
|
18
|
+
// allowlist names it and the denylist does not. The allowlist names the CORE's own
|
|
19
|
+
// modules, so for a store module the allow side is "inside this module's own
|
|
20
|
+
// modules/<key>/ directory" and the deny side is the SAME repo-wide denylist
|
|
21
|
+
// (matchesDeny — game content, pixel art, copyrighted references, instance topology,
|
|
22
|
+
// secrets), plus credential-named files anywhere in the module (SECRET_FILE_RE). Anything the denylist catches refuses the whole publish rather than being
|
|
23
|
+
// silently dropped: a module that ships without a file it expected is a broken module.
|
|
24
|
+
//
|
|
25
|
+
// Both halves live here so the CLI (packModule) and the store route
|
|
26
|
+
// (verifyModuleArtifact) cannot disagree about the format. Pure apart from reading the
|
|
27
|
+
// module directory in packModule; never touches the network or the database.
|
|
28
|
+
|
|
29
|
+
const fs = require('node:fs');
|
|
30
|
+
const path = require('node:path');
|
|
31
|
+
const { sha256hex, canonicalTreeHash, normalizeMode, buildTar, readTarAsync } = require('./artifact-format');
|
|
32
|
+
const { matchesDeny } = require('./publish-manifest');
|
|
33
|
+
const { validateManifest } = require('../../src/module-loader/manifest-schema');
|
|
34
|
+
|
|
35
|
+
const ARTIFACT_NAME = 'bongos-module';
|
|
36
|
+
const MANIFEST_SCHEMA = 1;
|
|
37
|
+
const TAR_ROOT = 'package/';
|
|
38
|
+
const INNER_MANIFEST_PATH = '.bongos-module.json';
|
|
39
|
+
const INNER_MANIFEST = TAR_ROOT + INNER_MANIFEST_PATH;
|
|
40
|
+
// Local-only files a module dir may hold that are never part of the module itself.
|
|
41
|
+
const NEVER_PACKED = new Set(['.upstream-signoff.json', INNER_MANIFEST_PATH]);
|
|
42
|
+
// The store's caps. A real feature module is tens of KB (the biggest bundled one packs
|
|
43
|
+
// to ~90 KB), so these are generous headroom — and deliberately small, because the
|
|
44
|
+
// store verifies an upload inside the web process: every byte is inflated and hashed
|
|
45
|
+
// there. 5 MB on the wire matches the upstreaming queue's SUBMISSION_MAX_BYTES; the
|
|
46
|
+
// unpacked cap bounds a gzip bomb.
|
|
47
|
+
const MAX_TARBALL_BYTES = 5 * 1024 * 1024;
|
|
48
|
+
const READ_LIMITS = { maxUnpackedBytes: 16 * 1024 * 1024, maxFiles: 2000 };
|
|
49
|
+
|
|
50
|
+
function tarballName(key, version) { return `${ARTIFACT_NAME}-${key}-${version}.tgz`; }
|
|
51
|
+
|
|
52
|
+
// Every file under dir, as module-relative forward-slash paths, sorted. Skips
|
|
53
|
+
// VCS/dependency noise the way listModuleFiles (module.js) does.
|
|
54
|
+
function listFiles(dir, rel = '') {
|
|
55
|
+
const out = [];
|
|
56
|
+
for (const entry of fs.readdirSync(path.join(dir, rel), { withFileTypes: true })) {
|
|
57
|
+
if (entry.name === 'node_modules' || entry.name === '.git') continue;
|
|
58
|
+
const r = rel ? `${rel}/${entry.name}` : entry.name;
|
|
59
|
+
if (entry.isDirectory()) out.push(...listFiles(dir, r));
|
|
60
|
+
else if (entry.isFile()) out.push(r);
|
|
61
|
+
}
|
|
62
|
+
return out.sort();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// A module-relative path is safe to unpack only if it stays inside the module.
|
|
66
|
+
function isSafeRelPath(p) {
|
|
67
|
+
return typeof p === 'string' && p.length > 0 && !p.startsWith('/') && !p.includes('\\')
|
|
68
|
+
&& !/^[A-Za-z]:/.test(p) && !p.split('/').some((seg) => seg === '' || seg === '.' || seg === '..');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// The repo denylist anchors its secret entries at the root (`.env`, not
|
|
72
|
+
// `modules/x/.env`), which is right for the core tree and wrong for a module that
|
|
73
|
+
// travels on its own. So a store module is also refused any file whose NAME marks it
|
|
74
|
+
// as a credential, wherever it sits in the module.
|
|
75
|
+
const SECRET_FILE_RE = /^(?:\.env(?:\..*)?|\.npmrc|\.netrc|\.pgpass|id_(?:rsa|dsa|ecdsa|ed25519)(?:\.pub)?|.*\.(?:pem|key|p12|pfx|jks|keystore))$/i;
|
|
76
|
+
|
|
77
|
+
// The files the gate refuses, named at their canonical modules/<key>/ path.
|
|
78
|
+
function deniedFiles(key, relPaths) {
|
|
79
|
+
return relPaths
|
|
80
|
+
.filter((p) => matchesDeny(`modules/${key}/${p}`) || SECRET_FILE_RE.test(p.split('/').pop()))
|
|
81
|
+
.map((p) => `modules/${key}/${p}`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function buildModuleManifest({ key, version, coreVersion, builtAtIso, files, treeHash }) {
|
|
85
|
+
return {
|
|
86
|
+
artifact: ARTIFACT_NAME,
|
|
87
|
+
manifest_schema: MANIFEST_SCHEMA,
|
|
88
|
+
generator: 'scripts/gds/module-artifact.js',
|
|
89
|
+
module_key: key,
|
|
90
|
+
module_version: version,
|
|
91
|
+
core_version: coreVersion,
|
|
92
|
+
built_at: builtAtIso,
|
|
93
|
+
file_count: files.length,
|
|
94
|
+
tree_sha256: treeHash,
|
|
95
|
+
files: files.map((f) => ({ path: f.path, mode: normalizeMode(f.mode), sha256: f.sha256 })),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Pack modules/<key>/ into { tgz, manifest, sidecar } or throw with a plain reason.
|
|
100
|
+
// `modeOf` is injectable because Windows reports no executable bit.
|
|
101
|
+
function packModule(key, { modulesDir, now = () => new Date(), modeOf } = {}) {
|
|
102
|
+
const dir = path.join(modulesDir, key);
|
|
103
|
+
if (!fs.existsSync(dir)) throw new Error(`modules/${key} does not exist`);
|
|
104
|
+
let moduleJson;
|
|
105
|
+
try { moduleJson = JSON.parse(fs.readFileSync(path.join(dir, 'module.json'), 'utf8')); }
|
|
106
|
+
catch { throw new Error(`modules/${key}/module.json is missing or is not valid JSON`); }
|
|
107
|
+
const { valid, errors } = validateManifest(moduleJson);
|
|
108
|
+
if (!valid) throw new Error(`modules/${key}/module.json is invalid: ${errors.join('; ')}`);
|
|
109
|
+
if (moduleJson.key !== key) throw new Error(`modules/${key}/module.json names key "${moduleJson.key}"`);
|
|
110
|
+
|
|
111
|
+
const rel = listFiles(dir).filter((p) => !NEVER_PACKED.has(p));
|
|
112
|
+
const denied = deniedFiles(key, rel);
|
|
113
|
+
if (denied.length) {
|
|
114
|
+
throw new Error(`the publish denylist refuses: ${denied.join(', ')} — remove them from the module (ADR 0098)`);
|
|
115
|
+
}
|
|
116
|
+
const unsafe = rel.filter((p) => !isSafeRelPath(p));
|
|
117
|
+
if (unsafe.length) throw new Error(`unpackable file name(s): ${unsafe.join(', ')}`);
|
|
118
|
+
|
|
119
|
+
const files = [];
|
|
120
|
+
const entries = [];
|
|
121
|
+
for (const p of rel) {
|
|
122
|
+
const abs = path.join(dir, ...p.split('/'));
|
|
123
|
+
const buf = fs.readFileSync(abs);
|
|
124
|
+
const mode = normalizeMode(modeOf ? modeOf(abs) : (fs.statSync(abs).mode & 0o777).toString(8));
|
|
125
|
+
files.push({ path: p, mode, sha256: sha256hex(buf) });
|
|
126
|
+
entries.push({ name: TAR_ROOT + p, mode: parseInt(mode, 8), buf });
|
|
127
|
+
}
|
|
128
|
+
const manifest = buildModuleManifest({
|
|
129
|
+
key, version: moduleJson.version, coreVersion: moduleJson.coreVersion,
|
|
130
|
+
builtAtIso: now().toISOString(), files, treeHash: canonicalTreeHash(files),
|
|
131
|
+
});
|
|
132
|
+
entries.push({ name: INNER_MANIFEST, mode: 0o644, buf: Buffer.from(JSON.stringify(manifest, null, 2) + '\n') });
|
|
133
|
+
const tgz = buildTar(entries);
|
|
134
|
+
const sidecar = { ...manifest, tarball: { name: tarballName(key, manifest.module_version), sha256: sha256hex(tgz), bytes: tgz.length } };
|
|
135
|
+
return { tgz, manifest, sidecar, moduleJson };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// The store's check on an upload. Trusts NOTHING the client says beyond the bytes:
|
|
139
|
+
// every hash is recomputed, the inner manifest must describe exactly the files the
|
|
140
|
+
// tarball holds, module.json must validate and name the route's key, and the
|
|
141
|
+
// denylist runs again. Resolves { ok: true, ... } or { ok: false, code, message }.
|
|
142
|
+
// Async so the inflate runs off the event loop (readTarAsync); the rest is one pass
|
|
143
|
+
// over at most READ_LIMITS.maxUnpackedBytes.
|
|
144
|
+
async function verifyModuleArtifact(tgz, { key, maxBytes = MAX_TARBALL_BYTES } = {}) {
|
|
145
|
+
const fail = (code, message) => ({ ok: false, code, message });
|
|
146
|
+
if (!Buffer.isBuffer(tgz) || tgz.length === 0) return fail('empty_artifact', 'the upload is empty');
|
|
147
|
+
if (tgz.length > maxBytes) return fail('artifact_too_large', `the tarball is over ${maxBytes} bytes`);
|
|
148
|
+
|
|
149
|
+
let entries;
|
|
150
|
+
try { entries = await readTarAsync(tgz, READ_LIMITS); }
|
|
151
|
+
catch (e) { return fail('bad_tarball', e.message); }
|
|
152
|
+
|
|
153
|
+
const byName = new Map();
|
|
154
|
+
for (const e of entries) {
|
|
155
|
+
if (!e.name.startsWith(TAR_ROOT)) return fail('bad_tarball', `"${e.name}" is outside ${TAR_ROOT}`);
|
|
156
|
+
const rel = e.name.slice(TAR_ROOT.length);
|
|
157
|
+
if (!isSafeRelPath(rel)) return fail('bad_tarball', `"${e.name}" is not a safe path`);
|
|
158
|
+
if (byName.has(rel)) return fail('bad_tarball', `"${e.name}" appears twice`);
|
|
159
|
+
byName.set(rel, e);
|
|
160
|
+
}
|
|
161
|
+
const inner = byName.get(INNER_MANIFEST_PATH);
|
|
162
|
+
if (!inner) return fail('no_manifest', `the tarball carries no ${INNER_MANIFEST}`);
|
|
163
|
+
let manifest;
|
|
164
|
+
try { manifest = JSON.parse(inner.buf.toString('utf8')); }
|
|
165
|
+
catch { return fail('no_manifest', `${INNER_MANIFEST} is not valid JSON`); }
|
|
166
|
+
if (manifest.artifact !== ARTIFACT_NAME || manifest.manifest_schema !== MANIFEST_SCHEMA || !Array.isArray(manifest.files)) {
|
|
167
|
+
return fail('bad_manifest', `${INNER_MANIFEST} is not a ${ARTIFACT_NAME} schema ${MANIFEST_SCHEMA} manifest`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Recompute the pin from the bytes and hold the manifest to it, file by file.
|
|
171
|
+
byName.delete(INNER_MANIFEST_PATH);
|
|
172
|
+
const actual = [...byName.entries()]
|
|
173
|
+
.map(([p, e]) => ({ path: p, mode: normalizeMode(e.mode.toString(8)), sha256: sha256hex(e.buf) }))
|
|
174
|
+
.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
|
|
175
|
+
const declared = [...manifest.files].sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
|
|
176
|
+
if (declared.length !== actual.length || manifest.file_count !== actual.length) {
|
|
177
|
+
return fail('hash_mismatch', `the manifest lists ${declared.length} file(s) but the tarball holds ${actual.length}`);
|
|
178
|
+
}
|
|
179
|
+
for (let i = 0; i < actual.length; i++) {
|
|
180
|
+
const a = actual[i]; const d = declared[i];
|
|
181
|
+
if (d.path !== a.path || normalizeMode(d.mode) !== a.mode || d.sha256 !== a.sha256) {
|
|
182
|
+
return fail('hash_mismatch', `"${a.path}" does not match its manifest entry`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
const treeSha = canonicalTreeHash(actual);
|
|
186
|
+
if (manifest.tree_sha256 !== treeSha) return fail('hash_mismatch', 'tree_sha256 does not match the files');
|
|
187
|
+
|
|
188
|
+
const denied = deniedFiles(key, actual.map((f) => f.path));
|
|
189
|
+
if (denied.length) return fail('denied_content', `the publish denylist refuses: ${denied.join(', ')}`);
|
|
190
|
+
|
|
191
|
+
const mj = byName.get('module.json');
|
|
192
|
+
if (!mj) return fail('bad_module', 'the tarball has no module.json');
|
|
193
|
+
let moduleJson;
|
|
194
|
+
try { moduleJson = JSON.parse(mj.buf.toString('utf8')); }
|
|
195
|
+
catch { return fail('bad_module', 'module.json is not valid JSON'); }
|
|
196
|
+
const v = validateManifest(moduleJson);
|
|
197
|
+
if (!v.valid) return fail('bad_module', `module.json is invalid: ${v.errors.join('; ')}`);
|
|
198
|
+
if (moduleJson.key !== key || manifest.module_key !== key) {
|
|
199
|
+
return fail('key_mismatch', `the tarball is module "${moduleJson.key}", not "${key}"`);
|
|
200
|
+
}
|
|
201
|
+
if (manifest.module_version !== moduleJson.version || manifest.core_version !== moduleJson.coreVersion) {
|
|
202
|
+
return fail('bad_manifest', 'the manifest version does not match module.json');
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return {
|
|
206
|
+
ok: true, manifest, moduleJson,
|
|
207
|
+
version: moduleJson.version, coreVersion: moduleJson.coreVersion,
|
|
208
|
+
treeSha256: treeSha, tarballSha256: sha256hex(tgz), bytes: tgz.length,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
module.exports = { MAX_TARBALL_BYTES, packModule, verifyModuleArtifact };
|
package/scripts/gds/module.js
CHANGED
|
@@ -102,12 +102,12 @@ function envPrefix() {
|
|
|
102
102
|
// Pure builders (exported for tests — no I/O, no network)
|
|
103
103
|
// ---------------------------------------------------------------------------
|
|
104
104
|
|
|
105
|
-
//
|
|
105
|
+
// art-pipeline -> "Art Pipeline"; economy -> "Economy". A readable default title.
|
|
106
106
|
function titleize(key) {
|
|
107
107
|
return String(key).split(/[-_]+/).filter(Boolean)
|
|
108
108
|
.map((w) => w[0].toUpperCase() + w.slice(1)).join(' ');
|
|
109
109
|
}
|
|
110
|
-
//
|
|
110
|
+
// art-pipeline -> artPipeline (for the generated route-factory function name).
|
|
111
111
|
function camel(key) {
|
|
112
112
|
return String(key).replace(/[-_]+(.)/g, (_, c) => c.toUpperCase());
|
|
113
113
|
}
|
|
@@ -125,6 +125,8 @@ function buildManifest(key, { core = coreVersion() } = {}) {
|
|
|
125
125
|
key,
|
|
126
126
|
title: titleize(key),
|
|
127
127
|
description: `TODO: one sentence describing the ${key} module.`,
|
|
128
|
+
// The module's own version (task 1003781). A fresh scaffold is pre-release.
|
|
129
|
+
version: '0.1.0',
|
|
128
130
|
coreVersion: `^${core}`,
|
|
129
131
|
default: false,
|
|
130
132
|
author: 'TODO: your name or builder handle',
|
|
@@ -265,7 +267,7 @@ function evaluateUpgrade({ rows = [], errors = [], core } = {}) {
|
|
|
265
267
|
|
|
266
268
|
function scaffoldModule({ key, modulesDir = MODULES_DIR, core = coreVersion(), force = false } = {}) {
|
|
267
269
|
if (!key || !KEY_RE.test(key)) {
|
|
268
|
-
throw new Error(`invalid module key "${key}" — must match ${KEY_RE} (lowercase kebab-case, e.g. "
|
|
270
|
+
throw new Error(`invalid module key "${key}" — must match ${KEY_RE} (lowercase kebab-case, e.g. "art-pipeline")`);
|
|
269
271
|
}
|
|
270
272
|
const dir = path.join(modulesDir, key);
|
|
271
273
|
if (fs.existsSync(dir) && !force) {
|
|
@@ -411,14 +413,14 @@ function formatCatalog(rows, { catalogOnly = false, query = '', core = null } =
|
|
|
411
413
|
return out.join('\n');
|
|
412
414
|
}
|
|
413
415
|
|
|
414
|
-
out.push(` ${pad('', 5)}${pad('KEY', 20)}${pad('SOURCE', 10)}${pad('AUTHOR', 20)}${pad('ORIGIN', 26)}${pad('LICENSE', 12)}UPKEEP`);
|
|
416
|
+
out.push(` ${pad('', 5)}${pad('KEY', 20)}${pad('VERSION', 9)}${pad('SOURCE', 10)}${pad('AUTHOR', 20)}${pad('ORIGIN', 26)}${pad('LICENSE', 12)}UPKEEP`);
|
|
415
417
|
for (const r of shown) {
|
|
416
418
|
const mark = r.enabled ? '[x]' : '[ ]';
|
|
417
419
|
// An inferred credit is marked so it never reads as something the author
|
|
418
420
|
// typed — the catalog's whole job is honest attribution (ADR 0107 §2).
|
|
419
421
|
const suffix = r.credit.inferred ? '*' : '';
|
|
420
422
|
const upkeep = (r.maintenance && r.maintenance.status) || 'undeclared';
|
|
421
|
-
out.push(` ${pad(mark, 5)}${pad(clip(r.key, 19), 20)}${pad(r.source, 10)}${pad(clip((r.credit.author || '—') + suffix, 19), 20)}${pad(clip((r.credit.origin || '—') + suffix, 25), 26)}${pad(clip(r.license || '—', 11), 12)}${upkeep}`);
|
|
423
|
+
out.push(` ${pad(mark, 5)}${pad(clip(r.key, 19), 20)}${pad(clip(r.version || '—', 8), 9)}${pad(r.source, 10)}${pad(clip((r.credit.author || '—') + suffix, 19), 20)}${pad(clip((r.credit.origin || '—') + suffix, 25), 26)}${pad(clip(r.license || '—', 11), 12)}${upkeep}`);
|
|
422
424
|
}
|
|
423
425
|
out.push('');
|
|
424
426
|
out.push(' [x] enabled here · [ ] available, not enabled');
|
|
@@ -776,6 +778,90 @@ async function cmdSubmit(args, {
|
|
|
776
778
|
return 0;
|
|
777
779
|
}
|
|
778
780
|
|
|
781
|
+
// ---------------------------------------------------------------------------
|
|
782
|
+
// bongos module publish <key> — put a module version in the store (ADR 0338 D1)
|
|
783
|
+
// ---------------------------------------------------------------------------
|
|
784
|
+
|
|
785
|
+
// `bongos module publish <key>` — task 1004271. Packs modules/<key>/ into the
|
|
786
|
+
// store's tarball + hash manifest (scripts/gds/module-artifact.js — the core
|
|
787
|
+
// release's own format, through the publish denylist), then uploads it to
|
|
788
|
+
// POST /store/modules/:key/versions, which re-verifies every hash itself and
|
|
789
|
+
// records the version. `--dry-run` packs and reports without uploading;
|
|
790
|
+
// `--out <dir>` also writes the tarball and its side-car manifest there.
|
|
791
|
+
// Distinct from `submit`, which proposes a module INTO core (ADR 0135).
|
|
792
|
+
async function cmdPublish(args, {
|
|
793
|
+
log = console.log, errlog = console.error,
|
|
794
|
+
modulesDir = MODULES_DIR, pack, upload, writeFile = fs.writeFileSync,
|
|
795
|
+
} = {}) {
|
|
796
|
+
const key = args.find((a, i) => !a.startsWith('-') && args[i - 1] !== '--out');
|
|
797
|
+
if (!key) {
|
|
798
|
+
errlog('usage: bongos module publish <key> [--dry-run] [--out <dir>]');
|
|
799
|
+
return 2;
|
|
800
|
+
}
|
|
801
|
+
const dryRun = args.includes('--dry-run');
|
|
802
|
+
const outIdx = args.indexOf('--out');
|
|
803
|
+
const outDir = outIdx !== -1 ? args[outIdx + 1] : null;
|
|
804
|
+
if (outIdx !== -1 && (!outDir || outDir.startsWith('--'))) {
|
|
805
|
+
errlog('usage: bongos module publish <key> [--dry-run] [--out <dir>]');
|
|
806
|
+
return 2;
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
const artifact = require('./module-artifact');
|
|
810
|
+
let packed;
|
|
811
|
+
try { packed = (pack || ((k) => artifact.packModule(k, { modulesDir })))(key); }
|
|
812
|
+
catch (e) { errlog(`REFUSING: ${e.message}`); return 1; }
|
|
813
|
+
const { tgz, sidecar } = packed;
|
|
814
|
+
log(`bongos module publish — "${key}" ${sidecar.module_version} (needs core ${sidecar.core_version})`);
|
|
815
|
+
log(` ✓ packed ${sidecar.file_count} file(s), ${tgz.length} bytes — tree ${sidecar.tree_sha256.slice(0, 12)}…`);
|
|
816
|
+
if (tgz.length > artifact.MAX_TARBALL_BYTES) {
|
|
817
|
+
errlog(`REFUSING: the tarball is ${tgz.length} bytes, over the store's ${artifact.MAX_TARBALL_BYTES}-byte cap.`);
|
|
818
|
+
return 1;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
if (outDir) {
|
|
822
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
823
|
+
const tgzPath = path.join(outDir, sidecar.tarball.name);
|
|
824
|
+
writeFile(tgzPath, tgz);
|
|
825
|
+
writeFile(tgzPath.replace(/\.tgz$/, '.manifest.json'), JSON.stringify(sidecar, null, 2) + '\n');
|
|
826
|
+
log(` ✓ wrote ${tgzPath} (+ .manifest.json)`);
|
|
827
|
+
}
|
|
828
|
+
if (dryRun) {
|
|
829
|
+
log('\n--dry-run: nothing uploaded.');
|
|
830
|
+
return 0;
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
const doUpload = upload || (async (k, body) => {
|
|
834
|
+
// Lazy require — the same reason cmdSubmit gives: this file loads in the server
|
|
835
|
+
// process too, and cli-lib's session/fetch plumbing must never come with it.
|
|
836
|
+
const { readSessionToken, dispatchedFetch, noKeepAliveDispatcher, CLI_USER_AGENT } = require('./cli-lib');
|
|
837
|
+
const session = readSessionToken();
|
|
838
|
+
if (!session) throw new Error('no Bongos session — run `bongos setup` first');
|
|
839
|
+
const res = await dispatchedFetch(`${session.apiBase}/api/bongos/store/modules/${encodeURIComponent(k)}/versions`, {
|
|
840
|
+
method: 'POST',
|
|
841
|
+
headers: { 'Content-Type': 'application/gzip', Authorization: `Bearer ${session.token}`, 'User-Agent': CLI_USER_AGENT },
|
|
842
|
+
body,
|
|
843
|
+
dispatcher: noKeepAliveDispatcher(),
|
|
844
|
+
});
|
|
845
|
+
const text = await res.text();
|
|
846
|
+
let data;
|
|
847
|
+
try { data = text ? JSON.parse(text) : null; } catch { data = { raw: text }; }
|
|
848
|
+
return { status: res.status, ok: res.ok, data };
|
|
849
|
+
});
|
|
850
|
+
|
|
851
|
+
let res;
|
|
852
|
+
try { res = await doUpload(key, tgz); }
|
|
853
|
+
catch (e) { errlog(`bongos module publish: upload failed — ${e.message}`); return 1; }
|
|
854
|
+
if (!res.ok) {
|
|
855
|
+
const err = res.data && (res.data.error || res.data);
|
|
856
|
+
const detail = (err && (err.message || err.code)) || JSON.stringify(res.data);
|
|
857
|
+
errlog(`\nREFUSING: the store rejected "${key}" ${sidecar.module_version} (HTTP ${res.status}): ${detail}`);
|
|
858
|
+
return 1;
|
|
859
|
+
}
|
|
860
|
+
const row = res.data && res.data.version;
|
|
861
|
+
log(`\n✓ Published "${key}" ${row ? row.version : sidecar.module_version} to the store${res.data && res.data.created_module ? ' — its first version, so you are its author' : ''}.`);
|
|
862
|
+
return 0;
|
|
863
|
+
}
|
|
864
|
+
|
|
779
865
|
function printHelp(out = console.log) {
|
|
780
866
|
out('bongos module — scaffold + manage Cloud Bongos modules\n');
|
|
781
867
|
out('Usage:');
|
|
@@ -787,6 +873,8 @@ function printHelp(out = console.log) {
|
|
|
787
873
|
out(' AGPL/publish pre-check before upstreaming + record a DCO-style sign-off');
|
|
788
874
|
out(' bongos module submit <key> [--sign-off "Name <email>"]');
|
|
789
875
|
out(' Pre-check + sign-off, then file into the rank-gated upstreaming review queue');
|
|
876
|
+
out(' bongos module publish <key> [--dry-run] [--out <dir>]');
|
|
877
|
+
out(' Pack a module version (tarball + hash manifest) and upload it to the module store');
|
|
790
878
|
}
|
|
791
879
|
|
|
792
880
|
async function main(argv) {
|
|
@@ -797,6 +885,7 @@ async function main(argv) {
|
|
|
797
885
|
if (sub === 'upgrade') return cmdUpgrade(args.slice(1));
|
|
798
886
|
if (sub === 'check') return cmdCheck(args.slice(1));
|
|
799
887
|
if (sub === 'submit') return cmdSubmit(args.slice(1));
|
|
888
|
+
if (sub === 'publish') return cmdPublish(args.slice(1));
|
|
800
889
|
if (!sub || sub === 'help' || sub === '--help' || sub === '-h') {
|
|
801
890
|
printHelp();
|
|
802
891
|
return sub ? 0 : 2;
|
|
@@ -807,7 +896,7 @@ async function main(argv) {
|
|
|
807
896
|
}
|
|
808
897
|
|
|
809
898
|
module.exports = {
|
|
810
|
-
main, cmdNew, cmdList, cmdUpgrade, cmdCheck, cmdSubmit, printHelp,
|
|
899
|
+
main, cmdNew, cmdList, cmdUpgrade, cmdCheck, cmdSubmit, cmdPublish, printHelp,
|
|
811
900
|
gatherCatalog, formatCatalog, ownProjectName,
|
|
812
901
|
scaffoldModule, gatherUpgrade, evaluateUpgrade, buildManifest,
|
|
813
902
|
formatMaintenanceHint, isPastRemoveAfter,
|
|
@@ -103,6 +103,7 @@ const REPO_ROOT = path.resolve(__dirname, '..', '..');
|
|
|
103
103
|
// Reuse — never re-type. The core definition, the redaction, and the tree parser
|
|
104
104
|
// all already exist and are unit-tested at their homes.
|
|
105
105
|
const { isPublishable, PUBLISH_ALLOWLIST } = require(path.join(REPO_ROOT, 'scripts/gds/publish-manifest.js'));
|
|
106
|
+
const { sha256hex, canonicalTreeHash, normalizeMode, octal, tarChecksum, ustarHeader, splitUstarName, buildTar } = require('./artifact-format');
|
|
106
107
|
const {
|
|
107
108
|
buildRedactionRules, collectSensitiveLiterals, findLeaks,
|
|
108
109
|
} = require(path.join(REPO_ROOT, 'scripts/gds/mirror-redact.js'));
|
|
@@ -144,9 +145,6 @@ function coreVersion() {
|
|
|
144
145
|
// Pure helpers (exported for tests — no I/O, no git, no network)
|
|
145
146
|
// ---------------------------------------------------------------------------
|
|
146
147
|
|
|
147
|
-
function sha256hex(buf) {
|
|
148
|
-
return crypto.createHash('sha256').update(buf).digest('hex');
|
|
149
|
-
}
|
|
150
148
|
|
|
151
149
|
// ---------------------------------------------------------------------------
|
|
152
150
|
// CONSUMER-CORE extraction policy (task 1824) — the file-classification layer.
|
|
@@ -418,31 +416,8 @@ function prunePackageLock(sourceLock, { name, version, rootRanges, rootDepNames
|
|
|
418
416
|
};
|
|
419
417
|
}
|
|
420
418
|
|
|
421
|
-
// The PIN
|
|
422
|
-
//
|
|
423
|
-
// recomputes this from the unpacked tree to prove an install matches the pin.
|
|
424
|
-
// `files` = [{ path, mode, sha256 }]. Modes are normalized so the pin is stable
|
|
425
|
-
// regardless of the checkout's umask.
|
|
426
|
-
function canonicalTreeHash(files) {
|
|
427
|
-
const lines = (files || [])
|
|
428
|
-
.map((f) => `${f.path}\u0000${normalizeMode(f.mode)}\u0000${f.sha256}`)
|
|
429
|
-
.sort();
|
|
430
|
-
return sha256hex(Buffer.from(lines.join('\n') + '\n', 'utf8'));
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
// Collapse ANY mode form — git's 100644/100755, our canonical 0000644/0000755, or a
|
|
434
|
-
// raw filesystem octal string like "755" — to two canonical tar modes, keyed only on
|
|
435
|
-
// the executable bit. IDEMPOTENT (normalizeMode(normalizeMode(x)) === normalizeMode(x))
|
|
436
|
-
// AND exec-aware, so the tree pin reproduces byte-identically whether a consumer
|
|
437
|
-
// recomputes it from the manifest's files[] (already-canonical modes) or from the
|
|
438
|
-
// unpacked tree's stat() modes. Expects a STRING (git mode / canonical mode / octal).
|
|
439
|
-
function normalizeMode(mode) {
|
|
440
|
-
const s = String(mode);
|
|
441
|
-
if (s === '100755' || s === '0000755' || s === '755') return '0000755';
|
|
442
|
-
const n = parseInt(s, 8);
|
|
443
|
-
if (Number.isFinite(n) && (n & 0o111)) return '0000755';
|
|
444
|
-
return '0000644';
|
|
445
|
-
}
|
|
419
|
+
// The PIN (canonicalTreeHash) and normalizeMode live in scripts/gds/artifact-format.js
|
|
420
|
+
// (task 1004271) — the module store verifies with the same functions.
|
|
446
421
|
|
|
447
422
|
// The pin manifest (WITHOUT tarball.* — added by withTarballInfo for the side-car).
|
|
448
423
|
function buildCoreManifest({
|
|
@@ -478,94 +453,8 @@ function tarballName(version) { return `${ARTIFACT_NAME}-${version}.tgz`; }
|
|
|
478
453
|
function manifestName(version) { return `${ARTIFACT_NAME}-${version}.manifest.json`; }
|
|
479
454
|
function stageDirName(version) { return `${ARTIFACT_NAME}-${version}`; }
|
|
480
455
|
|
|
481
|
-
//
|
|
482
|
-
//
|
|
483
|
-
// source yields byte-identical bytes — a bonus over the tree_sha256 pin. Long paths
|
|
484
|
-
// use the ustar name/prefix split, falling back to a GNU 'L' longname entry (read
|
|
485
|
-
// by GNU tar, bsdtar/macOS, and node-tar) for the rare path that won't split.
|
|
486
|
-
|
|
487
|
-
function octal(n, width) {
|
|
488
|
-
// width-1 octal digits, zero-padded, then a NUL — the classic numeric field.
|
|
489
|
-
return Buffer.from(n.toString(8).padStart(width - 1, '0').slice(-(width - 1)) + '\0', 'ascii');
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
function tarChecksum(block) {
|
|
493
|
-
let sum = 0;
|
|
494
|
-
for (let i = 0; i < 512; i++) sum += (i >= 148 && i < 156) ? 0x20 : block[i];
|
|
495
|
-
return sum;
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
// Build one 512-byte ustar header. `name`/`prefix` must already fit (<=100 / <=155).
|
|
499
|
-
function ustarHeader({ name, prefix = '', mode, size, typeflag }) {
|
|
500
|
-
const h = Buffer.alloc(512, 0);
|
|
501
|
-
h.write(name, 0, 100, 'utf8');
|
|
502
|
-
octal(mode, 8).copy(h, 100);
|
|
503
|
-
octal(0, 8).copy(h, 108); // uid
|
|
504
|
-
octal(0, 8).copy(h, 116); // gid
|
|
505
|
-
octal(size, 12).copy(h, 124);
|
|
506
|
-
octal(0, 12).copy(h, 136); // mtime = 0 (determinism)
|
|
507
|
-
h.write(typeflag, 156, 1, 'ascii');
|
|
508
|
-
h.write('ustar\0', 257, 6, 'ascii');
|
|
509
|
-
h.write('00', 263, 2, 'ascii');
|
|
510
|
-
if (prefix) h.write(prefix, 345, 155, 'utf8');
|
|
511
|
-
// checksum field: 6 octal digits + NUL + space, computed with the field = spaces.
|
|
512
|
-
const cksum = tarChecksum(h);
|
|
513
|
-
h.write(cksum.toString(8).padStart(6, '0').slice(-6) + '\0 ', 148, 8, 'ascii');
|
|
514
|
-
return h;
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
// Split a >100-byte path into { name<=100, prefix<=155 } at a '/' boundary, or
|
|
518
|
-
// return null if no split fits (→ caller uses the GNU longname fallback).
|
|
519
|
-
function splitUstarName(fullPath) {
|
|
520
|
-
const bytes = Buffer.byteLength(fullPath, 'utf8');
|
|
521
|
-
if (bytes <= 100) return { name: fullPath, prefix: '' };
|
|
522
|
-
// Find the LAST '/' such that the tail (after it) is <=100 and the head <=155.
|
|
523
|
-
for (let i = fullPath.length - 1; i > 0; i--) {
|
|
524
|
-
if (fullPath[i] !== '/') continue;
|
|
525
|
-
const prefix = fullPath.slice(0, i);
|
|
526
|
-
const name = fullPath.slice(i + 1);
|
|
527
|
-
if (Buffer.byteLength(name, 'utf8') <= 100 && Buffer.byteLength(prefix, 'utf8') <= 155) {
|
|
528
|
-
return { name, prefix };
|
|
529
|
-
}
|
|
530
|
-
}
|
|
531
|
-
return null;
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
function padTo512(buf) {
|
|
535
|
-
const rem = buf.length % 512;
|
|
536
|
-
return rem === 0 ? buf : Buffer.concat([buf, Buffer.alloc(512 - rem, 0)]);
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
// GNU longname pseudo-entry: a header (typeflag 'L', name '././@LongLink') whose
|
|
540
|
-
// BODY is the full path (incl. trailing NUL), immediately followed by the real
|
|
541
|
-
// file entry. Read by GNU tar, bsdtar/macOS, and node-tar.
|
|
542
|
-
function gnuLongName(fullPath) {
|
|
543
|
-
const payload = Buffer.from(fullPath + '\0', 'utf8');
|
|
544
|
-
const hdr = ustarHeader({ name: '././@LongLink', mode: 0o644, size: payload.length, typeflag: 'L' });
|
|
545
|
-
return Buffer.concat([hdr, padTo512(payload)]);
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
// entries: [{ name, mode, buf }] — `name` includes the TAR_ROOT prefix. Returns a
|
|
549
|
-
// gzip'd tar Buffer. Pure + deterministic given sorted entries.
|
|
550
|
-
function buildTar(entries) {
|
|
551
|
-
const sorted = [...entries].sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
552
|
-
const chunks = [];
|
|
553
|
-
for (const e of sorted) {
|
|
554
|
-
const mode = e.mode & 0o7777;
|
|
555
|
-
const split = splitUstarName(e.name);
|
|
556
|
-
if (split) {
|
|
557
|
-
chunks.push(ustarHeader({ name: split.name, prefix: split.prefix, mode, size: e.buf.length, typeflag: '0' }));
|
|
558
|
-
} else {
|
|
559
|
-
chunks.push(gnuLongName(e.name));
|
|
560
|
-
chunks.push(ustarHeader({ name: e.name.slice(0, 100), mode, size: e.buf.length, typeflag: '0' }));
|
|
561
|
-
}
|
|
562
|
-
chunks.push(padTo512(e.buf));
|
|
563
|
-
}
|
|
564
|
-
// two zero blocks terminate the archive
|
|
565
|
-
chunks.push(Buffer.alloc(1024, 0));
|
|
566
|
-
const tar = Buffer.concat(chunks);
|
|
567
|
-
return zlib.gzipSync(tar, { level: 9 });
|
|
568
|
-
}
|
|
456
|
+
// The ustar+gzip writer (buildTar and its header helpers) lives in
|
|
457
|
+
// scripts/gds/artifact-format.js (task 1004271), shared with the module store.
|
|
569
458
|
|
|
570
459
|
// ---- the git-LESS source: enumerate the working tree (task 1003995) --------
|
|
571
460
|
//
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// what happened, what the agent will do next, whether a PERSON is needed (and who), and
|
|
9
9
|
// whether this is a rule rather than a fault. That last one is the expensive mistake:
|
|
10
10
|
// an agent that reads a deliberate restriction as breakage goes looking for a way round
|
|
11
|
-
// it (the
|
|
11
|
+
// it (the agent that asked the owner for a personal access token).
|
|
12
12
|
//
|
|
13
13
|
// STATIC TEXT ONLY. A card interpolates nothing — no task title, no server message — so
|
|
14
14
|
// printing it adds no prompt-injection surface (the strand-watch / push-path-brief rule).
|
|
@@ -83,7 +83,7 @@ const CONFIG = {
|
|
|
83
83
|
tag: process.env.PROVISION_TAG || 'cloudbongos-instance',
|
|
84
84
|
doToken: process.env.DO_API_TOKEN || null,
|
|
85
85
|
sshKeyIds: (process.env.BOX_SSH_KEY_IDS || '').split(',').map((s) => s.trim()).filter(Boolean),
|
|
86
|
-
// The Cloudflare DNS hook (the
|
|
86
|
+
// The Cloudflare DNS hook (the instance's infra/box-dns-cloudflare.sh).
|
|
87
87
|
// P4 (#1945) formalizes the UPSERT: point the instance's domain at the target
|
|
88
88
|
// public IP (below), in the domain's OWN Cloudflare zone, proxied=false (see
|
|
89
89
|
// dnsUpsertEnv). No hook configured → DNS is skipped (a co-tenant instance is
|
|
@@ -179,7 +179,7 @@ const MANIFEST_VERIFY_TRIES = numEnv('PROVISION_MANIFEST_VERIFY_TRIES', 90);
|
|
|
179
179
|
const MANIFEST_VERIFY_INTERVAL_MS = numEnv('PROVISION_MANIFEST_VERIFY_INTERVAL_MS', 2000);
|
|
180
180
|
|
|
181
181
|
// ---------------------------------------------------------------------------
|
|
182
|
-
// Lazy dep load (so --help / arg errors need no Postgres)
|
|
182
|
+
// Lazy dep load (so --help / arg errors need no Postgres).
|
|
183
183
|
// ---------------------------------------------------------------------------
|
|
184
184
|
let _deps;
|
|
185
185
|
function loadDeps() {
|
|
@@ -130,8 +130,7 @@ function dnsUpsertEnv(inst, { publicIp, zone, proxied = false } = {}) {
|
|
|
130
130
|
}
|
|
131
131
|
|
|
132
132
|
// The RFC 5737 TEST-NET-1 sentinel a released hostname is parked at (task
|
|
133
|
-
// 1003237
|
|
134
|
-
// (idea 309 / task 1234). 192.0.2.1 is reserved for documentation and is
|
|
133
|
+
// 1003237, first proven in task 1234). 192.0.2.1 is reserved for documentation and is
|
|
135
134
|
// GUARANTEED never routable, so a name parked here can never dial platform
|
|
136
135
|
// infra or a recycled address.
|
|
137
136
|
const DNS_BLACKHOLE_IP = '192.0.2.1';
|
|
@@ -141,7 +140,7 @@ const DNS_BLACKHOLE_IP = '192.0.2.1';
|
|
|
141
140
|
// address UPSERTed at the platform's IP for a project that no longer answers
|
|
142
141
|
// there — the ADR 0181 §2 leak — and this is how the runner lets it go.
|
|
143
142
|
//
|
|
144
|
-
// REPOINT, not delete
|
|
143
|
+
// REPOINT, not delete (task 1234): deleting a name
|
|
145
144
|
// opens an NXDOMAIN window that resolvers negative-cache for the zone's SOA
|
|
146
145
|
// neg-TTL (~30 min, not editable), so a client that queried mid-gap stays
|
|
147
146
|
// broken long after. A name that always resolves — to a guaranteed-dead host —
|
package/scripts/gds/provision.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
'use strict';
|
|
3
3
|
// scripts/gds/provision.js — the INSTANCE-provisioning control-plane runner
|
|
4
|
-
// (ADR 0111 §2/§3, BONGOS-V1 goal 32, task #1944).
|
|
4
|
+
// (ADR 0111 §2/§3, BONGOS-V1 goal 32, task #1944).
|
|
5
5
|
//
|
|
6
6
|
// WHAT IT IS. The trust-boundary-safe executor for the `provisioning` module.
|
|
7
7
|
// The internet-facing web tier can only ENQUEUE a provision/teardown intent into
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
// → Caddy site block + reload
|
|
21
21
|
// → /healthz
|
|
22
22
|
//
|
|
23
|
-
// DESIGN —
|
|
23
|
+
// DESIGN — shared with do-api.js:
|
|
24
24
|
// • DRY-RUN by DEFAULT. Without --apply, every step only PRINTS the command it
|
|
25
25
|
// WOULD run and mutates nothing (no DB writes, no shell, no cloud call). The
|
|
26
26
|
// LIVE leg is an explicit operator/timer `--apply`.
|
|
@@ -37,8 +37,8 @@
|
|
|
37
37
|
// • The step-command generators are PURE functions (exported) → the SAME command
|
|
38
38
|
// strings drive local exec (co-tenant) AND the dedicated droplet's cloud-init.
|
|
39
39
|
//
|
|
40
|
-
// This is control-plane ops tooling (scripts/), so
|
|
41
|
-
//
|
|
40
|
+
// This is control-plane ops tooling (scripts/), so it may require the core + a
|
|
41
|
+
// module's data slice directly (a module may not, but a CLI may). It
|
|
42
42
|
// reuses modules/provisioning/provisioning.js (the intent queue + state) and
|
|
43
43
|
// scripts/gds/do-api.js (the shared DO client extracted in task #1943).
|
|
44
44
|
|
|
@@ -336,7 +336,7 @@ async function provisionInstance(inst, deps) {
|
|
|
336
336
|
if (inst.hosting_shape === 'dedicated') {
|
|
337
337
|
// Dedicated droplet: create it + settle its public IP via the shared DO client
|
|
338
338
|
// (do-api, dry-run-default). The on-droplet app standup rides cloud-init
|
|
339
|
-
// user-data built from the SAME step commands below
|
|
339
|
+
// user-data built from the SAME step commands below. For the
|
|
340
340
|
// dry-run plan we surface the intent; --apply creates the droplet.
|
|
341
341
|
const doApi = deps.doApi;
|
|
342
342
|
log(' [server] dedicated droplet');
|
|
@@ -1249,8 +1249,8 @@ async function drainOAuthManifests(deps) {
|
|
|
1249
1249
|
}
|
|
1250
1250
|
|
|
1251
1251
|
// ---------------------------------------------------------------------------
|
|
1252
|
-
// run-intents — drain provisioning_intents (the systemd-timer entry point).
|
|
1253
|
-
//
|
|
1252
|
+
// run-intents — drain provisioning_intents (the systemd-timer entry point).
|
|
1253
|
+
// Dry-run lists the pending queue; --apply claims +
|
|
1254
1254
|
// executes each, with a poison-attempt cap. FOR UPDATE SKIP LOCKED (in
|
|
1255
1255
|
// provisioning.claimNextIntent) makes overlapping ticks safe. It ALSO drains the
|
|
1256
1256
|
// GitHub-App-Manifest conversion queue on the same tick (task 2080).
|
|
@@ -1324,8 +1324,8 @@ async function cmdRunIntents(deps) {
|
|
|
1324
1324
|
|
|
1325
1325
|
// A light drift reconcile: surface instances stuck mid-flight (provisioning /
|
|
1326
1326
|
// tearing_down) with no open intent — a runner died mid-op. Reports; --apply
|
|
1327
|
-
// re-enqueues a fresh intent so the next tick retries. (Full droplet-vs-DB drift
|
|
1328
|
-
//
|
|
1327
|
+
// re-enqueues a fresh intent so the next tick retries. (Full droplet-vs-DB drift
|
|
1328
|
+
// is a later hardening.)
|
|
1329
1329
|
async function cmdReconcile(deps) {
|
|
1330
1330
|
const { db, provisioning, log, apply } = deps;
|
|
1331
1331
|
const { rows } = await db.query(
|
|
@@ -68,9 +68,8 @@ const PUBLISH_ALLOWLIST = [
|
|
|
68
68
|
'modules/hall-ui/',
|
|
69
69
|
'modules/status-ui/',
|
|
70
70
|
'modules/public-landing/', // the front door (task 1003322, ADR 0218) — the repo-root public-landing/ entry this replaces was always here, and for the same reason: default:true, and it is the only thing an instance serves at its apex root. Omit it and a released core answers the game-less 404 tail on its own home page.
|
|
71
|
-
'modules/dev-box/',
|
|
72
71
|
'modules/discord/',
|
|
73
|
-
'modules/provisioning/', // instance-hosting control plane — CORE
|
|
72
|
+
'modules/provisioning/', // instance-hosting control plane — CORE (its do-api toolchain is core). Reclassified IN so the core CLI scripts/gds/provision.js resolves it from the core package post-split, not a MODULE_NOT_FOUND (task 2135). default:false — shipped ≠ enabled.
|
|
74
73
|
'modules/agents/', // the declarative agent registry + spawn seam (goal 1000038) — CORE: its own manifest declares maintenance.status 'core-maintained', and it consumes the core `grade` port, so a released core without it cannot run the MAS machinery the methodology describes. default:false — shipped is not enabled (ADR 0083), exactly like provisioning + platform-identity below. Omitted at module-creation time (tasks 1002485/1002486): the omission was invisible until tests/agents_spawn.mjs became the first PUBLISHABLE file to import it and the R89 boundary guard caught it (task 1002489).
|
|
75
74
|
'modules/npm-release/', // the deploy page's "where is the work" + version history (task 1002622). default:false — on only in the hall that publishes the core. MUST be here: modules/hall-ui/ ships as one subtree, so a released core without this carries deploy.js's loader and the #nr-work mounts with no routes behind them — the copy-desk asymmetry above.
|
|
76
75
|
'modules/platform-identity/', // hub identity provider (ADR 0141) — ships in core, enabled ONLY on the hub (cloudbongos.com). MUST be here or the hub's enabledModules() throws "unknown module" and provision.js's federation load MODULE_NOT_FOUNDs (task 1002235, same rationale as provisioning above). default:false — shipped ≠ enabled; a vanilla/self-hosted instance never turns it on.
|