@bongos/core 1.17.3 → 1.18.3
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 +212 -332
- package/.devcontainer/healthcheck.sh +0 -0
- package/.devcontainer/init-firewall.sh +0 -0
- package/.devcontainer/post-create.sh +0 -0
- package/GOVERNANCE.md +1 -1
- package/README.md +26 -22
- package/bin/bongos.js +0 -0
- package/config/branding.neutral.json +5 -1
- package/docker-entrypoint.sh +0 -0
- package/docs/adr/0142-project-context-is-prose-only.md +41 -0
- package/docs/adr/0143-onboarding-repo-picker-oauth-scope.md +35 -0
- package/docs/adr/README.md +6 -0
- package/docs/api/openapi.json +1630 -632
- package/docs/api-reference.md +50 -15
- package/docs/file-map.md +0 -18
- package/docs/onboarding/diagrams/05-versioning.mmd +4 -6
- package/docs/onboarding/diagrams/assertions.json +5 -9
- package/docs/recipes/packaging-the-core.md +13 -0
- package/docs/recipes/upgrading-the-core.md +5 -2
- package/migrations/039_newcomer_friendly_column.sql +5 -0
- package/migrations/102_remove_box_first_provision_approval.sql +4 -0
- package/migrations/core_192_github_oauth_tokens.sql +33 -0
- package/modules/hall-ui/public/builders.js +37 -0
- package/modules/hall-ui/public/goals-page.js +98 -176
- package/modules/hall-ui/public/goals.css +183 -0
- package/modules/hall-ui/public/goals.html +35 -261
- package/modules/hall-ui/public/goals.js +7 -2
- package/modules/hall-ui/public/hall-widgets.js +12 -4
- package/modules/hall-ui/public/my-projects.html +78 -0
- package/modules/hall-ui/public/my-projects.js +142 -0
- package/modules/hall-ui/public/scouting.html +45 -0
- package/modules/hall-ui/public/scouting.js +73 -0
- package/modules/hall-ui/public/settings.css +355 -0
- package/modules/hall-ui/public/settings.html +86 -414
- package/modules/hall-ui/public/settings.js +96 -35
- package/modules/hall-ui/public/style.css +31 -0
- package/modules/hall-ui/public/watch.css +184 -0
- package/modules/hall-ui/public/watch.html +111 -285
- package/modules/hall-ui/public/watch.js +59 -21
- package/modules/status-ui/public/explore.html +54 -0
- package/modules/status-ui/public/explore.js +67 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/public-landing/create.html +292 -19
- package/public-landing/index.html +11 -76
- package/scripts/cron/style_guide_audit.sh +0 -0
- package/scripts/gds/api.js +0 -0
- package/scripts/gds/architect-audit.js +0 -0
- package/scripts/gds/audit-authorship.sh +0 -0
- package/scripts/gds/audit-replay.sh +0 -0
- package/scripts/gds/box.js +0 -0
- package/scripts/gds/capture.js +0 -0
- package/scripts/gds/cli-lib.js +11 -5
- package/scripts/gds/conductor-dispatch.js +0 -0
- package/scripts/gds/coreB-carve-proof.sh +0 -0
- package/scripts/gds/db-backup-nightly.sh +0 -0
- package/scripts/gds/db-backup-offsite.sh +0 -0
- package/scripts/gds/doctor.js +0 -0
- package/scripts/gds/grade-smoke.js +0 -0
- package/scripts/gds/hooks/pre-push.js +0 -0
- package/scripts/gds/install-git-hooks.js +0 -0
- package/scripts/gds/lint-migrations.js +102 -0
- package/scripts/gds/login.js +42 -10
- package/scripts/gds/main-audit.js +0 -0
- package/scripts/gds/mirror-redact.js +5 -0
- package/scripts/gds/oauth-secret.js +0 -0
- package/scripts/gds/onboarding-analyze.js +0 -0
- package/scripts/gds/onboarding-ship.js +0 -0
- package/scripts/gds/package-core.js +263 -4
- package/scripts/gds/paste-token.js +0 -0
- package/scripts/gds/portability-smoke-art.sh +0 -0
- package/scripts/gds/portability-smoke-memory.sh +0 -0
- package/scripts/gds/provision.js +270 -11
- package/scripts/gds/publish-manifest.js +15 -0
- package/scripts/gds/push-main.js +0 -0
- package/scripts/gds/render-ideas.js +0 -0
- package/scripts/gds/run-unit-tests.js +38 -14
- package/scripts/gds/security-baseline-check.sh +0 -0
- package/scripts/gds/session.js +0 -0
- package/scripts/gds/setup.js +41 -0
- package/scripts/gds/ship.js +126 -32
- package/scripts/gds/smoke-batch-claim.sh +0 -0
- package/scripts/gds/smoke-batch-release.sh +0 -0
- package/scripts/gds/smoke-box.sh +0 -0
- package/scripts/gds/smoke-claim-consistency.sh +0 -0
- package/scripts/gds/smoke-dependencies.sh +0 -0
- package/scripts/gds/smoke-write.sh +0 -0
- package/scripts/gds/upgrade.js +147 -5
- package/scripts/hall-preview/server.js +18 -1
- package/scripts/migrate.sh +10 -0
- package/scripts/migrate_cost_ledger.sh +0 -0
- package/scripts/render-diagrams.sh +0 -0
- package/scripts/security/dep-audit.js +0 -0
- package/scripts/setup-builder.js +0 -0
- package/scripts/setup-builder.sh +0 -0
- package/scripts/snapshots/maybe-render-on-deploy.sh +0 -0
- package/src/bongos/auth.js +362 -5
- package/src/bongos/db.js +66 -0
- package/src/bongos/github-repos.js +93 -0
- package/src/bongos/routes/auth.js +93 -11
- package/src/bongos/routes/github.js +90 -0
- package/src/bongos/routes/instance.js +15 -3
- package/src/bongos/routes.js +2 -0
- package/src/branding.js +17 -2
- package/src/instance-config.js +16 -0
- package/src/module-api.js +48 -1
- package/tests/branding.mjs +29 -23
- package/tests/github_repos_helpers.mjs +91 -0
- package/tests/github_token_store.mjs +121 -0
- package/tests/idp_device_exchange.mjs +105 -0
- package/tests/idp_federation.mjs +100 -0
- package/tests/migrations_forward_safe.mjs +86 -0
- package/tests/module_api.mjs +1 -1
- package/tests/package_core.mjs +246 -1
- package/tests/provision.mjs +226 -0
- package/tests/pruned_lockfile_integrity.mjs +79 -0
- package/tests/ship_ci_deploy.mjs +60 -0
- package/tests/upgrade.mjs +254 -5
- package/.gitignore +0 -91
- package/scripts/hall-preview/fixtures/access-requests.json +0 -1
- package/scripts/hall-preview/fixtures/achievements.json +0 -76
- package/scripts/hall-preview/fixtures/analytics__builder.json +0 -251
- package/scripts/hall-preview/fixtures/audit-log.json +0 -1
- package/scripts/hall-preview/fixtures/boxes__cost-ledger.json +0 -1
- package/scripts/hall-preview/fixtures/builders__active-claims.json +0 -1
- package/scripts/hall-preview/fixtures/builders__directory.json +0 -81
- package/scripts/hall-preview/fixtures/builders__roster.json +0 -1
- package/scripts/hall-preview/fixtures/goals.json +0 -502
- package/scripts/hall-preview/fixtures/goals__25.json +0 -96
- package/scripts/hall-preview/fixtures/goals__26.json +0 -67
- package/scripts/hall-preview/fixtures/goals__3.json +0 -103
- package/scripts/hall-preview/fixtures/goals__35.json +0 -49
- package/scripts/hall-preview/fixtures/goals__conflicts.json +0 -154
- package/scripts/hall-preview/fixtures/goals__detail.json +0 -96
- package/scripts/hall-preview/fixtures/goals__inbox.json +0 -5
- package/scripts/hall-preview/fixtures/goals__requests.json +0 -3
- package/scripts/hall-preview/fixtures/grades__by-builder.json +0 -1
- package/scripts/hall-preview/fixtures/inbox.json +0 -330
- package/scripts/hall-preview/fixtures/instance.json +0 -124
- package/scripts/hall-preview/fixtures/leaderboard.json +0 -184
- package/scripts/hall-preview/fixtures/me.json +0 -476
- package/scripts/hall-preview/fixtures/me__recent-ships.json +0 -304
- package/scripts/hall-preview/fixtures/override-requests.json +0 -1
- package/scripts/hall-preview/fixtures/provisioning__fleet.json +0 -1
- package/scripts/hall-preview/fixtures/provisioning__fleet__cost-ledger.json +0 -1
- package/scripts/hall-preview/fixtures/provisioning__instances.json +0 -1
- package/scripts/hall-preview/fixtures/public__analytics__ecosystem.json +0 -516
- package/scripts/hall-preview/fixtures/public__bounty-table.json +0 -37
- package/scripts/hall-preview/fixtures/public__cost-summary.json +0 -742
- package/scripts/hall-preview/fixtures/public__estimation-drift.json +0 -41
- package/scripts/hall-preview/fixtures/public__grades.json +0 -50
- package/scripts/hall-preview/fixtures/public__progress.json +0 -124
- package/scripts/hall-preview/fixtures/public__recent-shipped.json +0 -426
- package/scripts/hall-preview/fixtures/public__repo-health.json +0 -160
- package/scripts/hall-preview/fixtures/public__repo-info.json +0 -5
- package/scripts/hall-preview/fixtures/security__docs.json +0 -1
- package/scripts/hall-preview/fixtures/security__reports.json +0 -1
- package/scripts/hall-preview/fixtures/sessions__search.json +0 -1
- package/scripts/hall-preview/fixtures/tasks.json +0 -2420
- package/scripts/hall-preview/fixtures/tasks__claimable.json +0 -2555
- package/scripts/hall-preview/fixtures/tasks__detail.json +0 -111
- package/scripts/hall-preview/fixtures/tasks__peer-votes__feed.json +0 -330
- package/scripts/hall-preview/fixtures/versions.json +0 -89
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// tests/github_token_store.mjs — DB-free tests for the encrypted repo-token store
|
|
2
|
+
// and the /github/repos gating (task 1002099 / ADR 0141). No real Postgres: db
|
|
3
|
+
// helpers take an injectable pool `p`; no real GitHub: the route's 503/403 branches
|
|
4
|
+
// don't reach GitHub. secret-box reads BUILDER_SECRET_KEY fresh each call, so we set
|
|
5
|
+
// a valid key up front and toggle it per test.
|
|
6
|
+
|
|
7
|
+
import { strict as assert } from 'node:assert';
|
|
8
|
+
import { test } from 'node:test';
|
|
9
|
+
import { createRequire } from 'node:module';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import { fileURLToPath } from 'node:url';
|
|
12
|
+
|
|
13
|
+
// A valid 32-byte key (64 hex chars) BEFORE requiring the modules — secret-box configured.
|
|
14
|
+
process.env.BUILDER_SECRET_KEY = 'ab'.repeat(32);
|
|
15
|
+
|
|
16
|
+
const require = createRequire(import.meta.url);
|
|
17
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
18
|
+
const db = require(path.join(ROOT, 'src/bongos/db.js'));
|
|
19
|
+
const secretBox = require(path.join(ROOT, 'src/bongos/secret-box.js'));
|
|
20
|
+
const buildGithubRouter = require(path.join(ROOT, 'src/bongos/routes/github.js'));
|
|
21
|
+
const { attachFail } = require(path.join(ROOT, 'src/bongos/middleware/error-envelope.js'));
|
|
22
|
+
|
|
23
|
+
// Mock pool: records every query, returns programmed rows for SELECTs.
|
|
24
|
+
function mockPool(selectRows) {
|
|
25
|
+
const calls = [];
|
|
26
|
+
return {
|
|
27
|
+
calls,
|
|
28
|
+
query: async (text, params) => {
|
|
29
|
+
calls.push({ text, params });
|
|
30
|
+
return { rows: /^\s*SELECT/i.test(text) ? (selectRows || []) : [] };
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
test('secret-box is configured for this test file', () => {
|
|
36
|
+
assert.equal(secretBox.isConfigured(), true);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('upsertGithubToken: stores an ENCRYPTED blob (never plaintext) + a future expires_at', async () => {
|
|
40
|
+
const p = mockPool();
|
|
41
|
+
const out = await db.upsertGithubToken({ builderId: 7, token: 'ghs_secret', scope: 'read:user public_repo', p });
|
|
42
|
+
assert.equal(out.stored, true);
|
|
43
|
+
const insert = p.calls.find((c) => /INSERT INTO github_oauth_tokens/i.test(c.text));
|
|
44
|
+
assert.ok(insert, 'issued an INSERT');
|
|
45
|
+
const enc = insert.params[1];
|
|
46
|
+
assert.notEqual(enc, 'ghs_secret', 'the plaintext token must never be stored');
|
|
47
|
+
assert.equal(secretBox.decrypt(enc), 'ghs_secret', 'the stored blob round-trips back to the token');
|
|
48
|
+
const expiresAt = insert.params[3];
|
|
49
|
+
assert.ok(expiresAt instanceof Date && expiresAt.getTime() > Date.now(), 'has a future expires_at');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('getGithubToken: decrypts a live (unexpired) row', async () => {
|
|
53
|
+
const enc = secretBox.encrypt('ghs_live');
|
|
54
|
+
const p = mockPool([{ token_enc: enc, scope: 'x', expires_at: new Date(Date.now() + 60_000) }]);
|
|
55
|
+
const row = await db.getGithubToken({ builderId: 7, p });
|
|
56
|
+
assert.equal(row.token, 'ghs_live');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('getGithubToken: an EXPIRED row returns null AND is swept (DELETE issued)', async () => {
|
|
60
|
+
const enc = secretBox.encrypt('ghs_old');
|
|
61
|
+
const p = mockPool([{ token_enc: enc, scope: 'x', expires_at: new Date(Date.now() - 60_000) }]);
|
|
62
|
+
const row = await db.getGithubToken({ builderId: 7, p });
|
|
63
|
+
assert.equal(row, null);
|
|
64
|
+
assert.ok(p.calls.some((c) => /DELETE FROM github_oauth_tokens/i.test(c.text)), 'lazily swept the expired row');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('secret-box UNCONFIGURED: upsert no-ops (no plaintext ever written), get returns null', async () => {
|
|
68
|
+
const saved = process.env.BUILDER_SECRET_KEY;
|
|
69
|
+
delete process.env.BUILDER_SECRET_KEY;
|
|
70
|
+
try {
|
|
71
|
+
const p = mockPool();
|
|
72
|
+
const out = await db.upsertGithubToken({ builderId: 7, token: 'ghs_secret', p });
|
|
73
|
+
assert.equal(out.stored, false);
|
|
74
|
+
assert.equal(p.calls.length, 0, 'no query issued when secret-box is off');
|
|
75
|
+
assert.equal(await db.getGithubToken({ builderId: 7, p }), null);
|
|
76
|
+
} finally {
|
|
77
|
+
process.env.BUILDER_SECRET_KEY = saved;
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// ---- route gating: the 503 / 403 signals the wizard branches on ----
|
|
82
|
+
function fakeRes() {
|
|
83
|
+
const res = { _status: 200, _body: null };
|
|
84
|
+
res.status = (c) => { res._status = c; return res; };
|
|
85
|
+
res.json = (b) => { res._body = b; return res; };
|
|
86
|
+
attachFail({}, res, () => {}); // installs res.fail exactly as the router does
|
|
87
|
+
return res;
|
|
88
|
+
}
|
|
89
|
+
function finalHandler(router, method, pth) {
|
|
90
|
+
const layer = router.stack.find((l) => l.route && l.route.path === pth && l.route.methods[method]);
|
|
91
|
+
const stack = layer.route.stack;
|
|
92
|
+
return stack[stack.length - 1].handle; // the handler after requireBuilder
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
test('GET /github/repos → 503 github_repos_unavailable when secret-box is unconfigured', async () => {
|
|
96
|
+
const saved = process.env.BUILDER_SECRET_KEY;
|
|
97
|
+
delete process.env.BUILDER_SECRET_KEY;
|
|
98
|
+
try {
|
|
99
|
+
const handler = finalHandler(buildGithubRouter(), 'get', '/github/repos');
|
|
100
|
+
const res = fakeRes();
|
|
101
|
+
await handler({ builder: { id: 1 }, query: {} }, res);
|
|
102
|
+
assert.equal(res._status, 503);
|
|
103
|
+
assert.equal(res._body.error.code, 'github_repos_unavailable');
|
|
104
|
+
} finally {
|
|
105
|
+
process.env.BUILDER_SECRET_KEY = saved;
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test('GET /github/repos → 403 repo_scope_required when configured but no stored token', async () => {
|
|
110
|
+
const orig = db.getGithubToken;
|
|
111
|
+
db.getGithubToken = async () => null; // configured, but this builder never granted repo access
|
|
112
|
+
try {
|
|
113
|
+
const handler = finalHandler(buildGithubRouter(), 'get', '/github/repos');
|
|
114
|
+
const res = fakeRes();
|
|
115
|
+
await handler({ builder: { id: 1 }, query: {} }, res);
|
|
116
|
+
assert.equal(res._status, 403);
|
|
117
|
+
assert.equal(res._body.error.code, 'repo_scope_required');
|
|
118
|
+
} finally {
|
|
119
|
+
db.getGithubToken = orig;
|
|
120
|
+
}
|
|
121
|
+
});
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// tests/idp_device_exchange.mjs
|
|
2
|
+
//
|
|
3
|
+
// CLI federation crypto (ADR 0141 §1, task #1002108): the hub mints an Ed25519
|
|
4
|
+
// assertion (modules/platform-identity/hub-keys.js) and the instance verifies it
|
|
5
|
+
// OFFLINE (src/bongos/auth.js verifyHubAssertion). The two live across the
|
|
6
|
+
// core<->module boundary and cannot share code, so this test drives a REAL
|
|
7
|
+
// mint→verify roundtrip with a generated keypair and then tries to break it:
|
|
8
|
+
// tamper, wrong audience, expiry, wrong key, algorithm-confusion, malformed.
|
|
9
|
+
// DB-free + network-free.
|
|
10
|
+
//
|
|
11
|
+
// Run: node tests/idp_device_exchange.mjs
|
|
12
|
+
|
|
13
|
+
import { strict as assert } from 'node:assert';
|
|
14
|
+
import { createRequire } from 'node:module';
|
|
15
|
+
import crypto from 'node:crypto';
|
|
16
|
+
|
|
17
|
+
const require = createRequire(import.meta.url);
|
|
18
|
+
const auth = require('../src/bongos/auth.js');
|
|
19
|
+
const hubKeys = require('../modules/platform-identity/hub-keys.js');
|
|
20
|
+
|
|
21
|
+
let passed = 0, failed = 0;
|
|
22
|
+
function t(name, fn) {
|
|
23
|
+
try { fn(); passed++; console.log(` PASS ${name}`); }
|
|
24
|
+
catch (e) { failed++; console.log(` FAIL ${name}\n ${e.stack || e.message}`); }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// A hub keypair (base64 DER), as generateHubKeypair produces for env placement.
|
|
28
|
+
const { privateKeyB64, publicKeyB64 } = hubKeys.generateHubKeypair();
|
|
29
|
+
const signingKey = crypto.createPrivateKey({ key: Buffer.from(privateKeyB64, 'base64'), format: 'der', type: 'pkcs8' });
|
|
30
|
+
const publicKey = crypto.createPublicKey({ key: Buffer.from(publicKeyB64, 'base64'), format: 'der', type: 'spki' });
|
|
31
|
+
|
|
32
|
+
function mint(overrides = {}, opts = {}) {
|
|
33
|
+
return hubKeys.mintHubAssertion(
|
|
34
|
+
{ clientId: 'client-x', githubId: 42, githubLogin: 'octo', displayName: 'Octo', avatarUrl: 'a', ...overrides },
|
|
35
|
+
{ signingKey, ...opts },
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
console.log('hub assertion mint → instance verify (roundtrip):');
|
|
40
|
+
|
|
41
|
+
t('a valid assertion verifies, and carries IDENTITY ONLY (no rank)', () => {
|
|
42
|
+
const p = auth.verifyHubAssertion(mint(), { publicKey, clientId: 'client-x' });
|
|
43
|
+
assert.equal(p.sub, '42');
|
|
44
|
+
assert.equal(p.github_login, 'octo');
|
|
45
|
+
assert.equal(p.aud, 'client-x');
|
|
46
|
+
assert.equal(p.iss, 'cloudbongos-hub');
|
|
47
|
+
assert.equal(p.rank, undefined, 'assertion must never carry a rank');
|
|
48
|
+
assert.equal(p.credits, undefined);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
console.log('\nadversarial — every one of these MUST be rejected:');
|
|
52
|
+
|
|
53
|
+
t('wrong audience (a code for client A cannot sign you into client B)', () => {
|
|
54
|
+
assert.throws(() => auth.verifyHubAssertion(mint(), { publicKey, clientId: 'other-client' }), /audience/);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
t('expired assertion (minted 10 min ago; TTL is ~120s)', () => {
|
|
58
|
+
const stale = mint({}, { nowMs: Date.now() - 10 * 60 * 1000 });
|
|
59
|
+
assert.throws(() => auth.verifyHubAssertion(stale, { publicKey, clientId: 'client-x' }), /expired/);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
t('tampered payload (escalate sub to another github_id) fails the signature', () => {
|
|
63
|
+
const [h, p, s] = mint().split('.');
|
|
64
|
+
const payload = JSON.parse(Buffer.from(p, 'base64url').toString('utf8'));
|
|
65
|
+
payload.sub = '999';
|
|
66
|
+
const forged = `${h}.${Buffer.from(JSON.stringify(payload)).toString('base64url')}.${s}`;
|
|
67
|
+
assert.throws(() => auth.verifyHubAssertion(forged, { publicKey, clientId: 'client-x' }), /signature/);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
t('a different signing key is rejected (one instance cannot forge for another)', () => {
|
|
71
|
+
const otherPub = crypto.generateKeyPairSync('ed25519').publicKey;
|
|
72
|
+
assert.throws(() => auth.verifyHubAssertion(mint(), { publicKey: otherPub, clientId: 'client-x' }), /signature/);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
t('algorithm-confusion (header alg swapped to "none") is rejected', () => {
|
|
76
|
+
const [h, p, s] = mint().split('.');
|
|
77
|
+
const hdr = JSON.parse(Buffer.from(h, 'base64url').toString('utf8'));
|
|
78
|
+
hdr.alg = 'none';
|
|
79
|
+
const forged = `${Buffer.from(JSON.stringify(hdr)).toString('base64url')}.${p}.${s}`;
|
|
80
|
+
assert.throws(() => auth.verifyHubAssertion(forged, { publicKey, clientId: 'client-x' }), /alg|signature/);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
t('malformed / empty tokens are rejected', () => {
|
|
84
|
+
assert.throws(() => auth.verifyHubAssertion('not-a-jwt', { publicKey, clientId: 'client-x' }), /malformed/);
|
|
85
|
+
assert.throws(() => auth.verifyHubAssertion('', { publicKey, clientId: 'client-x' }), /malformed/);
|
|
86
|
+
assert.throws(() => auth.verifyHubAssertion('a.b.c.d', { publicKey, clientId: 'client-x' }), /malformed/);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
t('a missing public key never verifies (fail closed)', () => {
|
|
90
|
+
assert.throws(() => auth.verifyHubAssertion(mint(), { publicKey: null, clientId: 'client-x' }), /public key/);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
t('mintHubAssertion refuses without a signing key', () => {
|
|
94
|
+
assert.throws(() => hubKeys.mintHubAssertion({ clientId: 'c', githubId: 1, githubLogin: 'x' }, {}), /signing key/);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
t('generateHubKeypair yields distinct keypairs', () => {
|
|
98
|
+
const a = hubKeys.generateHubKeypair();
|
|
99
|
+
const b = hubKeys.generateHubKeypair();
|
|
100
|
+
assert.notEqual(a.privateKeyB64, b.privateKeyB64);
|
|
101
|
+
assert.notEqual(a.publicKeyB64, b.publicKeyB64);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
console.log(`\nidp_device_exchange: ${passed} passed, ${failed} failed`);
|
|
105
|
+
if (failed > 0) process.exit(1);
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// tests/idp_federation.mjs
|
|
2
|
+
//
|
|
3
|
+
// Instance-side platform-IdP federation (ADR 0141 §1, task #1002107): when
|
|
4
|
+
// auth.idp is set, web sign-in federates through the Cloud Bongos hub instead of
|
|
5
|
+
// github.com. DB-free + network-free — tests the pure authorize-URL builder and
|
|
6
|
+
// the DORMANCY invariant (federation stays OFF, so the direct-GitHub path is
|
|
7
|
+
// unchanged, unless the branding auth.idp flag is explicitly true). The identity→
|
|
8
|
+
// admission→session logic is the SAME shared primitives completeWebFlow uses
|
|
9
|
+
// (firstTimeAdmissionBlocked/upsertBuilder/createSession — covered elsewhere), so
|
|
10
|
+
// it can't drift; the network exchange (exchangeIdpCode) is an integration path.
|
|
11
|
+
//
|
|
12
|
+
// Run: node tests/idp_federation.mjs
|
|
13
|
+
|
|
14
|
+
import { strict as assert } from 'node:assert';
|
|
15
|
+
import { createRequire } from 'node:module';
|
|
16
|
+
|
|
17
|
+
const require = createRequire(import.meta.url);
|
|
18
|
+
const auth = require('../src/bongos/auth.js');
|
|
19
|
+
|
|
20
|
+
let passed = 0, failed = 0;
|
|
21
|
+
function t(name, fn) {
|
|
22
|
+
try { fn(); passed++; console.log(` PASS ${name}`); }
|
|
23
|
+
catch (e) { failed++; console.log(` FAIL ${name}\n ${e.stack || e.message}`); }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
console.log('idpAuthorizeUrl (pure hub authorize-URL builder):');
|
|
27
|
+
|
|
28
|
+
t('builds the hub /sso/authorize URL with client_id, redirect_uri, state', () => {
|
|
29
|
+
const url = auth.idpAuthorizeUrl('https://cloudbongos.com', 'client-abc', 'st8', 'https://proj.example.com/api/gds/auth/web/callback');
|
|
30
|
+
const u = new URL(url);
|
|
31
|
+
assert.equal(u.origin, 'https://cloudbongos.com');
|
|
32
|
+
assert.equal(u.pathname, '/api/gds/sso/authorize');
|
|
33
|
+
assert.equal(u.searchParams.get('client_id'), 'client-abc');
|
|
34
|
+
assert.equal(u.searchParams.get('redirect_uri'), 'https://proj.example.com/api/gds/auth/web/callback');
|
|
35
|
+
assert.equal(u.searchParams.get('state'), 'st8');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
t('trims a trailing slash on the hub origin', () => {
|
|
39
|
+
const url = auth.idpAuthorizeUrl('https://cloudbongos.com/', 'c', 's', 'https://p/cb');
|
|
40
|
+
assert.equal(new URL(url).pathname, '/api/gds/sso/authorize');
|
|
41
|
+
assert.ok(!url.includes('com//api'), 'no double slash between origin and path');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
t('percent-encodes redirect_uri + state (no query injection)', () => {
|
|
45
|
+
const url = auth.idpAuthorizeUrl('https://hub', 'c', 'a&b=evil', 'https://p/cb?x=1');
|
|
46
|
+
const u = new URL(url);
|
|
47
|
+
// The raw string must not let an injected &b=evil become its own param.
|
|
48
|
+
assert.equal(u.searchParams.get('state'), 'a&b=evil');
|
|
49
|
+
assert.equal(u.searchParams.get('redirect_uri'), 'https://p/cb?x=1');
|
|
50
|
+
assert.equal(u.searchParams.get('b'), null);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
console.log('\ndormancy invariant (federation OFF by default → direct-GitHub fallback):');
|
|
54
|
+
|
|
55
|
+
// The neutral branding pack ships auth.idp=false, and this repo has no
|
|
56
|
+
// config/branding.json, so idp is off. Even with the hub env creds present,
|
|
57
|
+
// idpConfigured() must stay false — the branding flag is the gate, so a stray env
|
|
58
|
+
// var can never silently flip a self-hosted instance into federation.
|
|
59
|
+
function withHubEnv(fn) {
|
|
60
|
+
const keys = ['CLOUDBONGOS_HUB_ORIGIN', 'CLOUDBONGOS_HUB_CLIENT_ID', 'CLOUDBONGOS_HUB_CLIENT_SECRET'];
|
|
61
|
+
const prev = keys.map((k) => [k, process.env[k]]);
|
|
62
|
+
process.env.CLOUDBONGOS_HUB_ORIGIN = 'https://cloudbongos.com';
|
|
63
|
+
process.env.CLOUDBONGOS_HUB_CLIENT_ID = 'client-abc';
|
|
64
|
+
process.env.CLOUDBONGOS_HUB_CLIENT_SECRET = 'x'.repeat(64);
|
|
65
|
+
try { fn(); }
|
|
66
|
+
finally { for (const [k, v] of prev) { if (v === undefined) delete process.env[k]; else process.env[k] = v; } }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
t('idpConfigured() is false by default even with hub env present (flag gates it)', () => {
|
|
70
|
+
withHubEnv(() => {
|
|
71
|
+
assert.equal(auth.idpConfigured(), false);
|
|
72
|
+
assert.equal(auth.loadIdpConfig(), null);
|
|
73
|
+
assert.equal(auth.idpEnabled(), false);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
t('buildIdpAuthorizeUrl throws when the IdP is not configured (never silently misroutes)', () => {
|
|
78
|
+
withHubEnv(() => {
|
|
79
|
+
assert.throws(() => auth.buildIdpAuthorizeUrl('s', 'https://p/cb'), /not configured/);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
console.log('\nmembership check-in role (§3 — coarse, never a rank):');
|
|
84
|
+
|
|
85
|
+
t('membershipRole is builder by default, owner for the configured firstAdmin', () => {
|
|
86
|
+
const prev = process.env.CLOUDBONGOS_FIRST_ADMIN;
|
|
87
|
+
process.env.CLOUDBONGOS_FIRST_ADMIN = 'ownerlogin';
|
|
88
|
+
try {
|
|
89
|
+
assert.equal(auth.membershipRole({ githubLogin: 'ownerlogin' }), 'owner');
|
|
90
|
+
assert.equal(auth.membershipRole({ githubLogin: 'OwnerLogin' }), 'owner', 'case-insensitive');
|
|
91
|
+
assert.equal(auth.membershipRole({ githubLogin: 'someone-else' }), 'builder');
|
|
92
|
+
assert.equal(auth.membershipRole({}), 'builder');
|
|
93
|
+
} finally {
|
|
94
|
+
if (prev === undefined) delete process.env.CLOUDBONGOS_FIRST_ADMIN;
|
|
95
|
+
else process.env.CLOUDBONGOS_FIRST_ADMIN = prev;
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
console.log(`\nidp_federation: ${passed} passed, ${failed} failed`);
|
|
100
|
+
if (failed > 0) process.exit(1);
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// tests/migrations_forward_safe.mjs
|
|
2
|
+
//
|
|
3
|
+
// task 1002223 / audit H10 (finding F6) — the FAIL-CLOSED forward-safety gate for core migrations.
|
|
4
|
+
// `bongos upgrade` auto-rollback reverts the code pin but NOT the DB, so a core migration must be
|
|
5
|
+
// forward-safe (old code still runs on the new schema) — or explicitly acknowledge the exception
|
|
6
|
+
// with a `-- forward-unsafe: <reason>` pragma. This runs in the DB-free unit gate every PR: add a
|
|
7
|
+
// DROP COLUMN / RENAME / ALTER TYPE without the pragma and this REDs, long before a field rollback
|
|
8
|
+
// strands old code on new schema. (The migrate.sh notice is advisory; THIS is the enforcement.)
|
|
9
|
+
//
|
|
10
|
+
// Run: node tests/migrations_forward_safe.mjs
|
|
11
|
+
|
|
12
|
+
import { strict as assert } from 'node:assert';
|
|
13
|
+
import { createRequire } from 'node:module';
|
|
14
|
+
import { join, dirname } from 'node:path';
|
|
15
|
+
import { fileURLToPath } from 'node:url';
|
|
16
|
+
|
|
17
|
+
const require = createRequire(import.meta.url);
|
|
18
|
+
const lint = require('../scripts/gds/lint-migrations.js');
|
|
19
|
+
const REPO_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
20
|
+
|
|
21
|
+
let passed = 0, failed = 0;
|
|
22
|
+
function t(name, fn) {
|
|
23
|
+
try { fn(); passed++; console.log(` PASS ${name}`); }
|
|
24
|
+
catch (e) { failed++; console.log(` FAIL ${name}\n ${e.message}`); }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// ---- THE GATE: the committed core migrations must be forward-safe (or acknowledged) ----
|
|
28
|
+
t('core migrations: zero UNACKNOWLEDGED forward-unsafe operations', () => {
|
|
29
|
+
const { violations, acknowledged, scanned } = lint.scanMigrationsDir(join(REPO_ROOT, 'migrations'));
|
|
30
|
+
assert.ok(scanned > 0, 'expected to scan some core migrations');
|
|
31
|
+
assert.equal(violations.length, 0,
|
|
32
|
+
`unacknowledged forward-unsafe migration(s): ${violations.map((v) => `${v.file} [${v.unsafe.join(',')}]`).join('; ')}\n`
|
|
33
|
+
+ ' Add a top-of-file `-- forward-unsafe: <reason>` comment if the operation is intentional + safe here.');
|
|
34
|
+
// sanity: the two known historical exceptions stay acknowledged (not silently dropped)
|
|
35
|
+
assert.ok(acknowledged.some((a) => a.file.startsWith('039_')), 'expected 039 to remain an acknowledged exception');
|
|
36
|
+
assert.ok(acknowledged.some((a) => a.file.startsWith('102_')), 'expected 102 to remain an acknowledged exception');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// ---- the scanner classifies operations correctly ----
|
|
40
|
+
t('scanMigrationSql: flags each genuinely forward-unsafe operation', () => {
|
|
41
|
+
const cases = {
|
|
42
|
+
'drop-table': 'DROP TABLE widgets;',
|
|
43
|
+
'drop-column': 'ALTER TABLE t DROP COLUMN c;',
|
|
44
|
+
'rename-column': 'ALTER TABLE t RENAME COLUMN a TO b;',
|
|
45
|
+
'rename-table': 'ALTER TABLE t RENAME TO t2;',
|
|
46
|
+
'alter-column-type': 'ALTER TABLE t ALTER COLUMN c TYPE bigint;',
|
|
47
|
+
'set-not-null': 'ALTER TABLE t ALTER COLUMN c SET NOT NULL;',
|
|
48
|
+
};
|
|
49
|
+
for (const [expected, sql] of Object.entries(cases)) {
|
|
50
|
+
const r = lint.scanMigrationSql(sql);
|
|
51
|
+
assert.ok(r.unsafe.includes(expected), `expected ${expected} for: ${sql} — got ${JSON.stringify(r.unsafe)}`);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
t('scanMigrationSql: does NOT flag additive / recreate-in-place operations', () => {
|
|
56
|
+
const safe = [
|
|
57
|
+
'CREATE TABLE t (id int);',
|
|
58
|
+
'ALTER TABLE t ADD COLUMN c text;',
|
|
59
|
+
'CREATE INDEX idx ON t (c);',
|
|
60
|
+
'DROP INDEX IF EXISTS idx; CREATE INDEX idx ON t (c);', // recreate-in-place
|
|
61
|
+
'DROP TRIGGER IF EXISTS trg ON t; CREATE TRIGGER trg BEFORE INSERT ON t ...;',
|
|
62
|
+
'CREATE OR REPLACE VIEW v AS SELECT 1;',
|
|
63
|
+
'DROP VIEW IF EXISTS v; CREATE VIEW v AS SELECT 1;',
|
|
64
|
+
'DROP FUNCTION IF EXISTS f(); CREATE FUNCTION f() RETURNS void ...;',
|
|
65
|
+
"INSERT INTO t (id) VALUES (1);",
|
|
66
|
+
];
|
|
67
|
+
for (const sql of safe) {
|
|
68
|
+
const r = lint.scanMigrationSql(sql);
|
|
69
|
+
assert.equal(r.unsafe.length, 0, `false positive on safe SQL: ${sql} — got ${JSON.stringify(r.unsafe)}`);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
t('scanMigrationSql: the acknowledgement pragma flips an unsafe op to acknowledged', () => {
|
|
74
|
+
const r = lint.scanMigrationSql('-- forward-unsafe: intentional, two-step deploy\nALTER TABLE t DROP COLUMN c;');
|
|
75
|
+
assert.ok(r.unsafe.includes('drop-column'));
|
|
76
|
+
assert.equal(r.acknowledged, true);
|
|
77
|
+
assert.match(r.ackReason, /two-step/);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
t('scanMigrationSql: an unsafe keyword only inside a comment does NOT count', () => {
|
|
81
|
+
const r = lint.scanMigrationSql('-- this migration does NOT drop table anything\nCREATE TABLE t (id int);');
|
|
82
|
+
assert.equal(r.unsafe.length, 0, `comment text must not trip the scan — got ${JSON.stringify(r.unsafe)}`);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
console.log(`\nmigrations_forward_safe: ${passed} passed, ${failed} failed`);
|
|
86
|
+
process.exit(failed ? 1 : 0);
|
package/tests/module_api.mjs
CHANGED
|
@@ -20,7 +20,7 @@ const seams = require(path.join(ROOT, 'src', 'module-seams.js'));
|
|
|
20
20
|
// was added; no enabled module at its pinned coreVersion lost a capability.
|
|
21
21
|
const PUBLISHED_SURFACE = [
|
|
22
22
|
'CORE_VERSION',
|
|
23
|
-
'requireBuilder', 'requireRank', 'rankMeetsThreshold', 'requireBfgPrincipal', 'allowBoxScope',
|
|
23
|
+
'requireBuilder', 'requireRank', 'rankMeetsThreshold', 'requireBfgPrincipal', 'allowBoxScope', 'resolveSession',
|
|
24
24
|
'pool', 'getPool', 'instanceDbName', 'withTx', 'getBuilderById',
|
|
25
25
|
'secretBox', // Gemini-key storage relocated to modules/art-pipeline + its 'art.getBuilderGeminiKeyRow' port (ADR 0102)
|
|
26
26
|
'routeRankCheck', 'permissionPathCheck', // BV1.R73: kernel trust classifiers (1.6.0)
|