@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
|
@@ -1,38 +1,28 @@
|
|
|
1
|
-
// tests/government_session_scope.mjs — session-
|
|
2
|
-
//
|
|
1
|
+
// tests/government_session_scope.mjs — the session-source registry (BV1.R99,
|
|
2
|
+
// ADR 0151 §3). DB-free.
|
|
3
3
|
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
4
|
+
// Every builder_sessions row carries a `source`. modules/government/session-scopes.json
|
|
5
|
+
// declares the sources an instance accepts; the kernel (src/bongos/auth.js) DENIES a
|
|
6
|
+
// session whose source it does not declare, on every route (requireBuilder) and every
|
|
7
|
+
// page (runPageGate). Every declared source carries its builder's full rank authority.
|
|
7
8
|
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
9
|
+
// The registry used to be able to declare a NARROWING scope — a deny-by-default
|
|
10
|
+
// allow-list for a machine-baked token. Its only user was the dev box's token (source
|
|
11
|
+
// 'box'), retired by goal 1000120, and the allow-list machinery left with it in task
|
|
12
|
+
// 1003894. What this file now proves:
|
|
13
|
+
// A — both loaders read the same registry, and it declares exactly cli/web/legacy;
|
|
14
|
+
// B — both loaders REFUSE a row that still declares a scope, and a malformed or
|
|
15
|
+
// empty registry, rather than degrading to "every source is accepted";
|
|
16
|
+
// C — requireBuilder denies an undeclared source (a leftover 'box' row included)
|
|
17
|
+
// with 403 session_scope and never calls next(); declared sources pass;
|
|
18
|
+
// D — the page gate agrees (tests/auth_page_gate.mjs holds the fuller page matrix).
|
|
17
19
|
//
|
|
18
|
-
// ORACLE DISCIPLINE
|
|
19
|
-
//
|
|
20
|
-
// • the Archon-box assertions name Archon powers LITERALLY (builder.rank.set,
|
|
21
|
-
// government.manage, deploy.push_main …) and cross-check them against `floor`
|
|
22
|
-
// run through auth.rankMeetsThreshold — the REAL production rank gate, a
|
|
23
|
-
// pre-R99 oracle;
|
|
24
|
-
// • the deny-by-default assertions pin a hand-written expected set, so widening
|
|
25
|
-
// the registry reds this file instead of silently agreeing with it;
|
|
26
|
-
// • the subset property (B4) is checked against resolver.effectivePermissions —
|
|
27
|
-
// the unchanged R95 role answer.
|
|
28
|
-
//
|
|
29
|
-
// End-to-end reachability through the real requireBuilder chain lives in
|
|
30
|
-
// tests/box_scope_session.mjs (the #919 regression guard, unchanged in substance by
|
|
31
|
-
// R99); section G here adds the one case that file cannot express — a session
|
|
32
|
-
// source the registry does not declare at all.
|
|
20
|
+
// ORACLE DISCIPLINE: the expected source list is written by hand, not read from the
|
|
21
|
+
// registry, so declaring a new source reds this file instead of silently agreeing.
|
|
33
22
|
import assert from 'node:assert/strict';
|
|
34
23
|
import { test } from 'node:test';
|
|
35
24
|
import { createRequire } from 'node:module';
|
|
25
|
+
import fs from 'node:fs';
|
|
36
26
|
import path from 'node:path';
|
|
37
27
|
import { fileURLToPath } from 'node:url';
|
|
38
28
|
|
|
@@ -47,44 +37,11 @@ let nextSession = null;
|
|
|
47
37
|
dbKernel.lookupSession = async () => nextSession;
|
|
48
38
|
|
|
49
39
|
const auth = require(path.join(ROOT, 'src', 'bongos', 'auth.js'));
|
|
50
|
-
const catalog = require(path.join(GOV, 'catalog.js'));
|
|
51
|
-
const resolver = require(path.join(GOV, 'resolver.js'));
|
|
52
|
-
const db = require(path.join(GOV, 'db.js'));
|
|
53
40
|
const scopes = require(path.join(GOV, 'session-scopes.js'));
|
|
54
41
|
const { attachFail } = require(path.join(ROOT, 'src', 'bongos', 'middleware', 'error-envelope.js'));
|
|
55
42
|
|
|
56
|
-
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
const ME = 42;
|
|
60
|
-
const EVERYTHING = permissionKeys();
|
|
61
|
-
|
|
62
|
-
// What a box session is EXPECTED to be able to do — written by hand, deliberately
|
|
63
|
-
// NOT read from the registry. Widening the registry must red this file.
|
|
64
|
-
const EXPECTED_BOX_SCOPE = ['box.manage.own'];
|
|
65
|
-
|
|
66
|
-
// Archon powers, named literally. If a box session ever resolves one of these the
|
|
67
|
-
// #919 containment is gone.
|
|
68
|
-
const ARCHON_POWERS = [
|
|
69
|
-
'builder.rank.set', 'government.manage', 'deploy.push_main', 'version.close',
|
|
70
|
-
'task.confirm.grade_bypass', 'path.protected.modify', 'infra.ssh',
|
|
71
|
-
'builder.roster.read', 'claim.release.any', 'memory.read.any', 'audit_log.read',
|
|
72
|
-
];
|
|
73
|
-
// Metic powers, named literally — the working rank's authority a box token must
|
|
74
|
-
// not inherit either.
|
|
75
|
-
const METIC_POWERS = ['idea.triage', 'task.create', 'task.edit', 'goal.create', 'box.source.full'];
|
|
76
|
-
|
|
77
|
-
// Stub the ONE DB read the resolver makes, so the composition is exercised
|
|
78
|
-
// end-to-end without Postgres (the tests/government_ownership_scope.mjs pattern).
|
|
79
|
-
const realGetPermissionKeys = db.getPermissionKeysForBuilder;
|
|
80
|
-
async function holding(keys, fn) {
|
|
81
|
-
db.getPermissionKeysForBuilder = async () => keys;
|
|
82
|
-
try {
|
|
83
|
-
return await fn();
|
|
84
|
-
} finally {
|
|
85
|
-
db.getPermissionKeysForBuilder = realGetPermissionKeys;
|
|
86
|
-
}
|
|
87
|
-
}
|
|
43
|
+
// Hand-written: the sources this instance accepts.
|
|
44
|
+
const EXPECTED_SOURCES = ['cli', 'web', null];
|
|
88
45
|
|
|
89
46
|
function fakeRes() {
|
|
90
47
|
const res = {
|
|
@@ -97,338 +54,107 @@ function fakeRes() {
|
|
|
97
54
|
return res;
|
|
98
55
|
}
|
|
99
56
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
`${scope.source}: a scope either narrows nothing or names what it allows — never both/neither`);
|
|
109
|
-
assert.ok(Object.isFrozen(scope) && Object.isFrozen(scope.permissions));
|
|
110
|
-
}
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
test('A2: every allow-listed key is a real, operational, non-principal catalog key', () => {
|
|
114
|
-
for (const scope of scopes.SESSION_SCOPES) {
|
|
115
|
-
for (const key of scope.permissions) {
|
|
116
|
-
const permission = byKey(key);
|
|
117
|
-
assert.ok(permission, `${scope.source} allows "${key}", which the catalog does not define`);
|
|
118
|
-
assert.equal(catalog.isPrincipalKey(key), false,
|
|
119
|
-
`${scope.source} allows a machine-principal key — the identity axis never composes with a session scope`);
|
|
120
|
-
assert.equal(permission.system, false,
|
|
121
|
-
`${scope.source} allows a system:true key — a scoped token lives outside the trust boundary`);
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
test('A3: the kernel and the governance module read the SAME registry, identically', () => {
|
|
127
|
-
// The kernel loads it with fs (ADR 0083's one-way rule) while the module requires
|
|
128
|
-
// it as a sibling, so the two normalizations are the thing that could drift.
|
|
129
|
-
assert.deepEqual(auth.SESSION_SCOPES.map((s) => ({ ...s, permissions: [...s.permissions] })),
|
|
130
|
-
scopes.SESSION_SCOPES.map((s) => ({ ...s, permissions: [...s.permissions] })));
|
|
131
|
-
for (const source of scopes.sources()) {
|
|
132
|
-
assert.deepEqual(auth.sessionScopeFor(source), scopes.scopeFor(source));
|
|
133
|
-
}
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
test('A4: the declared box scope is exactly the hand-written expectation', () => {
|
|
137
|
-
// The pin that makes the deny-by-default tests below meaningful: widening the
|
|
138
|
-
// registry must be a conscious edit here, not a silent agreement.
|
|
139
|
-
assert.deepEqual([...scopes.allowedKeys('box')], EXPECTED_BOX_SCOPE);
|
|
140
|
-
assert.equal(scopes.isScoped('box'), true);
|
|
141
|
-
assert.equal(scopes.isScoped('cli'), false);
|
|
142
|
-
assert.equal(scopes.isScoped('web'), false);
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
// ═══ B — THE key test: a box session of an ARCHON ═════════════════════════════
|
|
146
|
-
|
|
147
|
-
test('B1: an ARCHON on a box session resolves ONLY the allow-list, not Archon powers', async () => {
|
|
148
|
-
await holding(seedForRank('archon'), async () => {
|
|
149
|
-
const viaBox = await resolveSessionPermissions(ME, 'box');
|
|
150
|
-
assert.deepEqual([...viaBox], EXPECTED_BOX_SCOPE,
|
|
151
|
-
'a box token must carry the scope\'s allow-list and nothing else, however senior its builder');
|
|
152
|
-
for (const key of [...ARCHON_POWERS, ...METIC_POWERS]) {
|
|
153
|
-
assert.equal(viaBox.includes(key), false, `a box session reached ${key} — the #919 containment is gone`);
|
|
154
|
-
}
|
|
155
|
-
});
|
|
156
|
-
});
|
|
57
|
+
async function runRequireBuilder(session) {
|
|
58
|
+
nextSession = session;
|
|
59
|
+
const req = { headers: { authorization: 'Bearer anytoken' } };
|
|
60
|
+
const res = fakeRes();
|
|
61
|
+
let nexted = false;
|
|
62
|
+
await auth.requireBuilder(req, res, () => { nexted = true; });
|
|
63
|
+
return { res, nexted, req };
|
|
64
|
+
}
|
|
157
65
|
|
|
158
|
-
|
|
159
|
-
await holding(seedForRank('archon'), async () => {
|
|
160
|
-
const viaCli = await resolveSessionPermissions(ME, 'cli');
|
|
161
|
-
for (const key of [...ARCHON_POWERS, ...METIC_POWERS]) {
|
|
162
|
-
assert.ok(viaCli.includes(key), `an Archon's CLI session must still hold ${key} — B1 would prove nothing otherwise`);
|
|
163
|
-
}
|
|
164
|
-
assert.ok(viaCli.length > EXPECTED_BOX_SCOPE.length + 10,
|
|
165
|
-
'the box scope must be dramatically smaller than the builder\'s real authority');
|
|
166
|
-
});
|
|
167
|
-
});
|
|
66
|
+
// ═══ A — one registry, read identically by both loaders ═══════════════════════
|
|
168
67
|
|
|
169
|
-
test('
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
// for xenos is rank-bought authority, and none of it may survive a box session.
|
|
173
|
-
const rankBought = catalog.PERMISSIONS
|
|
174
|
-
.filter((p) => p.floor !== null && auth.rankMeetsThreshold('archon', [p.floor]) && !auth.rankMeetsThreshold('xenos', [p.floor]))
|
|
175
|
-
.map((p) => p.key);
|
|
176
|
-
assert.ok(rankBought.length >= 30, `expected the archon/metic/thetes tiers, saw ${rankBought.length}`);
|
|
177
|
-
for (const rank of RANK_ORDER) {
|
|
178
|
-
const viaBox = narrowToSessionScope(seedForRank(rank), 'box');
|
|
179
|
-
for (const key of rankBought) {
|
|
180
|
-
assert.equal(viaBox.includes(key), false, `${rank}'s box session reached rank-bought ${key}`);
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
// ...and every rank's box session is identical, because the scope — not the rank —
|
|
184
|
-
// is what decides.
|
|
185
|
-
const perRank = RANK_ORDER.map((r) => [...narrowToSessionScope(seedForRank(r), 'box')]);
|
|
186
|
-
for (const resolved of perRank) assert.deepEqual(resolved, perRank[0]);
|
|
68
|
+
test('A1: the registry declares exactly the hand-written sources', () => {
|
|
69
|
+
assert.deepEqual(scopes.sources(), EXPECTED_SOURCES);
|
|
70
|
+
assert.deepEqual(auth.SESSION_SCOPES.map((s) => s.source), EXPECTED_SOURCES);
|
|
187
71
|
});
|
|
188
72
|
|
|
189
|
-
test('
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
// makes widening inexpressible rather than merely unintended.
|
|
193
|
-
const inputs = [
|
|
194
|
-
EVERYTHING, [], ['idea.triage'], ['box.manage.own'], seedForRank('xenos'),
|
|
195
|
-
seedForRank('thetes'), seedForRank('metic'), seedForRank('archon'),
|
|
196
|
-
['not.a.real.permission'], ['principal.bfg', 'box.manage.own'],
|
|
197
|
-
];
|
|
198
|
-
for (const source of [...scopes.sources(), 'ghost', undefined]) {
|
|
199
|
-
for (const input of inputs) {
|
|
200
|
-
const base = new Set(effectivePermissions(input));
|
|
201
|
-
for (const key of narrowToSessionScope(input, source)) {
|
|
202
|
-
assert.ok(base.has(key),
|
|
203
|
-
`source ${JSON.stringify(source)}: narrowing produced "${key}", which the roles never conferred — this is a WIDENING`);
|
|
204
|
-
}
|
|
205
|
-
}
|
|
73
|
+
test('A2: the kernel and the governance module agree on every source, and on the retired one', () => {
|
|
74
|
+
for (const source of [...EXPECTED_SOURCES, 'box', 'ghost', undefined, 7]) {
|
|
75
|
+
assert.equal(!!auth.sessionScopeFor(source), !!scopes.scopeFor(source), `loaders disagree on ${String(source)}`);
|
|
206
76
|
}
|
|
77
|
+
assert.equal(auth.sessionScopeFor('box'), null, 'the retired dev-box source must not be declared');
|
|
78
|
+
assert.ok(auth.sessionScopeFor(undefined), 'an absent source is the declared legacy row');
|
|
207
79
|
});
|
|
208
80
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
const withoutTheKey = EVERYTHING.filter((k) => !EXPECTED_BOX_SCOPE.includes(k));
|
|
215
|
-
await holding(withoutTheKey, async () => {
|
|
216
|
-
assert.deepEqual([...await resolveSessionPermissions(ME, 'box')], [],
|
|
217
|
-
'the scope GRANTED a key the roles do not confer — this is a union, not an intersection');
|
|
218
|
-
});
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
test('C2: a builder with no roles at all gets nothing from any scope', async () => {
|
|
222
|
-
await holding([], async () => {
|
|
223
|
-
for (const source of [...scopes.sources(), 'ghost']) {
|
|
224
|
-
assert.deepEqual([...await resolveSessionPermissions(ME, source)], [],
|
|
225
|
-
`source ${JSON.stringify(source)}: an empty role set must stay empty — a scope is not a grant`);
|
|
226
|
-
}
|
|
227
|
-
});
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
test('C3: a null/blank builder resolves to nothing on every source', async () => {
|
|
231
|
-
for (const builderId of [null, undefined, '']) {
|
|
232
|
-
for (const source of scopes.sources()) {
|
|
233
|
-
assert.deepEqual([...await resolveSessionPermissions(builderId, source)], []);
|
|
234
|
-
}
|
|
81
|
+
test('A3: no row declares a scope any more', () => {
|
|
82
|
+
const raw = JSON.parse(fs.readFileSync(path.join(GOV, 'session-scopes.json'), 'utf8'));
|
|
83
|
+
for (const row of raw.scopes) {
|
|
84
|
+
assert.equal(row.permissions, undefined, `${row.source}: an allow-list without enforcement would grant full authority`);
|
|
85
|
+
assert.equal(row.unscoped, undefined, `${row.source}: the scoped/unscoped distinction is gone`);
|
|
235
86
|
}
|
|
236
87
|
});
|
|
237
88
|
|
|
238
|
-
// ═══
|
|
239
|
-
|
|
240
|
-
test('D1: holding the ENTIRE catalog, a box session still resolves only the allow-list', async () => {
|
|
241
|
-
await holding(EVERYTHING, async () => {
|
|
242
|
-
assert.deepEqual([...await resolveSessionPermissions(ME, 'box')], EXPECTED_BOX_SCOPE,
|
|
243
|
-
'an unlisted permission leaked through — the allow-list is not deny-by-default');
|
|
244
|
-
});
|
|
245
|
-
});
|
|
246
|
-
|
|
247
|
-
test('D2: every catalog key outside the allow-list is denied on a box session', () => {
|
|
248
|
-
const resolved = new Set(narrowToSessionScope(EVERYTHING, 'box'));
|
|
249
|
-
const listed = new Set(EXPECTED_BOX_SCOPE);
|
|
250
|
-
const leaked = [...resolved].filter((k) => !listed.has(k));
|
|
251
|
-
assert.deepEqual(leaked, [], `these unlisted permissions survived a box session: ${leaked.join(', ')}`);
|
|
252
|
-
// and the coverage is real — the catalog is far bigger than the allow-list
|
|
253
|
-
assert.ok(EVERYTHING.length > EXPECTED_BOX_SCOPE.length + 50,
|
|
254
|
-
'the catalog should dwarf the box allow-list, or D1/D2 assert almost nothing');
|
|
255
|
-
});
|
|
89
|
+
// ═══ B — fail closed at load ══════════════════════════════════════════════════
|
|
256
90
|
|
|
257
|
-
test('
|
|
258
|
-
assert.
|
|
259
|
-
assert.
|
|
260
|
-
assert.
|
|
261
|
-
assert.
|
|
262
|
-
|
|
263
|
-
assert.
|
|
91
|
+
test('B1: the kernel parser refuses a narrowing row, an empty registry, and duplicates', () => {
|
|
92
|
+
assert.throws(() => auth.parseSessionScopes({ scopes: [{ source: 'box', permissions: ['task.ship'] }] }), /Narrowing session scopes were removed/);
|
|
93
|
+
assert.throws(() => auth.parseSessionScopes({ scopes: [{ source: 'cli', unscoped: true }] }), /declares a scope/);
|
|
94
|
+
assert.throws(() => auth.parseSessionScopes({ scopes: [] }), /declares no session sources/);
|
|
95
|
+
assert.throws(() => auth.parseSessionScopes({}), /declares no session sources/);
|
|
96
|
+
assert.throws(() => auth.parseSessionScopes({ scopes: [{ source: 'cli' }, { source: 'cli' }] }), /duplicate/);
|
|
97
|
+
assert.throws(() => auth.parseSessionScopes({ scopes: [{ source: '' }] }), /non-empty session source/);
|
|
98
|
+
// …and the happy shape parses, so the refusals above are not a parser that refuses everything.
|
|
99
|
+
assert.equal(auth.parseSessionScopes({ scopes: [{ source: 'cli' }, { source: null }] }).size, 2);
|
|
264
100
|
});
|
|
265
101
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
assert.throws(() => scopes.validateAgainstCatalog(scoped(['not.a.real.permission'])),
|
|
273
|
-
/which the permission catalog does not define/);
|
|
274
|
-
assert.throws(() => scopes.validateAgainstCatalog(scoped(['principal.bfg'])),
|
|
275
|
-
/machine-principal permission/);
|
|
276
|
-
assert.throws(() => scopes.validateAgainstCatalog(scoped(['deploy.push_main'])),
|
|
277
|
-
/system:true permission/);
|
|
278
|
-
assert.throws(() => scopes.validateAgainstCatalog([scopes.normalizeScope({ source: 'box', unscoped: true, permissions: ['box.manage.own'] })]),
|
|
279
|
-
/never both\/neither/);
|
|
280
|
-
assert.throws(() => scopes.validateAgainstCatalog([scopes.normalizeScope({ source: 'box' })]),
|
|
281
|
-
/never both\/neither/);
|
|
282
|
-
// the real registry passes its own validator (the fixtures above are not the
|
|
283
|
-
// only thing keeping this green)
|
|
284
|
-
assert.doesNotThrow(() => scopes.validateAgainstCatalog(scopes.SESSION_SCOPES));
|
|
102
|
+
test('B2: the governance validator refuses the same shapes', () => {
|
|
103
|
+
const v = (rows) => scopes.validateScopes(rows.map(scopes.normalizeScope));
|
|
104
|
+
assert.throws(() => v([{ source: 'box', permissions: ['task.ship'] }]), /Narrowing session scopes were removed/);
|
|
105
|
+
assert.throws(() => v([]), /declares no session sources/);
|
|
106
|
+
assert.throws(() => v([{ source: 'web' }, { source: 'web' }]), /declared twice/);
|
|
107
|
+
assert.equal(v([{ source: 'web' }]).length, 1);
|
|
285
108
|
});
|
|
286
109
|
|
|
287
|
-
|
|
288
|
-
assert.throws(() => auth.parseSessionScopes({}), /declares no session scopes/);
|
|
289
|
-
assert.throws(() => auth.parseSessionScopes({ scopes: [] }), /declares no session scopes/);
|
|
290
|
-
assert.throws(() => auth.parseSessionScopes({ scopes: [{ source: 'box' }] }), /never both\/neither/);
|
|
291
|
-
assert.throws(() => auth.parseSessionScopes({ scopes: [{ source: 'box', unscoped: true, permissions: ['x'] }] }), /never both\/neither/);
|
|
292
|
-
assert.throws(() => auth.parseSessionScopes({ scopes: [{ source: 7, unscoped: true }] }), /non-empty session source string/);
|
|
293
|
-
assert.throws(() => auth.parseSessionScopes({ scopes: [{ source: 'box', permissions: [42] }] }), /non-string permission key/);
|
|
294
|
-
assert.throws(() => auth.parseSessionScopes({ scopes: [{ source: 'cli', unscoped: true }, { source: 'cli', unscoped: true }] }), /duplicate session source/);
|
|
295
|
-
});
|
|
110
|
+
// ═══ C — requireBuilder: undeclared ⇒ 403, declared ⇒ through ═════════════════
|
|
296
111
|
|
|
297
|
-
test('
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
assert.equal(held.includes('not.a.real.permission'), false, 'the role answer already drops unknown keys');
|
|
304
|
-
assert.deepEqual(held.filter((k) => stub.permissions.includes(k)), [],
|
|
305
|
-
'an unknown allow-list key cannot survive the intersection');
|
|
112
|
+
test('C1: a leftover dev-box session (source box) is denied, whatever its rank', async () => {
|
|
113
|
+
const { res, nexted, req } = await runRequireBuilder({ builder_id: 3, github_login: 'boxbot', rank: 'archon', status: 'active', source: 'box' });
|
|
114
|
+
assert.equal(nexted, false, 'a retired source must not reach any route');
|
|
115
|
+
assert.equal(res._status, 403);
|
|
116
|
+
assert.equal(res._json.error.code, 'session_scope');
|
|
117
|
+
assert.equal(req.builder, undefined, 'no builder is attached to a denied request');
|
|
306
118
|
});
|
|
307
119
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
const roles = seedForRank(rank);
|
|
313
|
-
for (const source of ['cli', 'web', null, undefined]) {
|
|
314
|
-
assert.deepEqual([...narrowToSessionScope(roles, source)], [...effectivePermissions(roles)],
|
|
315
|
-
`${rank} on ${JSON.stringify(source)}: an unscoped session must narrow NOTHING`);
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
await holding(seedForRank('metic'), async () => {
|
|
319
|
-
assert.deepEqual([...await resolveSessionPermissions(ME, 'cli')],
|
|
320
|
-
[...await resolver.resolveBuilderPermissions(ME)]);
|
|
321
|
-
});
|
|
120
|
+
test('C2: an unknown source is denied the same way', async () => {
|
|
121
|
+
const { res, nexted } = await runRequireBuilder({ builder_id: 4, github_login: 'mystery', rank: 'archon', status: 'active', source: 'not-a-source' });
|
|
122
|
+
assert.equal(nexted, false);
|
|
123
|
+
assert.equal(res._json.error.code, 'session_scope');
|
|
322
124
|
});
|
|
323
125
|
|
|
324
|
-
test('
|
|
126
|
+
test('C3: cli, web and legacy sessions pass untouched', async () => {
|
|
325
127
|
for (const source of ['cli', 'web', null, undefined]) {
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
assert.equal(
|
|
128
|
+
const session = { builder_id: 1, github_login: 'someone', rank: 'xenos', status: 'active' };
|
|
129
|
+
if (source !== undefined) session.source = source;
|
|
130
|
+
const { nexted, res } = await runRequireBuilder(session);
|
|
131
|
+
assert.equal(nexted, true, `a ${String(source)} session must reach the route`);
|
|
132
|
+
assert.equal(res._status, null);
|
|
330
133
|
}
|
|
331
134
|
});
|
|
332
135
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
const
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
assert.equal(auth.sessionScopeFor(source), null, `${String(source)} must not resolve to a scope`);
|
|
340
|
-
assert.deepEqual([...await resolveSessionPermissions(ME, source)], [],
|
|
341
|
-
`${String(source)} must resolve to no authority at all, never to "full"`);
|
|
342
|
-
assert.equal(auth.sessionScopeAdmits(source, ['box.manage.own']), false);
|
|
343
|
-
assert.equal(auth.sessionScopeAdmits(source, undefined), false);
|
|
344
|
-
}
|
|
345
|
-
});
|
|
346
|
-
});
|
|
347
|
-
|
|
348
|
-
test('G2: requireBuilder DENIES a session whose source the registry does not declare', async () => {
|
|
349
|
-
nextSession = { builder_id: 3, github_login: 'ghost', rank: 'archon', status: 'active', source: 'ghost' };
|
|
350
|
-
const req = { headers: { authorization: 'Bearer ghosttoken' } };
|
|
136
|
+
test('C4: the retired doorway alias admits nothing extra', async () => {
|
|
137
|
+
// src/module-api.js keeps `allowBoxScope` as a no-op until the next MAJOR. Mounting
|
|
138
|
+
// it ahead of requireBuilder must not let a box session through.
|
|
139
|
+
const api = require(path.join(ROOT, 'src', 'module-api.js'));
|
|
140
|
+
nextSession = { builder_id: 3, github_login: 'boxbot', rank: 'archon', status: 'active', source: 'box' };
|
|
141
|
+
const req = { headers: { authorization: 'Bearer boxtoken' } };
|
|
351
142
|
const res = fakeRes();
|
|
352
143
|
let nexted = false;
|
|
144
|
+
api.allowBoxScope(req, res, () => {});
|
|
353
145
|
await auth.requireBuilder(req, res, () => { nexted = true; });
|
|
354
|
-
assert.equal(nexted, false
|
|
355
|
-
assert.equal(res.
|
|
356
|
-
assert.equal(res._json.error.code, 'box_scope');
|
|
357
|
-
// ...and it stays denied even on a route that opted in, because the DENIAL is the
|
|
358
|
-
// session's, not the route's.
|
|
359
|
-
const optedIn = { headers: { authorization: 'Bearer ghosttoken' } };
|
|
360
|
-
auth.allowBoxScope(optedIn, fakeRes(), () => {});
|
|
361
|
-
const res2 = fakeRes();
|
|
362
|
-
let nexted2 = false;
|
|
363
|
-
await auth.requireBuilder(optedIn, res2, () => { nexted2 = true; });
|
|
364
|
-
assert.equal(nexted2, false);
|
|
365
|
-
assert.equal(res2._status, 403);
|
|
366
|
-
nextSession = null;
|
|
367
|
-
});
|
|
368
|
-
|
|
369
|
-
// ═══ H — the earlier axes are undisturbed ════════════════════════════════════
|
|
370
|
-
|
|
371
|
-
test('H1: R95/R98/R100 invariants still hold — the role answer is untouched', async () => {
|
|
372
|
-
// R99 composes ON TOP of effectivePermissions; widening INSIDE it would red
|
|
373
|
-
// tests/government_parity.mjs Parts A and C.
|
|
374
|
-
assert.deepEqual(effectivePermissions(['principal.bfg', 'box.manage.own']), ['box.manage.own']);
|
|
375
|
-
assert.deepEqual(effectivePermissions(['idea.triage', 'not.a.real.permission']), ['idea.triage']);
|
|
376
|
-
for (const rank of RANK_ORDER) {
|
|
377
|
-
assert.deepEqual([...effectivePermissions(seedForRank(rank))].sort(),
|
|
378
|
-
[...seedForRank(rank)].filter((k) => !catalog.isPrincipalKey(k)).sort());
|
|
379
|
-
}
|
|
380
|
-
await holding(EVERYTHING, async () => {
|
|
381
|
-
// principals are still the other axis
|
|
382
|
-
assert.deepEqual([...resolver.resolvePrincipalPermissions('bfg')], ['principal.bfg']);
|
|
383
|
-
assert.deepEqual([...resolver.resolvePrincipalPermissions(null)], []);
|
|
384
|
-
// ownership is still a conjunction the plain resolve refuses to answer
|
|
385
|
-
assert.equal(await resolver.builderHasPermissions(ME, ['box.manage.own']), false);
|
|
386
|
-
assert.equal(await resolver.authorizeOwned(ME, 'box.manage.own', { ownerId: 99 }), false);
|
|
387
|
-
assert.equal(await resolver.authorizeOwned(ME, 'box.manage.own', { ownerId: ME }), true);
|
|
388
|
-
});
|
|
389
|
-
});
|
|
390
|
-
|
|
391
|
-
test('H2: a box session narrows to an OWNERSHIP-SCOPED key, which still is not an unconditional grant', async () => {
|
|
392
|
-
// The two axes compose without either swallowing the other: the session-scope
|
|
393
|
-
// says WHICH keys survive; the ownership axis still demands the row be yours.
|
|
394
|
-
await holding(seedForRank('archon'), async () => {
|
|
395
|
-
assert.deepEqual([...await resolveSessionPermissions(ME, 'box')], ['box.manage.own']);
|
|
396
|
-
assert.equal(await resolver.authorizeOwned(ME, 'box.manage.own', { ownerId: 99 }), false,
|
|
397
|
-
'surviving the session scope must not turn .own into an unconditional grant');
|
|
398
|
-
// the cross-owner escalation an Archon holds by role is NOT in the box scope
|
|
399
|
-
assert.equal(narrowToSessionScope(seedForRank('archon'), 'box').includes('box.fleet.manage'), false);
|
|
400
|
-
});
|
|
401
|
-
});
|
|
402
|
-
|
|
403
|
-
test('H3: the resolve is still per-request and frozen — no memoised session authority', async () => {
|
|
404
|
-
let calls = 0;
|
|
405
|
-
let keys = ['box.manage.own'];
|
|
406
|
-
db.getPermissionKeysForBuilder = async () => { calls += 1; return keys; };
|
|
407
|
-
try {
|
|
408
|
-
assert.deepEqual([...await resolveSessionPermissions(ME, 'box')], ['box.manage.own']);
|
|
409
|
-
keys = [];
|
|
410
|
-
assert.deepEqual([...await resolveSessionPermissions(ME, 'box')], []);
|
|
411
|
-
assert.equal(calls, 2, 'a revoked grant must take effect on the very next resolve (ADR 0016)');
|
|
412
|
-
} finally {
|
|
413
|
-
db.getPermissionKeysForBuilder = realGetPermissionKeys;
|
|
414
|
-
}
|
|
415
|
-
assert.ok(Object.isFrozen(narrowToSessionScope(EVERYTHING, 'box')));
|
|
416
|
-
assert.ok(Object.isFrozen(narrowToSessionScope(EVERYTHING, 'ghost')));
|
|
146
|
+
assert.equal(nexted, false);
|
|
147
|
+
assert.equal(res._json.error.code, 'session_scope');
|
|
417
148
|
});
|
|
418
149
|
|
|
419
|
-
// ═══
|
|
150
|
+
// ═══ D — the auth exports carry no narrowing machinery ════════════════════════
|
|
420
151
|
|
|
421
|
-
test('
|
|
422
|
-
const
|
|
423
|
-
|
|
424
|
-
assert.equal(typeof governmentPort[name], 'function', `the port must expose ${name}`);
|
|
152
|
+
test('D1: the narrowing helpers are gone from auth and the resolver', () => {
|
|
153
|
+
for (const name of ['allowBoxScope', 'sessionScope', 'sessionScopeAdmits']) {
|
|
154
|
+
assert.equal(auth[name], undefined, `auth.${name} should be gone`);
|
|
425
155
|
}
|
|
426
|
-
|
|
427
|
-
for (const name of ['
|
|
428
|
-
assert.equal(
|
|
156
|
+
const resolver = require(path.join(GOV, 'resolver.js'));
|
|
157
|
+
for (const name of ['narrowToSessionScope', 'resolveSessionPermissions']) {
|
|
158
|
+
assert.equal(resolver[name], undefined, `resolver.${name} should be gone`);
|
|
429
159
|
}
|
|
430
|
-
assert.deepEqual([...governmentPort.sessionScopeFor('box').permissions], EXPECTED_BOX_SCOPE);
|
|
431
|
-
assert.equal(governmentPort.sessionScopeFor('ghost'), null);
|
|
432
|
-
assert.deepEqual([...governmentPort.narrowToSessionScope(seedForRank('archon'), 'box')], EXPECTED_BOX_SCOPE);
|
|
433
|
-
assert.ok(governmentPort.listSessionScopes().length >= 2);
|
|
434
160
|
});
|
|
@@ -212,6 +212,10 @@ async function bootTaskPage({ task, failWith }) {
|
|
|
212
212
|
const realToast = sandbox.window.OTB.toast;
|
|
213
213
|
sandbox.window.OTB = { ...sandbox.window.OTB, toast: (m) => { toasts.push(String(m)); } };
|
|
214
214
|
void realToast;
|
|
215
|
+
// task 1003459: task.js now destructures window.OTBClaimAction at eval time
|
|
216
|
+
// (its Claim it button) — load claim-action.js first, exactly the real
|
|
217
|
+
// page's script order (task.html loads it before task.js).
|
|
218
|
+
vm.runInContext(fs.readFileSync(path.join(HALL, 'claim-action.js'), 'utf8'), sandbox, { filename: 'claim-action.js' });
|
|
215
219
|
vm.runInContext(fs.readFileSync(path.join(HALL, 'task.js'), 'utf8'), sandbox, { filename: 'task.js' });
|
|
216
220
|
assert.ok(document._ready, 'task.js registered a DOMContentLoaded handler');
|
|
217
221
|
await document._ready();
|
|
@@ -91,7 +91,7 @@ async function boot({ meStatus }) {
|
|
|
91
91
|
wirePager: () => {},
|
|
92
92
|
},
|
|
93
93
|
OTBHallRender: () => ({
|
|
94
|
-
|
|
94
|
+
analyticsState: {}, lbState: {},
|
|
95
95
|
shipState: {}, shipsPager: { page: 1 }, welcomeDismissalKey: () => 'k',
|
|
96
96
|
mountHallWidgets: spy('mountHallWidgets'),
|
|
97
97
|
renderLanding: spy('renderLanding'),
|
|
@@ -99,6 +99,9 @@ async function boot({ meStatus }) {
|
|
|
99
99
|
renderNav: spy('renderNav'), renderOnboarding: spy('renderOnboarding'),
|
|
100
100
|
renderProfile: spy('renderProfile'), renderRecentShips: spy('renderRecentShips'),
|
|
101
101
|
renderWelcomeCore: spy('renderWelcomeCore'),
|
|
102
|
+
// task 1003459 — wireEvents() calls this unconditionally now (the Home
|
|
103
|
+
// Next-up Claim button), so an incomplete stub throws mid-wire.
|
|
104
|
+
wireNextUpClaim: spy('wireNextUpClaim'),
|
|
102
105
|
}),
|
|
103
106
|
matchMedia: () => ({ matches: false, addEventListener() {} }),
|
|
104
107
|
localStorage: { getItem() { return null; }, setItem() {}, removeItem() {} },
|
package/tests/hall_palette.mjs
CHANGED
|
@@ -28,7 +28,7 @@ const SHELL_SRC = read('shell.js');
|
|
|
28
28
|
|
|
29
29
|
// Boot palette.js against a DOM stub whose sidebar carries `navItems`, and whose
|
|
30
30
|
// `visibleIds` decide which section groups are on offer. Returns window.OTBPalette.
|
|
31
|
-
function bootPalette({ navItems = [], visibleIds = ['nav-standing', 'nav-work', 'shell-user']
|
|
31
|
+
function bootPalette({ navItems = [], visibleIds = ['nav-standing', 'nav-work', 'shell-user'] } = {}) {
|
|
32
32
|
const makeNav = (it) => ({
|
|
33
33
|
hidden: !!it.hidden,
|
|
34
34
|
target: it.external ? '_blank' : '',
|
|
@@ -60,7 +60,6 @@ function bootPalette({ navItems = [], visibleIds = ['nav-standing', 'nav-work',
|
|
|
60
60
|
activeElement: null,
|
|
61
61
|
};
|
|
62
62
|
const windowObj = { location: { hash: '', assign() {} }, addEventListener() {}, open() {} };
|
|
63
|
-
if (modules) windowObj.__MODULES__ = { enabled: modules };
|
|
64
63
|
const sandbox = {
|
|
65
64
|
window: windowObj, document: documentObj, navigator: { platform: 'Win32', userAgent: 'test' },
|
|
66
65
|
console,
|
|
@@ -143,20 +142,6 @@ test('Settings panel anchors exist in settings.js RAIL_GROUPS', () => {
|
|
|
143
142
|
}
|
|
144
143
|
});
|
|
145
144
|
|
|
146
|
-
test('the Dev box panel jump carries settings.js own dev-box module condition', () => {
|
|
147
|
-
// settings.js builds that panel only when moduleOn('dev-box'); offering the
|
|
148
|
-
// jump regardless would land on a panel that was never rendered.
|
|
149
|
-
assert.match(read('settings.js'), /moduleOn\('dev-box'\)\s*\?\s*\[\{ id: 'devbox'/,
|
|
150
|
-
'settings.js still gates the devbox panel on the module');
|
|
151
|
-
assert.match(PALETTE_SRC, /href: '\/settings#devbox', module: 'dev-box'/,
|
|
152
|
-
'so the palette entry must declare the same module');
|
|
153
|
-
|
|
154
|
-
const on = bootPalette({ navItems: NAV_FIXTURE, modules: { 'dev-box': true } }).compute('dev box');
|
|
155
|
-
assert.ok(on.some((r) => r.href === '/settings#devbox'), 'offered when the module is on');
|
|
156
|
-
const off = bootPalette({ navItems: NAV_FIXTURE, modules: { 'dev-box': false } }).compute('dev box');
|
|
157
|
-
assert.ok(!off.some((r) => r.href === '/settings#devbox'), 'withheld when the module is off');
|
|
158
|
-
});
|
|
159
|
-
|
|
160
145
|
test('the anchor guard actually CATCHES a declared id that does not exist', () => {
|
|
161
146
|
const groups = ['work', 'karma', 'progress', 'achievements'];
|
|
162
147
|
assert.equal(groups.includes('standing-nope'.replace('standing-', '')), false,
|
|
@@ -286,6 +286,11 @@ const TASK_SRC = read('task.js');
|
|
|
286
286
|
// task.html loads md-reader.js ahead of task.js (task 1003465), so the sandbox
|
|
287
287
|
// does the same: the description renders through it.
|
|
288
288
|
const MD_SRC = read('md-reader.js');
|
|
289
|
+
// task 1003459: task.js now destructures window.OTBClaimAction at eval time
|
|
290
|
+
// (its Claim it button) — task.html loads claim-action.js ahead of task.js,
|
|
291
|
+
// so the sandbox does too. Real file, not a stub: it is dependency-free and
|
|
292
|
+
// DOM-free, so running it is exactly as cheap as stubbing it.
|
|
293
|
+
const CLAIM_ACTION_SRC = read('claim-action.js');
|
|
289
294
|
|
|
290
295
|
// grade / sessionLogs / claims (task 1003437): left undefined, the stub answers
|
|
291
296
|
// like a client that never asked (the key is absent); null or an array answers
|
|
@@ -400,6 +405,7 @@ function renderTask(task, { lastClaim = null, viewer = null, goal = null, goalsL
|
|
|
400
405
|
sandbox.globalThis = sandbox;
|
|
401
406
|
vm.createContext(sandbox);
|
|
402
407
|
if (withReader) vm.runInContext(MD_SRC, sandbox, { filename: 'md-reader.js' });
|
|
408
|
+
vm.runInContext(CLAIM_ACTION_SRC, sandbox, { filename: 'claim-action.js' });
|
|
403
409
|
vm.runInContext(TASK_SRC, sandbox, { filename: 'task.js' });
|
|
404
410
|
assert.ok(onReady, 'task.js registers its DOMContentLoaded boot');
|
|
405
411
|
return {
|