@bongos/core 1.19.1072 → 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 +6 -2
- 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 +2 -1
- 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 +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 +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 +84 -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/tests/box_auth_check.mjs
DELETED
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
// tests/box_auth_check.mjs
|
|
2
|
-
//
|
|
3
|
-
// Unit tests for the dev-box session-start auth gate (task 1712). The pure core
|
|
4
|
-
// `decideBoxAuth` is the high-risk surface: it decides whether a box session is
|
|
5
|
-
// told to STOP and re-auth before it can safely claim/ship. The truth table
|
|
6
|
-
// must be exactly right — box-scoped (source='box', ADR 0053) is the incident
|
|
7
|
-
// case and must be caught from the local file alone (no network), an expired
|
|
8
|
-
// real token must still be caught via the bounded /me probe, and any inability
|
|
9
|
-
// to verify must FAIL OPEN (silent) rather than cry wolf.
|
|
10
|
-
//
|
|
11
|
-
// Importing the helper must NOT run its main() (which reads the session file and
|
|
12
|
-
// hits the network) — box-auth-check.js guards main() behind
|
|
13
|
-
// `require.main === module`, and only require()s cli-lib inside main(), so this
|
|
14
|
-
// import is side-effect free (no I/O, no fetch). This test proves that holds.
|
|
15
|
-
//
|
|
16
|
-
// Run: node tests/box_auth_check.mjs
|
|
17
|
-
|
|
18
|
-
import { strict as assert } from 'node:assert';
|
|
19
|
-
import { createRequire } from 'node:module';
|
|
20
|
-
|
|
21
|
-
const require = createRequire(import.meta.url);
|
|
22
|
-
const { decideBoxAuth } = require('../scripts/gds/box-auth-check.js');
|
|
23
|
-
|
|
24
|
-
let passed = 0;
|
|
25
|
-
let failed = 0;
|
|
26
|
-
function test(name, fn) {
|
|
27
|
-
try {
|
|
28
|
-
fn();
|
|
29
|
-
passed++;
|
|
30
|
-
console.log(` ok ${name}`);
|
|
31
|
-
} catch (err) {
|
|
32
|
-
failed++;
|
|
33
|
-
console.log(` FAIL ${name}`);
|
|
34
|
-
console.log(` ${err.message}`);
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// A real, validated session on a box — the common clear-to-work case.
|
|
39
|
-
const OK = { onBox: true, hasToken: true, isBoxScoped: false, meStatus: 200 };
|
|
40
|
-
|
|
41
|
-
// ---------- skip: off a box, this check does nothing ----------
|
|
42
|
-
|
|
43
|
-
test('not a box → skip (laptops are untouched, whatever the token looks like)', () => {
|
|
44
|
-
assert.equal(decideBoxAuth({ onBox: false, hasToken: false, isBoxScoped: false, meStatus: null }).action, 'skip');
|
|
45
|
-
assert.equal(decideBoxAuth({ onBox: false, hasToken: true, isBoxScoped: true, meStatus: 403 }).action, 'skip');
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
// ---------- setup: on a box with no session ----------
|
|
49
|
-
|
|
50
|
-
test('box, no session → setup', () => {
|
|
51
|
-
const d = decideBoxAuth({ onBox: true, hasToken: false, isBoxScoped: false, meStatus: null });
|
|
52
|
-
assert.equal(d.action, 'setup');
|
|
53
|
-
assert.equal(d.reason, 'no-session');
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
// ---------- reauth: the box-scoped incident case (decided locally, no network) ----------
|
|
57
|
-
|
|
58
|
-
test('box, box-scoped token → reauth (box-scoped), regardless of meStatus', () => {
|
|
59
|
-
const d = decideBoxAuth({ onBox: true, hasToken: true, isBoxScoped: true, meStatus: null });
|
|
60
|
-
assert.equal(d.action, 'reauth');
|
|
61
|
-
assert.equal(d.reason, 'box-scoped');
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
test('box-scoped is decided BEFORE meStatus — even a 200 stays reauth', () => {
|
|
65
|
-
// A box token can never legitimately 200 on /me, but the ordering guarantee is
|
|
66
|
-
// what keeps the incident case off the network path.
|
|
67
|
-
const d = decideBoxAuth({ onBox: true, hasToken: true, isBoxScoped: true, meStatus: 200 });
|
|
68
|
-
assert.equal(d.action, 'reauth');
|
|
69
|
-
assert.equal(d.reason, 'box-scoped');
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
// ---------- reauth: an expired/revoked real token (caught via the bounded /me) ----------
|
|
73
|
-
|
|
74
|
-
test('box, real token, /me 401 → reauth (expired-or-revoked)', () => {
|
|
75
|
-
const d = decideBoxAuth({ ...OK, meStatus: 401 });
|
|
76
|
-
assert.equal(d.action, 'reauth');
|
|
77
|
-
assert.equal(d.reason, 'expired-or-revoked');
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
test('box, real token, /me 403 → reauth (expired-or-revoked)', () => {
|
|
81
|
-
const d = decideBoxAuth({ ...OK, meStatus: 403 });
|
|
82
|
-
assert.equal(d.action, 'reauth');
|
|
83
|
-
assert.equal(d.reason, 'expired-or-revoked');
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
// ---------- ok: a real, validated session ----------
|
|
87
|
-
|
|
88
|
-
test('box, real token, /me 200 → ok (clear to work)', () => {
|
|
89
|
-
const d = decideBoxAuth(OK);
|
|
90
|
-
assert.equal(d.action, 'ok');
|
|
91
|
-
assert.equal(d.reason, 'validated');
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
// ---------- fail-open: couldn't verify → stay silent, never warn falsely ----------
|
|
95
|
-
|
|
96
|
-
test('box, real token, /me not attempted (null) → skip (fail-open, no false alarm)', () => {
|
|
97
|
-
const d = decideBoxAuth({ ...OK, meStatus: null });
|
|
98
|
-
assert.equal(d.action, 'skip');
|
|
99
|
-
assert.equal(d.reason, 'unverified');
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
test('box, real token, /me 500 (server error) → skip (not a definitive auth failure)', () => {
|
|
103
|
-
const d = decideBoxAuth({ ...OK, meStatus: 500 });
|
|
104
|
-
assert.equal(d.action, 'skip');
|
|
105
|
-
assert.equal(d.reason, 'unverified');
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
test('box, real token, /me 502 (gateway) → skip (fail-open)', () => {
|
|
109
|
-
const d = decideBoxAuth({ ...OK, meStatus: 502 });
|
|
110
|
-
assert.equal(d.action, 'skip');
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
console.log(`\n${passed} passed, ${failed} failed`);
|
|
114
|
-
process.exit(failed === 0 ? 0 : 1);
|
|
@@ -1,129 +0,0 @@
|
|
|
1
|
-
// tests/box_code_staleness.mjs
|
|
2
|
-
//
|
|
3
|
-
// Unit smoke for boxes.computeCodeStaleness (idea 332 / task 1316, ADR 0072) —
|
|
4
|
-
// the pure "is this dev box running stale code?" decision behind the hall's
|
|
5
|
-
// "rebuild to update" flag. No DB, no network.
|
|
6
|
-
//
|
|
7
|
-
// Run: node tests/box_code_staleness.mjs
|
|
8
|
-
|
|
9
|
-
import { strict as assert } from 'node:assert';
|
|
10
|
-
import { createRequire } from 'node:module';
|
|
11
|
-
|
|
12
|
-
const require = createRequire(import.meta.url);
|
|
13
|
-
const boxes = require('../modules/dev-box/boxes.js');
|
|
14
|
-
|
|
15
|
-
let passed = 0;
|
|
16
|
-
let failed = 0;
|
|
17
|
-
function t(name, fn) {
|
|
18
|
-
try { fn(); passed++; console.log(` PASS ${name}`); }
|
|
19
|
-
catch (e) { failed++; console.log(` FAIL ${name}\n ${e.message}`); }
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const NOW = Date.UTC(2026, 5, 20, 12, 0, 0); // fixed "now" so cases are deterministic
|
|
23
|
-
const iso = (ms) => new Date(ms).toISOString();
|
|
24
|
-
const HOUR = 3600 * 1000;
|
|
25
|
-
const DAY = 24 * HOUR;
|
|
26
|
-
|
|
27
|
-
console.log('computeCodeStaleness:');
|
|
28
|
-
|
|
29
|
-
t('a parked/destroyed box is never judged (not_active)', () => {
|
|
30
|
-
for (const state of ['parked', 'destroyed', 'provisioning', 'none']) {
|
|
31
|
-
const r = boxes.computeCodeStaleness({ state, now: NOW });
|
|
32
|
-
assert.equal(r.stale, false, state);
|
|
33
|
-
assert.equal(r.reason, 'not_active', state);
|
|
34
|
-
}
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
t('active + never reported + just provisioned → not stale (awaiting first report)', () => {
|
|
38
|
-
const r = boxes.computeCodeStaleness({
|
|
39
|
-
state: 'active', provisionedAt: iso(NOW - 5 * 60 * 1000), codeReportedAt: null, now: NOW,
|
|
40
|
-
});
|
|
41
|
-
assert.equal(r.stale, false);
|
|
42
|
-
assert.equal(r.reason, 'awaiting_first_report');
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
t('active + never reported + past the provision grace → NOT stale (never_reported is an unknown, task 1002698)', () => {
|
|
46
|
-
const r = boxes.computeCodeStaleness({
|
|
47
|
-
state: 'active', provisionedAt: iso(NOW - 13 * DAY), codeReportedAt: null, now: NOW,
|
|
48
|
-
});
|
|
49
|
-
assert.equal(r.stale, false);
|
|
50
|
-
assert.equal(r.reason, 'never_reported');
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
// Regression (task 1002698): the whole fleet reported never_reported because the
|
|
54
|
-
// reporter ADR 0072 specified was never written (task 1002699). Silence must never
|
|
55
|
-
// be enough on its own to tell a builder to destroy a working box.
|
|
56
|
-
t('silence alone never flags a box — a report is required to call it stale', () => {
|
|
57
|
-
for (const age of [31 * 60 * 1000, 13 * DAY, 400 * DAY]) {
|
|
58
|
-
const r = boxes.computeCodeStaleness({
|
|
59
|
-
state: 'active', provisionedAt: iso(NOW - age), codeReportedAt: null,
|
|
60
|
-
serverCommittedAt: iso(NOW - HOUR), now: NOW,
|
|
61
|
-
});
|
|
62
|
-
assert.equal(r.stale, false, `age=${age}`);
|
|
63
|
-
}
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
t('active + reported a commit within the lag window → current', () => {
|
|
67
|
-
const r = boxes.computeCodeStaleness({
|
|
68
|
-
state: 'active',
|
|
69
|
-
provisionedAt: iso(NOW - 30 * DAY),
|
|
70
|
-
codeReportedAt: iso(NOW - 5 * 60 * 1000),
|
|
71
|
-
codeCommittedAt: iso(NOW - 3 * HOUR), // box code is 3h behind deployed
|
|
72
|
-
serverCommittedAt: iso(NOW - 1 * HOUR), // deployed commit is 1h old
|
|
73
|
-
now: NOW,
|
|
74
|
-
});
|
|
75
|
-
assert.equal(r.stale, false);
|
|
76
|
-
assert.equal(r.reason, 'current');
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
t('active + reported a commit > 2 days behind deployed → STALE (behind)', () => {
|
|
80
|
-
const r = boxes.computeCodeStaleness({
|
|
81
|
-
state: 'active',
|
|
82
|
-
provisionedAt: iso(NOW - 30 * DAY),
|
|
83
|
-
codeReportedAt: iso(NOW - 5 * 60 * 1000),
|
|
84
|
-
codeCommittedAt: iso(NOW - 9 * DAY), // box stuck 9 days behind (pull not ff)
|
|
85
|
-
serverCommittedAt: iso(NOW - 1 * HOUR),
|
|
86
|
-
now: NOW,
|
|
87
|
-
});
|
|
88
|
-
assert.equal(r.stale, true);
|
|
89
|
-
assert.equal(r.reason, 'behind');
|
|
90
|
-
assert.ok(r.lag_ms > 2 * DAY);
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
t('reported but no commit dates to compare → not stale (unknown, never false-alarm)', () => {
|
|
94
|
-
const r = boxes.computeCodeStaleness({
|
|
95
|
-
state: 'active', provisionedAt: iso(NOW - 30 * DAY),
|
|
96
|
-
codeReportedAt: iso(NOW - 60 * 1000), codeCommittedAt: null, serverCommittedAt: null, now: NOW,
|
|
97
|
-
});
|
|
98
|
-
assert.equal(r.stale, false);
|
|
99
|
-
assert.equal(r.reason, 'unknown');
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
t('accepts Date objects as well as ISO strings', () => {
|
|
103
|
-
const r = boxes.computeCodeStaleness({
|
|
104
|
-
state: 'active',
|
|
105
|
-
provisionedAt: new Date(NOW - 30 * DAY),
|
|
106
|
-
codeReportedAt: new Date(NOW - 60 * 1000),
|
|
107
|
-
codeCommittedAt: new Date(NOW - 10 * DAY),
|
|
108
|
-
serverCommittedAt: new Date(NOW - HOUR),
|
|
109
|
-
now: NOW,
|
|
110
|
-
});
|
|
111
|
-
assert.equal(r.stale, true);
|
|
112
|
-
assert.equal(r.reason, 'behind');
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
t('exactly at the 2-day lag boundary is NOT stale (strictly greater)', () => {
|
|
116
|
-
const r = boxes.computeCodeStaleness({
|
|
117
|
-
state: 'active',
|
|
118
|
-
provisionedAt: iso(NOW - 30 * DAY),
|
|
119
|
-
codeReportedAt: iso(NOW - 60 * 1000),
|
|
120
|
-
codeCommittedAt: iso(NOW - (2 * DAY + HOUR)),
|
|
121
|
-
serverCommittedAt: iso(NOW - HOUR), // lag exactly 2 days
|
|
122
|
-
now: NOW,
|
|
123
|
-
});
|
|
124
|
-
assert.equal(r.stale, false);
|
|
125
|
-
assert.equal(r.reason, 'current');
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
console.log(`\n${passed} passed, ${failed} failed`);
|
|
129
|
-
process.exit(failed ? 1 : 0);
|
|
@@ -1,196 +0,0 @@
|
|
|
1
|
-
// tests/box_connect_e2e.mjs
|
|
2
|
-
//
|
|
3
|
-
// END-TO-END smoke for the builder bootstrap (#685, ADR 0031 §1): runs the REAL
|
|
4
|
-
// infra/box-connect.sh against a local stub server and asserts the whole flow.
|
|
5
|
-
// NO production, NO Postgres, NO GitHub, NO real box — but the genuine shell
|
|
6
|
-
// script, the genuine box router handlers (POST /box/ssh-key, GET
|
|
7
|
-
// /box/managed-settings), and a stubbed device-flow.
|
|
8
|
-
//
|
|
9
|
-
// What it proves a real builder's `curl … | bash` would do:
|
|
10
|
-
// 1. generates an SSH key at OTB_SSH_KEY (private half stays local),
|
|
11
|
-
// 2. device-flow signs in and registers the PUBLIC key — and the fingerprint
|
|
12
|
-
// the server computes matches the key on disk,
|
|
13
|
-
// 3. downloads + installs managed-settings.json at the install dir,
|
|
14
|
-
// 4. exits 0.
|
|
15
|
-
//
|
|
16
|
-
// Hermetic + deterministic (the device-flow poll returns 'ok' immediately; a
|
|
17
|
-
// PATH shim no-ops `open`/`xdg-open` so no browser pops). Requires bash, curl,
|
|
18
|
-
// ssh-keygen — present on macOS and the CI ubuntu runner.
|
|
19
|
-
//
|
|
20
|
-
// Run: node tests/box_connect_e2e.mjs
|
|
21
|
-
|
|
22
|
-
import { strict as assert } from 'node:assert';
|
|
23
|
-
import { createRequire } from 'node:module';
|
|
24
|
-
import http from 'node:http';
|
|
25
|
-
import fs from 'node:fs';
|
|
26
|
-
import os from 'node:os';
|
|
27
|
-
import path from 'node:path';
|
|
28
|
-
import { spawn } from 'node:child_process';
|
|
29
|
-
|
|
30
|
-
const require = createRequire(import.meta.url);
|
|
31
|
-
const express = require('express');
|
|
32
|
-
const auth = require('../src/bongos/auth.js');
|
|
33
|
-
const o = require('../modules/dev-box/box-onboard.js');
|
|
34
|
-
const gdsDb = require('../src/bongos/db.js');
|
|
35
|
-
const boxes = require('../modules/dev-box/boxes.js');
|
|
36
|
-
const boxAccess = require('../modules/dev-box/box-access.js');
|
|
37
|
-
|
|
38
|
-
// Async spawn helper: run a real shell script against the in-process stub server
|
|
39
|
-
// without blocking the event loop (spawnSync would deadlock the HTTP server).
|
|
40
|
-
function runScript(scriptArgs, extraEnv) {
|
|
41
|
-
return new Promise((resolve) => {
|
|
42
|
-
const child = spawn('bash', scriptArgs, { env: { ...process.env, ...extraEnv } });
|
|
43
|
-
let stdout = '';
|
|
44
|
-
let stderr = '';
|
|
45
|
-
child.stdout.on('data', (d) => { stdout += d; });
|
|
46
|
-
child.stderr.on('data', (d) => { stderr += d; });
|
|
47
|
-
const killer = setTimeout(() => child.kill('SIGKILL'), 60000);
|
|
48
|
-
child.on('close', (status) => { clearTimeout(killer); resolve({ status, stdout, stderr }); });
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
let passed = 0;
|
|
53
|
-
let failed = 0;
|
|
54
|
-
function t(name, fn) {
|
|
55
|
-
try { fn(); passed++; console.log(` PASS ${name}`); }
|
|
56
|
-
catch (e) { failed++; console.log(` FAIL ${name}\n ${e.message}`); }
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// ── stub the trust boundary + the DB write the connect flow hits ────────────
|
|
60
|
-
let registered = null;
|
|
61
|
-
let CURRENT_DB_BUILDER = { rank: 'metic', status: 'active' }; // flipped to test the clawback
|
|
62
|
-
auth.requireBuilder = (req, _res, next) => { req.builder = { id: 1, github_login: 'e2etester', rank: 'metic' }; next(); };
|
|
63
|
-
auth.requireRank = () => (_req, _res, next) => next();
|
|
64
|
-
o.registerSshKey = async (_db, _id, k) => { registered = k; return { id: 1, key_type: k.keyType, fingerprint: k.fingerprint, label: k.label }; };
|
|
65
|
-
// Stubs for the GET /box/authorized-keys path (box-side sync test below).
|
|
66
|
-
gdsDb.getBuilderById = async () => CURRENT_DB_BUILDER;
|
|
67
|
-
boxes.getBoxByBuilderId = async () => ({ id: 1, builder_id: 1, state: 'active', scope: 'full' });
|
|
68
|
-
boxAccess.recordSourceEvent = async () => {};
|
|
69
|
-
o.listSshKeys = async () => ([{ public_key: 'ssh-ed25519 BUILDERKEYAAA', label: 'laptop' }]);
|
|
70
|
-
o.touchSshKeysUsed = async () => {};
|
|
71
|
-
|
|
72
|
-
const buildBoxRouter = require('../modules/dev-box/routes/box.js');
|
|
73
|
-
const app = express();
|
|
74
|
-
app.use(express.json());
|
|
75
|
-
// Stub device-flow: start hands back a code, poll immediately approves.
|
|
76
|
-
app.post('/api/gds/auth/device/start', (_req, res) =>
|
|
77
|
-
res.json({ device_code: 'dc-e2e', user_code: 'WXYZ-1234', verification_uri: 'http://127.0.0.1:9/device', interval: 1, expires_in: 30 }));
|
|
78
|
-
app.post('/api/gds/auth/device/poll', (_req, res) => res.json({ state: 'ok', token: 'e2e-token' }));
|
|
79
|
-
app.use('/api/gds', buildBoxRouter());
|
|
80
|
-
|
|
81
|
-
const server = http.createServer(app);
|
|
82
|
-
await new Promise((r) => server.listen(0, r));
|
|
83
|
-
const port = server.address().port;
|
|
84
|
-
|
|
85
|
-
// ── run the REAL bootstrap with test overrides ──────────────────────────────
|
|
86
|
-
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'otb-e2e-'));
|
|
87
|
-
const keyPath = path.join(tmp, 'otb_builder');
|
|
88
|
-
const settingsDir = path.join(tmp, 'claude');
|
|
89
|
-
// PATH shim so the script's best-effort `open <url>` doesn't launch a browser.
|
|
90
|
-
const shimDir = path.join(tmp, 'bin');
|
|
91
|
-
fs.mkdirSync(shimDir);
|
|
92
|
-
for (const cmd of ['open', 'xdg-open']) {
|
|
93
|
-
fs.writeFileSync(path.join(shimDir, cmd), '#!/bin/sh\nexit 0\n', { mode: 0o755 });
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// Async spawn (NOT spawnSync): the stub server runs in THIS process's event
|
|
97
|
-
// loop, so a synchronous spawn would block it and the child's HTTP calls would
|
|
98
|
-
// hang. spawn + await-on-close keeps the loop free to answer the bootstrap.
|
|
99
|
-
const run = await new Promise((resolve) => {
|
|
100
|
-
const child = spawn('bash', ['infra/box-connect.sh'], {
|
|
101
|
-
env: {
|
|
102
|
-
...process.env,
|
|
103
|
-
PATH: `${shimDir}:${process.env.PATH}`,
|
|
104
|
-
GDS_API_BASE: `http://127.0.0.1:${port}`,
|
|
105
|
-
OTB_SSH_KEY: keyPath,
|
|
106
|
-
OTB_MANAGED_SETTINGS_DIR: settingsDir,
|
|
107
|
-
},
|
|
108
|
-
});
|
|
109
|
-
let stdout = '';
|
|
110
|
-
let stderr = '';
|
|
111
|
-
child.stdout.on('data', (d) => { stdout += d; });
|
|
112
|
-
child.stderr.on('data', (d) => { stderr += d; });
|
|
113
|
-
const killer = setTimeout(() => child.kill('SIGKILL'), 60000);
|
|
114
|
-
child.on('close', (status) => { clearTimeout(killer); resolve({ status, stdout, stderr }); });
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
console.log('box-connect.sh end-to-end (real script, stub server):');
|
|
118
|
-
|
|
119
|
-
t('exits 0', () => {
|
|
120
|
-
assert.equal(run.status, 0, `non-zero exit. stdout:\n${run.stdout}\nstderr:\n${run.stderr}`);
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
t('generated an SSH keypair at OTB_SSH_KEY (private + public)', () => {
|
|
124
|
-
assert.ok(fs.existsSync(keyPath), 'private key missing');
|
|
125
|
-
assert.ok(fs.existsSync(`${keyPath}.pub`), 'public key missing');
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
t('registered the public key, and the server-computed fingerprint matches the key on disk', () => {
|
|
129
|
-
assert.ok(registered, 'registerSshKey was never called');
|
|
130
|
-
const pub = fs.readFileSync(`${keyPath}.pub`, 'utf8');
|
|
131
|
-
const expected = o.validateSshPublicKey(pub);
|
|
132
|
-
assert.equal(registered.fingerprint, expected.fingerprint, 'fingerprint mismatch');
|
|
133
|
-
// The script trims the comment before sending; stored normalized = "<type> <base64>".
|
|
134
|
-
assert.equal(registered.publicKey, expected.normalized);
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
t('installed managed-settings.json at the override dir, keyed off the session login', () => {
|
|
138
|
-
const f = path.join(settingsDir, 'managed-settings.json');
|
|
139
|
-
assert.ok(fs.existsSync(f), 'managed-settings.json not written');
|
|
140
|
-
const s = JSON.parse(fs.readFileSync(f, 'utf8'));
|
|
141
|
-
assert.equal(s.sshConfigs[0].sshHost, 'root@box-e2etester.dev.example.com');
|
|
142
|
-
assert.deepEqual(s.sshHostAllowlist, ['*.dev.example.com']);
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
t('did NOT leak the GDS token into stdout/stderr', () => {
|
|
146
|
-
assert.ok(!`${run.stdout}${run.stderr}`.includes('e2e-token'), 'token leaked to output');
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
// ── box-side: box-authorized-keys-fetch.sh against the real route ───────────
|
|
150
|
-
console.log('\nbox-authorized-keys-fetch.sh end-to-end (real script, stub server):');
|
|
151
|
-
|
|
152
|
-
const sessionFile = path.join(tmp, 'gds-session.json');
|
|
153
|
-
fs.writeFileSync(sessionFile, JSON.stringify({ token: 'e2e-token' }));
|
|
154
|
-
const akFile = path.join(tmp, 'authorized_keys');
|
|
155
|
-
const OPERATOR_LINE = 'ssh-ed25519 OPERATORKEYZZZ operator@control-plane';
|
|
156
|
-
fs.writeFileSync(akFile, OPERATOR_LINE + '\n'); // pre-seed operator key (must survive)
|
|
157
|
-
|
|
158
|
-
const akEnv = {
|
|
159
|
-
GDS_BASE: `http://127.0.0.1:${port}/api/gds`,
|
|
160
|
-
GDS_SESSION: sessionFile,
|
|
161
|
-
AUTHORIZED_KEYS: akFile,
|
|
162
|
-
};
|
|
163
|
-
|
|
164
|
-
// Scenario A — active builder: managed block synced, operator key preserved.
|
|
165
|
-
CURRENT_DB_BUILDER = { rank: 'metic', status: 'active' };
|
|
166
|
-
const sync = await runScript(['infra/box-authorized-keys-fetch.sh'], akEnv);
|
|
167
|
-
|
|
168
|
-
t('active builder: fetch exits 0', () => {
|
|
169
|
-
assert.equal(sync.status, 0, `stdout:\n${sync.stdout}\nstderr:\n${sync.stderr}`);
|
|
170
|
-
});
|
|
171
|
-
t('active builder: managed block holds the builder key AND the operator key survives', () => {
|
|
172
|
-
const ak = fs.readFileSync(akFile, 'utf8');
|
|
173
|
-
assert.ok(ak.includes('BUILDERKEYAAA'), 'builder key not synced');
|
|
174
|
-
assert.ok(ak.includes(OPERATOR_LINE), 'operator key clobbered');
|
|
175
|
-
assert.ok(ak.includes(o.MANAGED_BLOCK_BEGIN) && ak.includes(o.MANAGED_BLOCK_END), 'markers missing');
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
// Scenario B — deactivated builder: 403 clawback empties the block, operator stays.
|
|
179
|
-
CURRENT_DB_BUILDER = { rank: 'metic', status: 'inactive' };
|
|
180
|
-
const claw = await runScript(['infra/box-authorized-keys-fetch.sh'], akEnv);
|
|
181
|
-
|
|
182
|
-
t('deactivated builder: fetch exits 3 (revoked)', () => {
|
|
183
|
-
assert.equal(claw.status, 3, `stdout:\n${claw.stdout}\nstderr:\n${claw.stderr}`);
|
|
184
|
-
});
|
|
185
|
-
t('deactivated builder: managed block emptied (login cut), operator key STILL present', () => {
|
|
186
|
-
const ak = fs.readFileSync(akFile, 'utf8');
|
|
187
|
-
assert.ok(!ak.includes('BUILDERKEYAAA'), 'builder key NOT revoked — still in authorized_keys!');
|
|
188
|
-
assert.ok(ak.includes(OPERATOR_LINE), 'operator key clobbered by the clawback');
|
|
189
|
-
assert.ok(ak.includes(o.MANAGED_BLOCK_BEGIN), 'managed block markers should remain (empty)');
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
await new Promise((r) => server.close(r));
|
|
193
|
-
fs.rmSync(tmp, { recursive: true, force: true });
|
|
194
|
-
|
|
195
|
-
console.log(`\nbox_connect_e2e.mjs: ${passed} passed, ${failed} failed`);
|
|
196
|
-
process.exit(failed === 0 ? 0 : 1);
|
package/tests/box_cost.mjs
DELETED
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
// tests/box_cost.mjs — pure-function coverage for the container cost ledger
|
|
2
|
-
// (#602): storageCostUsd (parked-interval cost) + summarizeBoxCost (bucket
|
|
3
|
-
// cost_log rows into spinup/compute/storage/total). The DB writers
|
|
4
|
-
// (recordSpinupCost/recordStorageCost) and the per-builder summary queries take a
|
|
5
|
-
// `db` param and are exercised by the integration smoke, not here. No DB, no clock.
|
|
6
|
-
//
|
|
7
|
-
// Run: node tests/box_cost.mjs (also runs in the unit gate)
|
|
8
|
-
|
|
9
|
-
import { strict as assert } from 'node:assert';
|
|
10
|
-
import { createRequire } from 'node:module';
|
|
11
|
-
|
|
12
|
-
const require = createRequire(import.meta.url);
|
|
13
|
-
const boxes = require('../modules/dev-box/boxes.js');
|
|
14
|
-
|
|
15
|
-
let passed = 0, failed = 0;
|
|
16
|
-
const t = (n, f) => { try { f(); passed++; console.log(` PASS ${n}`); } catch (e) { failed++; console.log(` FAIL ${n}\n ${e.message}`); } };
|
|
17
|
-
|
|
18
|
-
t('storageCostUsd: hours parked × snapshot hourly rate (monthly/730)', () => {
|
|
19
|
-
const hourly = boxes.SNAPSHOT_MONTHLY_USD / 730;
|
|
20
|
-
// 730h parked ≈ one month
|
|
21
|
-
const month = boxes.storageCostUsd({ parkedSinceMs: 0, untilMs: 730 * 3_600_000 });
|
|
22
|
-
assert.ok(Math.abs(month - boxes.SNAPSHOT_MONTHLY_USD) < 0.001, `~monthly: got ${month}`);
|
|
23
|
-
// 10h parked
|
|
24
|
-
const ten = boxes.storageCostUsd({ parkedSinceMs: 0, untilMs: 10 * 3_600_000 });
|
|
25
|
-
assert.ok(Math.abs(ten - hourly * 10) < 0.0001, `10h: got ${ten}`);
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
t('storageCostUsd: zero/negative/invalid interval → 0', () => {
|
|
29
|
-
assert.equal(boxes.storageCostUsd({ parkedSinceMs: 100, untilMs: 100 }), 0);
|
|
30
|
-
assert.equal(boxes.storageCostUsd({ parkedSinceMs: 200, untilMs: 100 }), 0);
|
|
31
|
-
assert.equal(boxes.storageCostUsd({}), 0);
|
|
32
|
-
assert.equal(boxes.storageCostUsd({ parkedSinceMs: NaN, untilMs: 1 }), 0);
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
t('summarizeBoxCost: buckets by source into spinup/compute/storage + total', () => {
|
|
36
|
-
const rows = [
|
|
37
|
-
{ source: 'box-spinup', amount_usd: 0 },
|
|
38
|
-
{ source: 'box-spinup', amount_usd: 0.02 },
|
|
39
|
-
{ source: 'box-lifecycle', amount_usd: 1.50 },
|
|
40
|
-
{ source: 'box-lifecycle', amount_usd: 0.25 },
|
|
41
|
-
{ source: 'box-storage', amount_usd: 0.30 },
|
|
42
|
-
];
|
|
43
|
-
const s = boxes.summarizeBoxCost(rows);
|
|
44
|
-
assert.equal(s.spinup_usd, 0.02);
|
|
45
|
-
assert.equal(s.compute_usd, 1.75);
|
|
46
|
-
assert.equal(s.storage_usd, 0.30);
|
|
47
|
-
assert.equal(s.total_usd, 2.07);
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
t('summarizeBoxCost: empty / null → all zeros', () => {
|
|
51
|
-
assert.deepEqual(boxes.summarizeBoxCost([]), { spinup_usd: 0, compute_usd: 0, storage_usd: 0, total_usd: 0 });
|
|
52
|
-
assert.deepEqual(boxes.summarizeBoxCost(null), { spinup_usd: 0, compute_usd: 0, storage_usd: 0, total_usd: 0 });
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
t('summarizeBoxCost: an unknown source still counts toward total (no silent drop)', () => {
|
|
56
|
-
const s = boxes.summarizeBoxCost([{ source: 'box-mystery', amount_usd: 0.5 }]);
|
|
57
|
-
assert.equal(s.total_usd, 0.5);
|
|
58
|
-
assert.equal(s.compute_usd, 0);
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
t('BOX_COST_SOURCES covers exactly the three box ledger sources', () => {
|
|
62
|
-
assert.deepEqual([...boxes.BOX_COST_SOURCES].sort(), ['box-lifecycle', 'box-spinup', 'box-storage']);
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
t('SPINUP_COST_USD defaults to 0 (DO charges no provision fee; the row is an event marker)', () => {
|
|
66
|
-
assert.equal(boxes.SPINUP_COST_USD, 0);
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
console.log(`\nbox_cost: ${passed} passed, ${failed} failed`);
|
|
70
|
-
process.exit(failed ? 1 : 0);
|
package/tests/box_credential.mjs
DELETED
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
// tests/box_credential.mjs
|
|
2
|
-
//
|
|
3
|
-
// R102 (goal 1000051, ADR 0148 C4) — VERIFY the server-side-token posture holds
|
|
4
|
-
// once source scope is CLAIM-based (R92–R94). The security property task-scoping
|
|
5
|
-
// must not weaken: the GitHub credential is server-issued per fetch from the prod
|
|
6
|
-
// ENV (never the repo — ADR 0022), the box never receives a fabricated token, and
|
|
7
|
-
// the claim-scoped ACCESS DECISION carries NO secret — the token is minted
|
|
8
|
-
// SEPARATELY, so changing what the box materializes (sparse paths) never touches
|
|
9
|
-
// the token model.
|
|
10
|
-
//
|
|
11
|
-
// Pure + hermetic: makeEnvCredentialProvider takes an injected env; decideSourceAccess
|
|
12
|
-
// is pure. NO env, NO network, NO box. The DEFERRED hardening (short-lived per-claim
|
|
13
|
-
// credential + hard "can't over-pull" isolation) is documented in ADR 0148 C4 /
|
|
14
|
-
// Consequences — this test pins the posture that ships TODAY.
|
|
15
|
-
//
|
|
16
|
-
// Run: node tests/box_credential.mjs
|
|
17
|
-
|
|
18
|
-
import { strict as assert } from 'node:assert';
|
|
19
|
-
import { createRequire } from 'node:module';
|
|
20
|
-
|
|
21
|
-
const require = createRequire(import.meta.url);
|
|
22
|
-
const cred = require('../modules/dev-box/box-credential.js');
|
|
23
|
-
const access = require('../modules/dev-box/box-access.js');
|
|
24
|
-
const scopeMap = require('../src/bongos/module-scope-map.js');
|
|
25
|
-
|
|
26
|
-
let passed = 0;
|
|
27
|
-
let failed = 0;
|
|
28
|
-
function t(name, fn) {
|
|
29
|
-
try { fn(); passed++; console.log(` PASS ${name}`); }
|
|
30
|
-
catch (e) { failed++; console.log(` FAIL ${name}\n ${e.message}`); }
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const REPO = 'https://github.com/acme/portable-core.git';
|
|
34
|
-
const STARTER_REPO = 'https://github.com/acme/starter-surface.git';
|
|
35
|
-
|
|
36
|
-
console.log('box-credential — the token is SERVER-ENV sourced, never fabricated (ADR 0022):');
|
|
37
|
-
|
|
38
|
-
t('no env configured → configured:false + token:null (the box gets guidance, not a token)', () => {
|
|
39
|
-
const p = cred.makeEnvCredentialProvider({});
|
|
40
|
-
for (const scope of ['full', 'starter']) {
|
|
41
|
-
const c = p.issue(scope, { nowMs: 0 });
|
|
42
|
-
assert.equal(c.configured, false, `${scope}: unconfigured`);
|
|
43
|
-
assert.equal(c.token, null, `${scope}: NO token is ever fabricated`);
|
|
44
|
-
assert.equal(c.repoUrl, null, `${scope}: no repo without env`);
|
|
45
|
-
assert.match(c.guidance || '', /env/i, `${scope}: guidance points at the prod env`);
|
|
46
|
-
}
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
t('repo set but token unset → still configured:false (no partial/empty-token leak)', () => {
|
|
50
|
-
const c = cred.makeEnvCredentialProvider({ BOX_SOURCE_REPO_URL: REPO }).issue('full', { nowMs: 0 });
|
|
51
|
-
assert.equal(c.configured, false);
|
|
52
|
-
assert.equal(c.token, null);
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
t('token comes STRAIGHT from the env var — and lands ONLY in .token, no other field', () => {
|
|
56
|
-
const p = cred.makeEnvCredentialProvider({
|
|
57
|
-
BOX_SOURCE_REPO_URL: REPO,
|
|
58
|
-
BOX_SOURCE_FULL_TOKEN: 'FULL-SECRET-abc',
|
|
59
|
-
BOX_SOURCE_STARTER_TOKEN: 'STARTER-SECRET-xyz',
|
|
60
|
-
});
|
|
61
|
-
const full = p.issue('full', { nowMs: 0 });
|
|
62
|
-
assert.equal(full.token, 'FULL-SECRET-abc', 'full scope → the FULL env token, verbatim');
|
|
63
|
-
const starter = p.issue('starter', { nowMs: 0 });
|
|
64
|
-
assert.equal(starter.token, 'STARTER-SECRET-xyz', 'starter scope → the STARTER env token, verbatim');
|
|
65
|
-
// The secret must appear in NO field other than `token` (scope/repoUrl/guidance/
|
|
66
|
-
// expiresAt/ttl are safe to log + audit — see the source_grant audit).
|
|
67
|
-
for (const c of [full, starter]) {
|
|
68
|
-
for (const [k, v] of Object.entries(c)) {
|
|
69
|
-
if (k === 'token') continue;
|
|
70
|
-
assert.ok(!String(v).includes(c.token), `secret must not leak into .${k}`);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
console.log('\nbox-credential — the credential SCOPE is rank-based; scope can never over-reach the repo:');
|
|
76
|
-
|
|
77
|
-
t('a separate starter repo is a HARD boundary: only "full" reaches the full repo', () => {
|
|
78
|
-
const p = cred.makeEnvCredentialProvider({
|
|
79
|
-
BOX_SOURCE_REPO_URL: REPO,
|
|
80
|
-
BOX_SOURCE_STARTER_REPO_URL: STARTER_REPO,
|
|
81
|
-
BOX_SOURCE_FULL_TOKEN: 'F',
|
|
82
|
-
BOX_SOURCE_STARTER_TOKEN: 'S',
|
|
83
|
-
});
|
|
84
|
-
assert.equal(p.issue('full', { nowMs: 0 }).repoUrl, REPO, 'full → the full repo');
|
|
85
|
-
const starter = p.issue('starter', { nowMs: 0 });
|
|
86
|
-
assert.equal(starter.repoUrl, STARTER_REPO, 'starter → the SEPARATE starter repo, cannot reach the full one');
|
|
87
|
-
assert.equal(starter.separateStarterRepo, true);
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
t('fail-closed: an UNEXPECTED scope label ("task"/"base") never escalates to the full repo', () => {
|
|
91
|
-
// R94 passes only rank scopes ('starter'|'full') to issue(); this pins the safety
|
|
92
|
-
// margin if a task/base label ever reached it — it takes the NON-full (starter)
|
|
93
|
-
// branch, so on a separate-starter-repo instance it can't reach the crown jewels.
|
|
94
|
-
const p = cred.makeEnvCredentialProvider({
|
|
95
|
-
BOX_SOURCE_REPO_URL: REPO,
|
|
96
|
-
BOX_SOURCE_STARTER_REPO_URL: STARTER_REPO,
|
|
97
|
-
BOX_SOURCE_FULL_TOKEN: 'F',
|
|
98
|
-
BOX_SOURCE_STARTER_TOKEN: 'S',
|
|
99
|
-
});
|
|
100
|
-
for (const label of ['task', 'base', 'wizard', undefined]) {
|
|
101
|
-
const c = p.issue(label, { nowMs: 0 });
|
|
102
|
-
assert.equal(c.repoUrl, STARTER_REPO, `scope='${label}' must NOT reach the full repo`);
|
|
103
|
-
assert.notEqual(c.token, 'F', `scope='${label}' must NOT get the full-only token`);
|
|
104
|
-
}
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
console.log('\ntask-scoping decoupling — the claim-scoped ACCESS DECISION carries NO secret:');
|
|
108
|
-
|
|
109
|
-
t('decideSourceAccess returns scope/mode/sparsePaths only — the token is minted separately', () => {
|
|
110
|
-
const d = access.decideSourceAccess({ rank: 'metic', status: 'active', scopeKeys: ['dev-box'], moduleScopeMap: scopeMap });
|
|
111
|
-
assert.equal(d.allowed, true);
|
|
112
|
-
assert.deepEqual(Object.keys(d.spec).sort(), ['mode', 'scope', 'sparsePaths'], 'the spec is scope/mode/sparsePaths — NO credential field');
|
|
113
|
-
const blob = JSON.stringify(d);
|
|
114
|
-
assert.ok(!/token|credential|secret|ghp_|github_pat/i.test(blob), 'the decision blob holds no secret-shaped field');
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
t('revocation scrubs at the credential layer: an inactive/below-floor builder is denied BEFORE any credential', () => {
|
|
118
|
-
const inactive = access.decideSourceAccess({ rank: 'metic', status: 'inactive', scopeKeys: ['dev-box'], moduleScopeMap: scopeMap });
|
|
119
|
-
assert.equal(inactive.allowed, false, 'inactive → denied (the route never reaches issue())');
|
|
120
|
-
const belowFloor = access.decideSourceAccess({ rank: 'xenos', status: 'active', floor: 'metic', scopeKeys: ['dev-box'], moduleScopeMap: scopeMap });
|
|
121
|
-
assert.equal(belowFloor.allowed, false, 'below floor → denied (no credential path)');
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
console.log(`\nbox_credential.mjs: ${passed} passed, ${failed} failed`);
|
|
125
|
-
process.exit(failed === 0 ? 0 : 1);
|