@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/fitness.mjs
CHANGED
|
@@ -261,7 +261,7 @@ test('module boundary: both checks pass (green) on the current tree', () => {
|
|
|
261
261
|
const b = fitness.checkModulesImportOnlyApi();
|
|
262
262
|
assert.equal(a.hardFail, false, a.violations.concat(a.warnings).join('\n'));
|
|
263
263
|
assert.equal(b.hardFail, false, b.violations.concat(b.warnings).join('\n'));
|
|
264
|
-
// modules/
|
|
264
|
+
// modules/ holds the carved modules (the M07/M08 moves onward); each imports
|
|
265
265
|
// only the doorway and the core imports neither → zero findings even though
|
|
266
266
|
// the ratchet is now ENFORCING (BV1.R45).
|
|
267
267
|
assert.equal(a.violations.length + a.warnings.length, 0, 'core→module findings: ' + a.warnings.concat(a.violations).join('; '));
|
|
@@ -282,7 +282,7 @@ test('module boundary ratchet is FLIPPED — enforcing by default (BV1.R45)', ()
|
|
|
282
282
|
|
|
283
283
|
test('resolvesToModule: a core require into modules/<key>/ is detected', () => {
|
|
284
284
|
const coreFile = path.join(ROOT, 'src', 'bongos', 'x.js');
|
|
285
|
-
assert.equal(fitness.resolvesToModule(coreFile, '../../modules/
|
|
285
|
+
assert.equal(fitness.resolvesToModule(coreFile, '../../modules/art-pipeline/stage.js'), 'art-pipeline');
|
|
286
286
|
assert.equal(fitness.resolvesToModule(coreFile, './sibling.js'), null);
|
|
287
287
|
});
|
|
288
288
|
|
|
@@ -292,7 +292,7 @@ test('moduleImportViolation: doorway + intra-module OK; deep-core + sibling flag
|
|
|
292
292
|
assert.equal(fitness.moduleImportViolation(f, '../db.js'), null, 'intra-module relative is allowed');
|
|
293
293
|
assert.equal(fitness.moduleImportViolation(f, 'express'), null, 'bare npm specs are allowed');
|
|
294
294
|
assert.match(fitness.moduleImportViolation(f, '../../../src/bongos/db') || '', /core internals/);
|
|
295
|
-
assert.match(fitness.moduleImportViolation(f, '../../
|
|
295
|
+
assert.match(fitness.moduleImportViolation(f, '../../art-pipeline/stage') || '', /sibling module/);
|
|
296
296
|
});
|
|
297
297
|
|
|
298
298
|
test('module boundary actually CATCHES violations when enforcing (ratchet flip)', () => {
|
|
@@ -301,7 +301,7 @@ test('module boundary actually CATCHES violations when enforcing (ratchet flip)'
|
|
|
301
301
|
try {
|
|
302
302
|
const f = path.join(ROOT, 'modules', 'demo', 'x.js');
|
|
303
303
|
assert.ok(fitness.moduleImportViolation(f, '../../src/bongos/db'), 'deep-core import must be a violation');
|
|
304
|
-
// the live checks stay green (
|
|
304
|
+
// the live checks stay green (every carved module is clean) even when enforcing
|
|
305
305
|
assert.equal(fitness.checkCoreNeverImportsModule().hardFail, false);
|
|
306
306
|
assert.equal(fitness.checkModulesImportOnlyApi().hardFail, false);
|
|
307
307
|
} finally {
|
|
@@ -390,15 +390,15 @@ test('the discovered-module roster is derived from a NON-EMPTY disk read (mutati
|
|
|
390
390
|
|
|
391
391
|
test('core-carries-no-module-code actually CATCHES a hardcoded key (and ignores comments)', () => {
|
|
392
392
|
// a key named in CODE is a violation
|
|
393
|
-
const hit = fitness.scanKernelForModuleKeys("if (key === '
|
|
394
|
-
assert.ok(hit.some((v) => /
|
|
393
|
+
const hit = fitness.scanKernelForModuleKeys("if (key === 'art-pipeline') mountSpecially();", ['art-pipeline', 'discord']);
|
|
394
|
+
assert.ok(hit.some((v) => /art-pipeline/.test(v)), 'a hardcoded module key in code must be flagged');
|
|
395
395
|
// the same key in a comment / doc-example line is NOT a violation
|
|
396
|
-
const clean = fitness.scanKernelForModuleKeys("// e.g. logger('
|
|
396
|
+
const clean = fitness.scanKernelForModuleKeys("// e.g. logger('art-pipeline')\n * the art-pipeline doorway\nconst x = 1;", ['art-pipeline']);
|
|
397
397
|
assert.deepEqual(clean, [], 'a key on a comment line must be ignored: ' + clean.join('; '));
|
|
398
|
-
// token-delimited: `
|
|
399
|
-
assert.deepEqual(fitness.scanKernelForModuleKeys("const x = '
|
|
398
|
+
// token-delimited: `art-pipeline` must not fire inside the larger token `art-pipelines`
|
|
399
|
+
assert.deepEqual(fitness.scanKernelForModuleKeys("const x = 'art-pipelines';", ['art-pipeline']), [], 'a substring inside a larger token must not fire');
|
|
400
400
|
// no keys discovered → never fires (vacuous-safe before the first move)
|
|
401
|
-
assert.deepEqual(fitness.scanKernelForModuleKeys("mount('
|
|
401
|
+
assert.deepEqual(fitness.scanKernelForModuleKeys("mount('art-pipeline')", []), []);
|
|
402
402
|
});
|
|
403
403
|
|
|
404
404
|
// --- kernel imports no domain (BV1.R70, ADR 0091 §1) ---
|
|
@@ -442,45 +442,45 @@ test('module-migrations-additive: the live tree is clean (no module migrations y
|
|
|
442
442
|
});
|
|
443
443
|
|
|
444
444
|
test('module-migrations-additive actually CATCHES destructive + non-namespaced DDL', () => {
|
|
445
|
-
// the SQL namespace prefix derives from the key:
|
|
446
|
-
assert.equal(fitness.moduleNamespace('
|
|
445
|
+
// the SQL namespace prefix derives from the key: art-pipeline → art_pipeline
|
|
446
|
+
assert.equal(fitness.moduleNamespace('art-pipeline'), 'art_pipeline');
|
|
447
447
|
|
|
448
448
|
// additive + namespaced → clean (table, index-on-own-table, additive ALTER)
|
|
449
|
-
const good = fitness.moduleMigrationViolations('
|
|
450
|
-
'CREATE TABLE IF NOT EXISTS
|
|
451
|
-
'CREATE INDEX
|
|
452
|
-
'ALTER TABLE
|
|
449
|
+
const good = fitness.moduleMigrationViolations('art-pipeline',
|
|
450
|
+
'CREATE TABLE IF NOT EXISTS art_pipeline_runs (id serial primary key);\n' +
|
|
451
|
+
'CREATE INDEX idx_art_pipeline_runs_id ON art_pipeline_runs (id);\n' +
|
|
452
|
+
'ALTER TABLE art_pipeline_runs ADD COLUMN note text;');
|
|
453
453
|
assert.deepEqual(good, [], 'an additive, namespaced migration must pass: ' + good.join('; '));
|
|
454
454
|
|
|
455
455
|
// destructive DDL is rejected
|
|
456
|
-
assert.ok(fitness.moduleMigrationViolations('
|
|
457
|
-
assert.ok(fitness.moduleMigrationViolations('
|
|
458
|
-
assert.ok(fitness.moduleMigrationViolations('
|
|
456
|
+
assert.ok(fitness.moduleMigrationViolations('art-pipeline', 'DROP TABLE art_pipeline_runs;').some((v) => /destructive/i.test(v)), 'DROP TABLE must be flagged');
|
|
457
|
+
assert.ok(fitness.moduleMigrationViolations('art-pipeline', 'ALTER TABLE art_pipeline_runs DROP COLUMN note;').some((v) => /destructive/i.test(v)), 'ALTER … DROP must be flagged');
|
|
458
|
+
assert.ok(fitness.moduleMigrationViolations('art-pipeline', 'TRUNCATE art_pipeline_runs;').some((v) => /destructive/i.test(v)), 'TRUNCATE must be flagged');
|
|
459
459
|
|
|
460
460
|
// a CHECK-enum WIDENING (drop-if-exists + re-add the SAME-named constraint) is
|
|
461
461
|
// additive-in-spirit and ALLOWED (task 1978: provisioning gains 'standalone')
|
|
462
|
-
const widen = fitness.moduleMigrationViolations('
|
|
463
|
-
'ALTER TABLE
|
|
464
|
-
"ALTER TABLE
|
|
462
|
+
const widen = fitness.moduleMigrationViolations('art-pipeline',
|
|
463
|
+
'ALTER TABLE art_pipeline_runs DROP CONSTRAINT IF EXISTS art_pipeline_runs_shape_check;\n' +
|
|
464
|
+
"ALTER TABLE art_pipeline_runs ADD CONSTRAINT art_pipeline_runs_shape_check CHECK (shape IN ('a','b','c'));");
|
|
465
465
|
assert.deepEqual(widen, [], 'a CHECK-enum widening (drop+re-add same constraint) must pass: ' + widen.join('; '));
|
|
466
466
|
// but a BARE drop constraint (no matching re-add) is STILL destructive
|
|
467
|
-
assert.ok(fitness.moduleMigrationViolations('
|
|
467
|
+
assert.ok(fitness.moduleMigrationViolations('art-pipeline', 'ALTER TABLE art_pipeline_runs DROP CONSTRAINT art_pipeline_runs_shape_check;').some((v) => /destructive/i.test(v)), 'a bare DROP CONSTRAINT (no re-add) must still be flagged');
|
|
468
468
|
|
|
469
469
|
// an ACKNOWLEDGED contract-phase column drop (task 1003092, ADR 0177's expand-then-
|
|
470
470
|
// contract) is the ONE destructive shape allowed: the file carries the same
|
|
471
471
|
// `-- forward-unsafe:` pragma lint-migrations.js requires, the statement is exactly
|
|
472
472
|
// one DROP COLUMN, and the table is the module's own.
|
|
473
|
-
const PRAGMA = '-- forward-unsafe: drop-column on
|
|
474
|
-
const contract = fitness.moduleMigrationViolations('
|
|
475
|
-
PRAGMA + 'ALTER TABLE
|
|
473
|
+
const PRAGMA = '-- forward-unsafe: drop-column on art_pipeline_runs — the contract phase of a two-step rename\n';
|
|
474
|
+
const contract = fitness.moduleMigrationViolations('art-pipeline',
|
|
475
|
+
PRAGMA + 'ALTER TABLE art_pipeline_runs DROP COLUMN IF EXISTS note;\nALTER TABLE art_pipeline_runs ALTER COLUMN note2 SET NOT NULL;');
|
|
476
476
|
assert.deepEqual(contract, [], 'an acknowledged own-table DROP COLUMN must pass: ' + contract.join('; '));
|
|
477
477
|
// the pragma is per-STATEMENT permission for that one shape, not a per-file switch
|
|
478
|
-
assert.ok(fitness.moduleMigrationViolations('
|
|
479
|
-
assert.ok(fitness.moduleMigrationViolations('
|
|
480
|
-
assert.ok(fitness.moduleMigrationViolations('
|
|
481
|
-
assert.ok(fitness.moduleMigrationViolations('
|
|
478
|
+
assert.ok(fitness.moduleMigrationViolations('art-pipeline', PRAGMA + 'DROP TABLE art_pipeline_runs;').some((v) => /destructive/i.test(v)), 'DROP TABLE stays flagged even under the pragma');
|
|
479
|
+
assert.ok(fitness.moduleMigrationViolations('art-pipeline', PRAGMA + 'TRUNCATE art_pipeline_runs;').some((v) => /destructive/i.test(v)), 'TRUNCATE stays flagged even under the pragma');
|
|
480
|
+
assert.ok(fitness.moduleMigrationViolations('art-pipeline', PRAGMA + 'ALTER TABLE art_pipeline_runs DROP CONSTRAINT art_pipeline_runs_shape_check;').some((v) => /destructive/i.test(v)), 'a bare DROP CONSTRAINT stays flagged even under the pragma');
|
|
481
|
+
assert.ok(fitness.moduleMigrationViolations('art-pipeline', PRAGMA + 'ALTER TABLE art_pipeline_runs DROP COLUMN note, DROP COLUMN note2;').some((v) => /destructive/i.test(v)), 'a multi-clause drop is not the one allowed shape');
|
|
482
482
|
// and a column drop on a table OUTSIDE the namespace is refused twice over
|
|
483
|
-
const foreign = fitness.moduleMigrationViolations('
|
|
483
|
+
const foreign = fitness.moduleMigrationViolations('art-pipeline', PRAGMA + 'ALTER TABLE builders DROP COLUMN note;');
|
|
484
484
|
assert.ok(foreign.some((v) => /destructive/i.test(v)), 'a core-table DROP COLUMN is still destructive under the pragma');
|
|
485
485
|
assert.ok(foreign.some((v) => /namespace/i.test(v)), 'and still a namespace violation');
|
|
486
486
|
// the real contract migration is the shape's first user — it must pass the REAL check
|
|
@@ -489,13 +489,13 @@ test('module-migrations-additive actually CATCHES destructive + non-namespaced D
|
|
|
489
489
|
'platform_identity_021 is the acknowledged contract phase and must be clean');
|
|
490
490
|
|
|
491
491
|
// a created object outside the <key>_ namespace is rejected
|
|
492
|
-
assert.ok(fitness.moduleMigrationViolations('
|
|
492
|
+
assert.ok(fitness.moduleMigrationViolations('art-pipeline', 'CREATE TABLE runs (id int);').some((v) => /namespace/i.test(v)), 'a table outside art_pipeline_ must be flagged');
|
|
493
493
|
// a module migration altering a CORE table is rejected (reach core via the seam)
|
|
494
|
-
assert.ok(fitness.moduleMigrationViolations('
|
|
494
|
+
assert.ok(fitness.moduleMigrationViolations('art-pipeline', 'ALTER TABLE builders ADD COLUMN x int;').some((v) => /namespace/i.test(v)), 'altering a core table must be flagged');
|
|
495
495
|
|
|
496
496
|
// a commented-out destructive line is ignored
|
|
497
497
|
assert.deepEqual(
|
|
498
|
-
fitness.moduleMigrationViolations('
|
|
498
|
+
fitness.moduleMigrationViolations('art-pipeline', '-- DROP TABLE art_pipeline_runs;\nCREATE TABLE art_pipeline_runs (id int);'),
|
|
499
499
|
[], 'a commented-out DROP must be ignored',
|
|
500
500
|
);
|
|
501
501
|
});
|
|
@@ -781,12 +781,12 @@ test('demo proof module is live + clean (boundary + rank + migration green)', ()
|
|
|
781
781
|
assert.equal(fitness.checkModuleMigrationsAdditive().hardFail, false, 'demo migration is additive + namespaced');
|
|
782
782
|
});
|
|
783
783
|
|
|
784
|
-
test('demo RED-LINE (a): a cross-module import (demo →
|
|
784
|
+
test('demo RED-LINE (a): a cross-module import (demo → art-pipeline internals) is REJECTED', () => {
|
|
785
785
|
// the exact violation the BV1.R52 transcript injects: demo deep-importing a
|
|
786
786
|
// sibling module. moduleImportViolation runs on the real demo route path.
|
|
787
787
|
const demoRoute = path.join(ROOT, 'modules', 'demo', 'routes', 'demo.js');
|
|
788
|
-
const v = fitness.moduleImportViolation(demoRoute, '../../
|
|
789
|
-
assert.ok(v && /sibling module "
|
|
788
|
+
const v = fitness.moduleImportViolation(demoRoute, '../../art-pipeline/stage');
|
|
789
|
+
assert.ok(v && /sibling module "art-pipeline"/.test(v), 'demo importing art-pipeline internals must be flagged: ' + v);
|
|
790
790
|
// the doorway is still allowed (the legitimate single core import)
|
|
791
791
|
assert.equal(fitness.moduleImportViolation(demoRoute, '../../../src/module-api'), null, 'the doorway import is allowed');
|
|
792
792
|
});
|
|
@@ -922,11 +922,13 @@ test('write-route validation check actually CATCHES an unvalidated write route (
|
|
|
922
922
|
assert.ok(/POST \/widgets/.test(flagged[0]), 'the offending route is named');
|
|
923
923
|
});
|
|
924
924
|
|
|
925
|
-
test('write-route validation exemptions are the known dynamic-key prefs routes, plus the
|
|
926
|
-
// The .docx upload (task 1004320)
|
|
927
|
-
// body is
|
|
928
|
-
//
|
|
929
|
-
|
|
925
|
+
test('write-route validation exemptions are the known dynamic-key prefs routes, plus the named raw-file uploads', () => {
|
|
926
|
+
// The .docx upload (task 1004320) and the module-store tarball (task 1004271) have no
|
|
927
|
+
// JSON body to hold a field schema: each body is a file's bytes, capped by the route's
|
|
928
|
+
// own parser and read by a checker that refuses anything malformed (docx.readUpload,
|
|
929
|
+
// module-artifact.verifyModuleArtifact). Named here so each raw-body exemption is a
|
|
930
|
+
// reviewed diff.
|
|
931
|
+
const RAW_FILE_UPLOADS = ['POST /copy-desk/pages/:pageId/draft.docx', 'POST /store/modules/:key/versions'];
|
|
930
932
|
const exempt = [...fitness.WRITE_VALIDATION_EXEMPT];
|
|
931
933
|
assert.ok(exempt.length >= 1 && exempt.every((e) => e.startsWith('PATCH /me/') || RAW_FILE_UPLOADS.includes(e)),
|
|
932
934
|
'exemptions are all /me/* prefs patches or a named raw-file upload: ' + exempt.join(', '));
|
package/tests/gate_approvals.mjs
CHANGED
|
@@ -439,7 +439,6 @@ await t('both routes are requireBuilder-gated and registered', async () => {
|
|
|
439
439
|
assert.ok(layer, `${method.toUpperCase()} ${p} registered`);
|
|
440
440
|
const handles = layer.route.stack.map((s) => s.handle);
|
|
441
441
|
assert.ok(handles.includes(auth.requireBuilder), `${p} must be requireBuilder-gated`);
|
|
442
|
-
assert.ok(!handles.includes(auth.allowBoxScope), `${p} must NOT carry allowBoxScope`);
|
|
443
442
|
}
|
|
444
443
|
});
|
|
445
444
|
|
package/tests/go_live.mjs
CHANGED
|
@@ -44,7 +44,7 @@ const okTarget = {
|
|
|
44
44
|
|
|
45
45
|
// A divergence result shaped like deploy-divergence.check() returns.
|
|
46
46
|
const div = (over = {}) => ({
|
|
47
|
-
ok: true, onMain: '1.19.306', live: { coreVersion: '1.19.305' },
|
|
47
|
+
ok: true, onMain: '1.19.306', live: { version: '1.0.0', coreVersion: '1.19.305' },
|
|
48
48
|
hasUnreleased: false, unreleased: { count: 0 }, ...over,
|
|
49
49
|
});
|
|
50
50
|
|
|
@@ -82,14 +82,14 @@ test('--allow-unreleased downgrades that halt to a note rather than dropping it'
|
|
|
82
82
|
|
|
83
83
|
test('a no-op bump is a halt, not a success', () => {
|
|
84
84
|
const p = G.planGoLive({
|
|
85
|
-
div: div({ live: { coreVersion: '1.19.306' } }), target: okTarget, targetVersion: '1.19.306',
|
|
85
|
+
div: div({ live: { version: '1.0.0', coreVersion: '1.19.306' } }), target: okTarget, targetVersion: '1.19.306',
|
|
86
86
|
});
|
|
87
87
|
assert.ok(p.halts.find((x) => x.code === 'ALREADY_LIVE'));
|
|
88
88
|
});
|
|
89
89
|
|
|
90
90
|
test('live ahead of the target halts rather than silently downgrading', () => {
|
|
91
91
|
const p = G.planGoLive({
|
|
92
|
-
div: div({ live: { coreVersion: '1.19.310' } }), target: okTarget, targetVersion: '1.19.306',
|
|
92
|
+
div: div({ live: { version: '1.0.0', coreVersion: '1.19.310' } }), target: okTarget, targetVersion: '1.19.306',
|
|
93
93
|
});
|
|
94
94
|
assert.ok(p.halts.find((x) => x.code === 'LIVE_AHEAD'));
|
|
95
95
|
});
|
|
@@ -19,11 +19,12 @@
|
|
|
19
19
|
// government.manage (the low-trust builder's own attempt)
|
|
20
20
|
// V6 config: a role template proposing system keys is stripped; a seeded
|
|
21
21
|
// role's template grants are dropped wholesale; env overrides labels only
|
|
22
|
-
// V7 session-scope:
|
|
22
|
+
// V7 session-scope: a registry row that declares ANY allow-list THROWS (the
|
|
23
|
+
// narrowing form was removed with its last user, task 1003894)
|
|
23
24
|
//
|
|
24
25
|
// The wall behind the wall (a grant row forced past every vector above):
|
|
25
26
|
// V8 a forced principal grant resolves to NOTHING (effectivePermissions)
|
|
26
|
-
// V9 a
|
|
27
|
+
// V9 a session of an undeclared source (a leftover dev-box token) is refused
|
|
27
28
|
// V10 no route consumes the substrate-enforced keys via requirePermission —
|
|
28
29
|
// a forced deploy/protected-path/ssh grant admits nothing in-app
|
|
29
30
|
// V11 the substrate blocks a sub-floor builder on EVERY protected surface,
|
|
@@ -328,27 +329,28 @@ test('V6: the env layer relabels only — it cannot add a permission or a role',
|
|
|
328
329
|
});
|
|
329
330
|
|
|
330
331
|
// ===========================================================================
|
|
331
|
-
// V7 — the session-scope vector (
|
|
332
|
+
// V7 — the session-scope vector (a registry row can carry no allow-list at all)
|
|
332
333
|
// ===========================================================================
|
|
333
334
|
|
|
334
|
-
test('V7: a session-scope allow-list
|
|
335
|
-
|
|
335
|
+
test('V7: a session-scope row declaring an allow-list is refused at validation, whatever it names', () => {
|
|
336
|
+
// Enumerated from the catalog, as the rest of the matrix is: a system key, a
|
|
337
|
+
// principal key and a plain operational key are all refused, because the refusal
|
|
338
|
+
// is about the SHAPE (a narrowing scope with no enforcement behind it would read as
|
|
339
|
+
// a restriction while granting full authority), not about which key it names.
|
|
340
|
+
for (const k of [...SYSTEM_KEYS, ...catalog.principalKeys(), 'task.ship']) {
|
|
336
341
|
assert.throws(
|
|
337
|
-
() => sessionScopes.
|
|
342
|
+
() => sessionScopes.validateScopes([
|
|
338
343
|
sessionScopes.normalizeScope({ source: 'evil', permissions: [k] }),
|
|
339
344
|
]),
|
|
340
|
-
|
|
345
|
+
/Narrowing session scopes were removed/,
|
|
341
346
|
`a scope allowing ${k} must be refused at load`,
|
|
342
347
|
);
|
|
343
348
|
}
|
|
344
349
|
});
|
|
345
350
|
|
|
346
|
-
test('V7: the LIVE session-scope registry
|
|
351
|
+
test('V7: the LIVE session-scope registry declares no scope on any row', () => {
|
|
347
352
|
for (const scope of sessionScopes.SESSION_SCOPES) {
|
|
348
|
-
|
|
349
|
-
assert.equal(catalog.isSystem(k), false, `scope "${scope.source}" allows system key ${k}`);
|
|
350
|
-
assert.equal(catalog.isPrincipalKey(k), false, `scope "${scope.source}" allows principal key ${k}`);
|
|
351
|
-
}
|
|
353
|
+
assert.equal(scope.declaresScope, false, `source "${scope.source}" declares a scope`);
|
|
352
354
|
}
|
|
353
355
|
});
|
|
354
356
|
|
|
@@ -362,14 +364,13 @@ test('V8: a forced principal grant resolves to nothing (structurally dropped)',
|
|
|
362
364
|
'principal keys confer nothing through the role axis no matter how the row got written');
|
|
363
365
|
});
|
|
364
366
|
|
|
365
|
-
test('V9:
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
367
|
+
test('V9: an undeclared session source is refused outright, never treated as "full"', () => {
|
|
368
|
+
// Rows with source 'box' stay in builder_sessions until task 1003895 deletes them.
|
|
369
|
+
// They must be undeclared to BOTH loaders, which is what makes requireBuilder and
|
|
370
|
+
// the page gate refuse them (tests/government_session_scope.mjs runs the gate).
|
|
371
|
+
for (const source of ['box', 'not-a-source']) {
|
|
372
|
+
assert.equal(sessionScopes.scopeFor(source), null, `"${source}" must not be a declared session source`);
|
|
370
373
|
}
|
|
371
|
-
assert.deepEqual([...resolver.narrowToSessionScope(forcedEverything, 'not-a-source')], [],
|
|
372
|
-
'an unrecognised source resolves to nothing, never to "full"');
|
|
373
374
|
});
|
|
374
375
|
|
|
375
376
|
// ===========================================================================
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
// tests/government_ownership_scope.mjs — ownership-scoped permissions
|
|
2
2
|
// ("act on OWN X") as a first-class axis (BV1.R100, ADR 0151 §3). DB-free.
|
|
3
3
|
//
|
|
4
|
-
// The thing being proved is a CONJUNCTION. `
|
|
4
|
+
// The thing being proved is a CONJUNCTION. `task.abandon.own` is not a grant that
|
|
5
5
|
// happens to be named "own" — it is half a decision:
|
|
6
6
|
//
|
|
7
7
|
// 1. the ROLE fact — the builder holds the key (resolved from the DB, uncached);
|
|
8
8
|
// 2. the OWNER fact — the concrete row is theirs (a REQUEST-scoped fact the
|
|
9
9
|
// resolver structurally cannot know: it is handed a builder
|
|
10
|
-
// id, never a
|
|
10
|
+
// id, never a task).
|
|
11
11
|
//
|
|
12
12
|
// The security failure mode this file exists to catch is (2) quietly disappearing,
|
|
13
13
|
// leaving `.own` as an unconditional grant — i.e. `X.manage.own` silently becoming
|
|
@@ -103,20 +103,18 @@ test('A3: the modelled families are the ones the ownership sweep names', () => {
|
|
|
103
103
|
// `task` joined the modelled families in task 1002443 (ADR 0297) — the first
|
|
104
104
|
// family added because a ROUTE needed it rather than to complete the model.
|
|
105
105
|
assert.deepEqual(Object.keys(RESOURCE_FAMILIES).sort(),
|
|
106
|
-
['
|
|
106
|
+
['builder_prefs', 'claim', 'credential', 'goal', 'memory', 'task']);
|
|
107
107
|
assert.deepEqual([...ownershipScopedKeys()].sort(),
|
|
108
|
-
['
|
|
109
|
-
// The cross-owner side is NOT derivable from spelling:
|
|
110
|
-
//
|
|
108
|
+
['credential.manage.own', 'goal.manage.own', 'me.wandering.set', 'memory.manage.own', 'task.abandon.own', 'task.act.own']);
|
|
109
|
+
// The cross-owner side is NOT derivable from spelling: `task.abandon` carries no
|
|
110
|
+
// suffix at all — it predates the axis and was given
|
|
111
111
|
// scope/resource/verb in place rather than renamed, because it is seeded in live
|
|
112
112
|
// rank rows (governance_002) and a key rename is a migration, not a declaration.
|
|
113
113
|
assert.deepEqual([...crossOwnerKeys()].sort(),
|
|
114
|
-
['
|
|
114
|
+
['claim.release.any', 'memory.read.any', 'task.abandon']);
|
|
115
115
|
// The escalation path is DECLARED per key, never derived from the resource
|
|
116
|
-
// family (task 1003195).
|
|
117
|
-
//
|
|
118
|
-
// over every creator's task, so each genuinely subsumes doing it to your own.
|
|
119
|
-
assert.deepEqual(crossOwnerKeysFor('box.manage.own'), ['box.fleet.manage']);
|
|
116
|
+
// family (task 1003195). ONE real pair: `task.abandon` is the same `act` verb
|
|
117
|
+
// over every creator's task, so it genuinely subsumes retiring your own.
|
|
120
118
|
assert.deepEqual(crossOwnerKeysFor('task.abandon.own'), ['task.abandon']);
|
|
121
119
|
// claim and memory each HAVE an `any` key in their family and still declare no
|
|
122
120
|
// counterpart — the distinction the family-derived version could not make.
|
|
@@ -152,7 +150,7 @@ test('A4: RESOURCE_FAMILIES is frozen and agrees with each row it lists', () =>
|
|
|
152
150
|
// ═══ B — `.own` does NOT imply `.any` (oracle: floors × the real rank gate) ════
|
|
153
151
|
|
|
154
152
|
test('B1: for every paired family some rank reaches its OWN row but not everyone else\'s', () => {
|
|
155
|
-
assert.ok(PAIRED.length >= 3, `expected the
|
|
153
|
+
assert.ok(PAIRED.length >= 3, `expected the claim/memory/task pairs, saw ${PAIRED.length}`);
|
|
156
154
|
for (const [resource, fam] of PAIRED) {
|
|
157
155
|
for (const ownKey of fam.own) {
|
|
158
156
|
for (const anyKey of fam.any) {
|
|
@@ -177,11 +175,11 @@ test('B2: the seed grants the own key to ranks it withholds the cross-owner key
|
|
|
177
175
|
}
|
|
178
176
|
}
|
|
179
177
|
// The concrete claim, spelled out: a xenos may act on their own claim and their
|
|
180
|
-
// own
|
|
178
|
+
// own task; they may not release another builder's claim or abandon another's task.
|
|
181
179
|
assert.ok(RANK_SEED.xenos.includes('task.act.own'));
|
|
182
180
|
assert.ok(!RANK_SEED.xenos.includes('claim.release.any'));
|
|
183
|
-
assert.ok(RANK_SEED.xenos.includes('
|
|
184
|
-
assert.ok(!RANK_SEED.xenos.includes('
|
|
181
|
+
assert.ok(RANK_SEED.xenos.includes('task.abandon.own'));
|
|
182
|
+
assert.ok(!RANK_SEED.xenos.includes('task.abandon'));
|
|
185
183
|
});
|
|
186
184
|
|
|
187
185
|
test('B3: cross-owner authority is never CHEAPER than own-only authority', () => {
|
|
@@ -228,7 +226,7 @@ test('C3: no owner supplied = no admit, however the caller spells the omission',
|
|
|
228
226
|
// question. A handler that forgets it gets a denial, never a pass-through.
|
|
229
227
|
await holding(EVERYTHING, async () => {
|
|
230
228
|
for (const context of [undefined, null, {}, { ownerId: null }, { ownerId: undefined }, { ownerId: '' }, { ownerId: {} }, { ownerId: [] }, { ownerId: true }, { ownerId: NaN }]) {
|
|
231
|
-
assert.equal(await authorizeOwned(ME, '
|
|
229
|
+
assert.equal(await authorizeOwned(ME, 'task.abandon.own', context), false,
|
|
232
230
|
`an unidentifiable owner (${JSON.stringify(context)}) must deny`);
|
|
233
231
|
}
|
|
234
232
|
});
|
|
@@ -245,8 +243,8 @@ test('C4: authorizeOwned is the OWN entry point only — other keys fail closed'
|
|
|
245
243
|
assert.equal(await authorizeOwned(ME, 'principal.bfg', { ownerId: ME }), false);
|
|
246
244
|
assert.equal(await authorizeOwned(ME, 'nope.not.a.key', { ownerId: ME }), false);
|
|
247
245
|
// A blank actor cannot own anything, even matching a blank owner.
|
|
248
|
-
assert.equal(await authorizeOwned(null, '
|
|
249
|
-
assert.equal(await authorizeOwned('', '
|
|
246
|
+
assert.equal(await authorizeOwned(null, 'task.abandon.own', { ownerId: null }), false);
|
|
247
|
+
assert.equal(await authorizeOwned('', 'task.abandon.own', { ownerId: '' }), false);
|
|
250
248
|
});
|
|
251
249
|
});
|
|
252
250
|
|
|
@@ -257,7 +255,7 @@ test('C5: builderHasPermissions REFUSES an ownership-scoped key rather than answ
|
|
|
257
255
|
`${key}: "do you hold it" has no unconditional answer — the caller wants authorizeOwned()`);
|
|
258
256
|
}
|
|
259
257
|
// One own key poisons the whole AND-list; unscoped keys still answer normally.
|
|
260
|
-
assert.equal(await builderHasPermissions(ME, ['idea.triage', '
|
|
258
|
+
assert.equal(await builderHasPermissions(ME, ['idea.triage', 'task.abandon.own']), false);
|
|
261
259
|
assert.equal(await builderHasPermissions(ME, ['idea.triage', 'cost.log']), true);
|
|
262
260
|
assert.equal(await builderHasPermissions(ME, ['claim.release.any']), true);
|
|
263
261
|
});
|
|
@@ -266,17 +264,17 @@ test('C5: builderHasPermissions REFUSES an ownership-scoped key rather than answ
|
|
|
266
264
|
// ═══ D — `.any` DOES reach another builder's resource ═════════════════════════
|
|
267
265
|
|
|
268
266
|
test('D1: a DECLARED cross-owner counterpart reaches a row the caller does not own', async () => {
|
|
269
|
-
//
|
|
267
|
+
// task is the catalog's one real pair, and it must keep working: `task.abandon`
|
|
270
268
|
// is the whole point of a cross-owner key. Asserted BOTH with and without the own
|
|
271
269
|
// key, because the counterpart is a standalone admit — it does not merely widen a
|
|
272
270
|
// grant the builder already has.
|
|
273
|
-
await holding(['
|
|
274
|
-
assert.equal(await authorizeOwnedOrAny(ME, '
|
|
275
|
-
'
|
|
276
|
-
assert.equal(await authorizeOwnedOrAny(ME, '
|
|
271
|
+
await holding(['task.abandon.own', 'task.abandon'], async () => {
|
|
272
|
+
assert.equal(await authorizeOwnedOrAny(ME, 'task.abandon.own', { ownerId: SOMEONE_ELSE }), true,
|
|
273
|
+
'task.abandon is exactly the authority to abandon someone else\'s task');
|
|
274
|
+
assert.equal(await authorizeOwnedOrAny(ME, 'task.abandon.own', { ownerId: ME }), true);
|
|
277
275
|
});
|
|
278
|
-
await holding(['
|
|
279
|
-
assert.equal(await authorizeOwnedOrAny(ME, '
|
|
276
|
+
await holding(['task.abandon'], async () => {
|
|
277
|
+
assert.equal(await authorizeOwnedOrAny(ME, 'task.abandon.own', { ownerId: SOMEONE_ELSE }), true);
|
|
280
278
|
});
|
|
281
279
|
});
|
|
282
280
|
|
|
@@ -346,7 +344,7 @@ test('D1c: every declared counterpart is cross-owner, same-resource, and verb-do
|
|
|
346
344
|
`${p.key}'s counterpart ${key} (verb ${c.verb}) must dominate verb ${p.verb}`);
|
|
347
345
|
}
|
|
348
346
|
}
|
|
349
|
-
assert.ok(declared >= 1, 'expected at least the
|
|
347
|
+
assert.ok(declared >= 1, 'expected at least the task pair to be declared');
|
|
350
348
|
// A verb never dominates a wider one, and an unknown verb fails closed — the
|
|
351
349
|
// relation is asserted directly so a future edit to the table is caught here.
|
|
352
350
|
assert.equal(verbDominates('manage', 'read'), true);
|
|
@@ -447,8 +445,8 @@ function loadMutatedCatalog(label, mutate) {
|
|
|
447
445
|
test('F1: a ".own" key that lost its declared scope throws at module load', () => {
|
|
448
446
|
assert.throws(
|
|
449
447
|
() => loadMutatedCatalog('missing scope', (s) => s.replace(
|
|
450
|
-
`{ key: '
|
|
451
|
-
`{ key: '
|
|
448
|
+
`{ key: 'task.abandon.own', system: false, floor: 'xenos', scope: 'own', resource: 'task',`,
|
|
449
|
+
`{ key: 'task.abandon.own', system: false, floor: 'xenos',`)),
|
|
452
450
|
/does not declare scope/,
|
|
453
451
|
);
|
|
454
452
|
});
|
|
@@ -456,7 +454,7 @@ test('F1: a ".own" key that lost its declared scope throws at module load', () =
|
|
|
456
454
|
test('F2: an ownership scope with no resource family throws at module load', () => {
|
|
457
455
|
assert.throws(
|
|
458
456
|
() => loadMutatedCatalog('scope without resource', (s) => s.replace(
|
|
459
|
-
`scope: 'own', resource: '
|
|
457
|
+
`scope: 'own', resource: 'task',`, `scope: 'own',`)),
|
|
460
458
|
/scope and resource must come together/,
|
|
461
459
|
);
|
|
462
460
|
});
|
|
@@ -522,13 +520,13 @@ test('F7: a counterpart that is dangling, cross-resource, or not cross-owner thr
|
|
|
522
520
|
assert.throws(
|
|
523
521
|
() => loadMutatedCatalog('cross-resource counterpart', (s) => s.replace(
|
|
524
522
|
`resource: 'credential', verb: 'manage', counterpart: []`,
|
|
525
|
-
`resource: 'credential', verb: 'manage', counterpart: ['
|
|
523
|
+
`resource: 'credential', verb: 'manage', counterpart: ['task.abandon']`)),
|
|
526
524
|
/never crosses resource families/,
|
|
527
525
|
);
|
|
528
526
|
assert.throws(
|
|
529
527
|
() => loadMutatedCatalog('own key as counterpart', (s) => s.replace(
|
|
530
|
-
`resource: '
|
|
531
|
-
`resource: '
|
|
528
|
+
`resource: 'task', verb: 'act', counterpart: ['task.abandon']`,
|
|
529
|
+
`resource: 'task', verb: 'act', counterpart: ['memory.manage.own']`)),
|
|
532
530
|
/is not cross-owner/,
|
|
533
531
|
);
|
|
534
532
|
});
|
|
@@ -587,7 +585,7 @@ test('G2: the role resolve is untouched — own-scoped keys still resolve, princ
|
|
|
587
585
|
for (const key of ownershipScopedKeys()) {
|
|
588
586
|
assert.ok(resolved.includes(key), `${key} must still resolve — parity depends on it`);
|
|
589
587
|
}
|
|
590
|
-
assert.deepEqual(effectivePermissions(['principal.bfg', '
|
|
588
|
+
assert.deepEqual(effectivePermissions(['principal.bfg', 'task.abandon.own']), ['task.abandon.own']);
|
|
591
589
|
assert.deepEqual(effectivePermissions(['idea.triage', 'not.a.real.permission']), ['idea.triage']);
|
|
592
590
|
assert.deepEqual(effectivePermissions([]), []);
|
|
593
591
|
assert.ok(Object.isFrozen(resolved));
|
|
@@ -601,7 +599,7 @@ test('G3: the kernel port carries the ownership axis on the ONE governance port'
|
|
|
601
599
|
// still the same one object port the earlier slices registered
|
|
602
600
|
assert.equal(typeof governmentPort.resolveBuilderPermissions, 'function');
|
|
603
601
|
assert.equal(typeof governmentPort.resolvePrincipalPermissions, 'function');
|
|
604
|
-
assert.equal(governmentPort.isOwnershipScoped('
|
|
602
|
+
assert.equal(governmentPort.isOwnershipScoped('task.abandon.own'), true);
|
|
605
603
|
assert.equal(governmentPort.isOwnershipScoped('claim.release.any'), false);
|
|
606
604
|
assert.equal(governmentPort.isOwnershipScoped('nope.not.a.key'), false);
|
|
607
605
|
});
|
|
@@ -399,8 +399,8 @@ test('parsePageGates reads permission wherever it sits, and never borrows a sibl
|
|
|
399
399
|
|
|
400
400
|
// ONE-DIRECTIONAL, and deliberately so. A floor STRICTER than the route middleware
|
|
401
401
|
// is legitimate and common: the wall lives in the handler (task.confirm.grade_bypass
|
|
402
|
-
// on an any-builder POST /tasks/:id/confirm; goal.manage.own's owner|manager check
|
|
403
|
-
//
|
|
402
|
+
// on an any-builder POST /tasks/:id/confirm; goal.manage.own's owner|manager check).
|
|
403
|
+
// A floor LOOSER than the middleware is never
|
|
404
404
|
// legitimate — it would tell the governance tab a permission is delegable below the
|
|
405
405
|
// rank the route in fact demands.
|
|
406
406
|
test('B1: no permission floor is LOOSER than the rank gate its route actually carries', () => {
|