@bongos/core 1.19.130 → 1.19.131
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 +18 -13
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/run-unit-tests.js +1 -1
- package/scripts/gds/ship.js +33 -1
- package/src/module-api.js +3 -1
- package/tests/ship_publish_error.mjs +119 -0
- package/tests/subdomain_auth.mjs +44 -12
package/.bongos-core.json
CHANGED
|
@@ -2,22 +2,22 @@
|
|
|
2
2
|
"artifact": "bongos-core",
|
|
3
3
|
"manifest_schema": 1,
|
|
4
4
|
"generator": "scripts/gds/package-core.js",
|
|
5
|
-
"core_version": "1.19.
|
|
6
|
-
"core_contract": "1.19.
|
|
7
|
-
"source_commit": "
|
|
5
|
+
"core_version": "1.19.131",
|
|
6
|
+
"core_contract": "1.19.131",
|
|
7
|
+
"source_commit": "7fc91a6722c91cd40c09ce632c2a765377952e49",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-08-15T03:
|
|
9
|
+
"built_at": "2026-08-15T03:23:58.787Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 303,
|
|
13
13
|
"agent_docs_stubbed": 21,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 1247,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 1571,
|
|
20
|
+
"tree_sha256": "81215c25cbaed99553dba15247df2e2f9daf37245d8144df0a5e32b507b4c0b9",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/blocker-review/SKILL.md",
|
|
@@ -4172,12 +4172,12 @@
|
|
|
4172
4172
|
{
|
|
4173
4173
|
"path": "package-lock.json",
|
|
4174
4174
|
"mode": "0000644",
|
|
4175
|
-
"sha256": "
|
|
4175
|
+
"sha256": "ad651e607ce3937ca8522552d21ecfbaa03108734f9c7987c79a2e8ef8a7c7f3"
|
|
4176
4176
|
},
|
|
4177
4177
|
{
|
|
4178
4178
|
"path": "package.json",
|
|
4179
4179
|
"mode": "0000644",
|
|
4180
|
-
"sha256": "
|
|
4180
|
+
"sha256": "178ddf3d5958c428d370e3381cdca505455abe6006b9f920245de0062bc2d5aa"
|
|
4181
4181
|
},
|
|
4182
4182
|
{
|
|
4183
4183
|
"path": "public-docs/index.html",
|
|
@@ -5157,7 +5157,7 @@
|
|
|
5157
5157
|
{
|
|
5158
5158
|
"path": "scripts/gds/run-unit-tests.js",
|
|
5159
5159
|
"mode": "0000644",
|
|
5160
|
-
"sha256": "
|
|
5160
|
+
"sha256": "4c3bef6907b4807d93426e754ff20c75e56b498e4302c1cd47e4b3953bd8b4d0"
|
|
5161
5161
|
},
|
|
5162
5162
|
{
|
|
5163
5163
|
"path": "scripts/gds/sandbox-stage.js",
|
|
@@ -5322,7 +5322,7 @@
|
|
|
5322
5322
|
{
|
|
5323
5323
|
"path": "scripts/gds/ship.js",
|
|
5324
5324
|
"mode": "0000644",
|
|
5325
|
-
"sha256": "
|
|
5325
|
+
"sha256": "681de343f8491a2f22165246f21ac371a8f76411b395991c784635e8e9646ed7"
|
|
5326
5326
|
},
|
|
5327
5327
|
{
|
|
5328
5328
|
"path": "scripts/gds/smoke-batch-claim.sh",
|
|
@@ -5762,7 +5762,7 @@
|
|
|
5762
5762
|
{
|
|
5763
5763
|
"path": "src/module-api.js",
|
|
5764
5764
|
"mode": "0000644",
|
|
5765
|
-
"sha256": "
|
|
5765
|
+
"sha256": "fd76c822f4e795248c2c010052dab9cce8cc237a5f501fbf90e66c413c58eac6"
|
|
5766
5766
|
},
|
|
5767
5767
|
{
|
|
5768
5768
|
"path": "src/module-loader/catalog.js",
|
|
@@ -7654,6 +7654,11 @@
|
|
|
7654
7654
|
"mode": "0000644",
|
|
7655
7655
|
"sha256": "23731936e26feb6406333f484c86210be2f2eaee1b4060ba34c9e03f8fd09685"
|
|
7656
7656
|
},
|
|
7657
|
+
{
|
|
7658
|
+
"path": "tests/ship_publish_error.mjs",
|
|
7659
|
+
"mode": "0000644",
|
|
7660
|
+
"sha256": "eb8c52362355479a50c83ba84968d605b93f98e93513cf8436dd096e6514f6e8"
|
|
7661
|
+
},
|
|
7657
7662
|
{
|
|
7658
7663
|
"path": "tests/ship_resume.mjs",
|
|
7659
7664
|
"mode": "0000644",
|
|
@@ -7767,7 +7772,7 @@
|
|
|
7767
7772
|
{
|
|
7768
7773
|
"path": "tests/subdomain_auth.mjs",
|
|
7769
7774
|
"mode": "0000644",
|
|
7770
|
-
"sha256": "
|
|
7775
|
+
"sha256": "24257ca4ad1a7bcefb735ee1b02609469035730e0723645f71e10eb71525bb72"
|
|
7771
7776
|
},
|
|
7772
7777
|
{
|
|
7773
7778
|
"path": "tests/task_classifier_vocab.mjs",
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.131",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.19.
|
|
9
|
+
"version": "1.19.131",
|
|
10
10
|
"license": "AGPL-3.0-or-later",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"express": "^4.21.2",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.131",
|
|
4
4
|
"description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"main": "src/platform-server.js",
|
|
@@ -77,7 +77,7 @@ const INTEGRATION = new Set([
|
|
|
77
77
|
'governance_fold_bench', 'governance_live_proof', 'linkify_refs', 'memory_isolation',
|
|
78
78
|
'memory_pull', 'needs_rebase', 'parallel-load', 'reward_mode_cost_plus_only',
|
|
79
79
|
'session_records', 'setup_smoke',
|
|
80
|
-
'status-probe',
|
|
80
|
+
'status-probe',
|
|
81
81
|
]);
|
|
82
82
|
|
|
83
83
|
// Excluded so this gate is trustworthy in a CLEAN checkout (the CI condition):
|
package/scripts/gds/ship.js
CHANGED
|
@@ -1213,6 +1213,34 @@ async function ciLand(taskId, branch, valueSummary) {
|
|
|
1213
1213
|
return await ciLandLocal(taskId, branch, valueSummary);
|
|
1214
1214
|
}
|
|
1215
1215
|
|
|
1216
|
+
// task 1001916: render a FAILED POST /tasks/:id/publish-branch for the builder.
|
|
1217
|
+
// The server has carried { step, reason } — which git step failed + the
|
|
1218
|
+
// credential-scrubbed stderr — since task 1547, but the bail site printed only
|
|
1219
|
+
// `data.message || data.error`: "[object Object]" on the ADR 0116 envelope, a
|
|
1220
|
+
// bare `publish_failed` on the legacy flat shape. That hid a non-fast-forward
|
|
1221
|
+
// rejection (the server had already freshened the branch itself) across several
|
|
1222
|
+
// blind retry laps, so that case also gets its specific remedy. Pure + exported
|
|
1223
|
+
// for tests/ship_publish_error.mjs (no git, no network).
|
|
1224
|
+
const NON_FAST_FORWARD_RE = /fetch first|non-fast-forward/i;
|
|
1225
|
+
|
|
1226
|
+
function formatPublishFailure(data, status, branch) {
|
|
1227
|
+
const { code, message } = normalizeApiError(data);
|
|
1228
|
+
const envelopeDetails = (data && data.error && typeof data.error === 'object' && data.error.details) || null;
|
|
1229
|
+
const details = envelopeDetails || data || {};
|
|
1230
|
+
const label = message || code || (data && data.message) || `HTTP ${status}`;
|
|
1231
|
+
// git stderr is multi-line; this is embedded mid-sentence in one log line.
|
|
1232
|
+
const reason = details.reason ? String(details.reason).replace(/\s+/g, ' ').trim() : '';
|
|
1233
|
+
const parts = [];
|
|
1234
|
+
if (details.step) parts.push(`step: ${details.step}`);
|
|
1235
|
+
if (reason) parts.push(`reason: ${reason}`);
|
|
1236
|
+
let out = parts.length ? `${label} (${parts.join('; ')})` : label;
|
|
1237
|
+
if (NON_FAST_FORWARD_RE.test(reason)) {
|
|
1238
|
+
out += ` — the server already moved ${branch}: fetch origin, reset onto origin/${branch}, `
|
|
1239
|
+
+ 're-run the generators, and re-ship so the push fast-forwards.';
|
|
1240
|
+
}
|
|
1241
|
+
return out;
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1216
1244
|
// ciLandServer — the dev-box path: no GitHub push credential on this machine, so
|
|
1217
1245
|
// hand the branch to the GDS, which pushes it + opens the PR + auto-merges with
|
|
1218
1246
|
// the server-side credential. Requires a RE-AUTHED (non-box) cli session — a
|
|
@@ -1249,7 +1277,7 @@ async function ciLandServer(taskId, branch, valueSummary) {
|
|
|
1249
1277
|
return landBailed('server-mediated push is not configured (operator action)', strandCommand(taskId, 'server'));
|
|
1250
1278
|
}
|
|
1251
1279
|
if (!r.ok) {
|
|
1252
|
-
const e = (r.data
|
|
1280
|
+
const e = formatPublishFailure(r.data, r.status, branch);
|
|
1253
1281
|
console.log(` ci-land: server publish failed — ${e}. Task stays at confirmed. ${strandNextStep(taskId, 'server')}`);
|
|
1254
1282
|
return landBailed(`server publish failed — ${e}`, strandCommand(taskId, 'server'));
|
|
1255
1283
|
}
|
|
@@ -4472,6 +4500,10 @@ module.exports = {
|
|
|
4472
4500
|
// object, so a consumer MUST test `.ok`; the old `if (!merged)` would sail past
|
|
4473
4501
|
// a bail. Also pins pending (a poll timeout) as distinct from bailed.
|
|
4474
4502
|
landed, landPending, landBailed, strandCommand,
|
|
4503
|
+
// task 1001916: the publish-branch failure formatter — exported pure so both
|
|
4504
|
+
// wire shapes (envelope + legacy flat), the step/reason surfacing, and the
|
|
4505
|
+
// non-fast-forward remedy hint are pinned without a server.
|
|
4506
|
+
formatPublishFailure,
|
|
4475
4507
|
// task 1719: pure post-land status reconciler — a task that reached 'shipped'
|
|
4476
4508
|
// in-session shows the SHIPPED banner + writes no breadcrumb; otherwise keep the
|
|
4477
4509
|
// pre-merge status (confirmed → next-turn breadcrumb fallback). Exported for tests.
|
package/src/module-api.js
CHANGED
|
@@ -687,8 +687,10 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
687
687
|
// landed since 1.19.128 with no explicit bump. run 31858158416. (task 1002620)
|
|
688
688
|
// 1.19.130 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
689
689
|
// landed since 1.19.129 with no explicit bump. run 31861289412. (task 1002620)
|
|
690
|
+
// 1.19.131 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
691
|
+
// landed since 1.19.130 with no explicit bump. run 31861682626. (task 1002620)
|
|
690
692
|
// ---------------------------------------------------------------------------
|
|
691
|
-
const CORE_VERSION = '1.19.
|
|
693
|
+
const CORE_VERSION = '1.19.131'; // CI auto-patch carrier (ADR 0161); see changelog above
|
|
692
694
|
|
|
693
695
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
694
696
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// tests/ship_publish_error.mjs
|
|
2
|
+
//
|
|
3
|
+
// Unit tests for formatPublishFailure (scripts/gds/ship.js, task 1001916) — the
|
|
4
|
+
// display string ciLandServer prints when POST /tasks/:id/publish-branch fails.
|
|
5
|
+
//
|
|
6
|
+
// Why this exists: the server has carried { step, reason } (the credential-
|
|
7
|
+
// scrubbed git stderr) since task 1547, but ciLandServer printed only
|
|
8
|
+
// `r.data.message || r.data.error`. On the ADR 0116 envelope that renders
|
|
9
|
+
// "[object Object]", and on the legacy flat shape a bare "publish_failed" — so a
|
|
10
|
+
// non-fast-forward rejection (the server had already freshened the branch) was
|
|
11
|
+
// invisible and the builder retried blind for several laps. What we prove here:
|
|
12
|
+
// 1. Both wire shapes resolve to the CODE, never "[object Object]".
|
|
13
|
+
// 2. step + reason appear when the server sent them.
|
|
14
|
+
// 3. A non-fast-forward reason carries the specific remedy hint (reset onto
|
|
15
|
+
// origin/<branch>, re-run the generators, re-ship) — and only then.
|
|
16
|
+
// Pure: no network, no git, no fs.
|
|
17
|
+
//
|
|
18
|
+
// Run: node tests/ship_publish_error.mjs
|
|
19
|
+
|
|
20
|
+
import { strict as assert } from 'node:assert';
|
|
21
|
+
import { createRequire } from 'node:module';
|
|
22
|
+
|
|
23
|
+
const require = createRequire(import.meta.url);
|
|
24
|
+
const ship = require('../scripts/gds/ship.js');
|
|
25
|
+
|
|
26
|
+
let passed = 0;
|
|
27
|
+
let failed = 0;
|
|
28
|
+
function test(name, fn) {
|
|
29
|
+
try {
|
|
30
|
+
fn();
|
|
31
|
+
passed++;
|
|
32
|
+
console.log(` ok ${name}`);
|
|
33
|
+
} catch (err) {
|
|
34
|
+
failed++;
|
|
35
|
+
console.log(` FAIL ${name}`);
|
|
36
|
+
console.log(` ${err.message}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const BRANCH = 'claude/task-1001916';
|
|
41
|
+
const fmt = (data, status = 500, branch = BRANCH) => ship.formatPublishFailure(data, status, branch);
|
|
42
|
+
|
|
43
|
+
// ---------- 1. bare error, no step/reason ----------
|
|
44
|
+
|
|
45
|
+
test('legacy flat { error: code } with no details reads as the bare code', () => {
|
|
46
|
+
const out = fmt({ error: 'publish_failed' });
|
|
47
|
+
assert.match(out, /publish_failed/);
|
|
48
|
+
assert.ok(!/step:/.test(out), 'no step was sent, so none is invented');
|
|
49
|
+
assert.ok(!/reason:/.test(out), 'no reason was sent, so none is invented');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('ADR 0116 envelope with no details resolves to the code, never [object Object]', () => {
|
|
53
|
+
const out = fmt({ error: { code: 'publish_failed', message: 'publish_failed' } });
|
|
54
|
+
assert.match(out, /publish_failed/);
|
|
55
|
+
assert.ok(!/\[object Object\]/.test(out), 'the envelope object must never be stringified raw');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('no body at all falls back to the HTTP status', () => {
|
|
59
|
+
assert.match(fmt(null, 502), /HTTP 502/);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// ---------- 2. error + step + reason ----------
|
|
63
|
+
|
|
64
|
+
test('envelope details carry step + reason into the message', () => {
|
|
65
|
+
const out = fmt({
|
|
66
|
+
error: {
|
|
67
|
+
code: 'publish_failed',
|
|
68
|
+
message: 'publish_failed',
|
|
69
|
+
details: { step: 'push', reason: 'remote: Permission to org/repo.git denied' },
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
assert.match(out, /publish_failed/);
|
|
73
|
+
assert.match(out, /step: push/);
|
|
74
|
+
assert.match(out, /Permission to org\/repo\.git denied/);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('legacy flat top-level step + reason are surfaced the same way', () => {
|
|
78
|
+
const out = fmt({ error: 'publish_failed', step: 'fetch', reason: 'could not read Username' });
|
|
79
|
+
assert.match(out, /step: fetch/);
|
|
80
|
+
assert.match(out, /could not read Username/);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('a non-git failure reason gets NO fast-forward remedy hint', () => {
|
|
84
|
+
const out = fmt({ error: 'publish_failed', step: 'push', reason: 'Permission denied (publickey)' });
|
|
85
|
+
assert.ok(!/fast-forward/i.test(out), 'the remedy hint is for a stale-branch push only');
|
|
86
|
+
assert.ok(!/reset/i.test(out), 'nor the reset instruction');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// ---------- 3. the non-fast-forward remedy hint ----------
|
|
90
|
+
|
|
91
|
+
const NON_FF_STDERR =
|
|
92
|
+
'! [rejected] claude/task-1001916 -> claude/task-1001916 (fetch first)\n' +
|
|
93
|
+
"error: failed to push some refs to 'github.com/org/repo.git'";
|
|
94
|
+
|
|
95
|
+
test('a "fetch first" rejection names the branch and the reset-onto-origin remedy', () => {
|
|
96
|
+
const out = fmt({ error: 'publish_failed', step: 'push', reason: NON_FF_STDERR });
|
|
97
|
+
assert.match(out, /step: push/);
|
|
98
|
+
assert.match(out, new RegExp(`origin/${BRANCH}`), 'points at the ref the server already moved');
|
|
99
|
+
assert.match(out, /reset/i, 'names the reset');
|
|
100
|
+
assert.match(out, /generator/i, 'and the regenerate step before re-shipping');
|
|
101
|
+
assert.match(out, /re-ship|fast-forward/i, 'and why: so the push fast-forwards');
|
|
102
|
+
assert.ok(!/\n/.test(out), 'multi-line git stderr is collapsed — this goes mid-sentence into one log line');
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test('the "non-fast-forward" wording triggers the same hint through the envelope', () => {
|
|
106
|
+
const out = fmt({
|
|
107
|
+
error: {
|
|
108
|
+
code: 'publish_failed',
|
|
109
|
+
message: 'publish_failed',
|
|
110
|
+
details: { step: 'push', reason: 'Updates were rejected: non-fast-forward' },
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
assert.match(out, new RegExp(`origin/${BRANCH}`));
|
|
114
|
+
assert.match(out, /reset/i);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
console.log('');
|
|
118
|
+
console.log(`ship_publish_error: ${passed} passed, ${failed} failed`);
|
|
119
|
+
process.exit(failed === 0 ? 0 : 1);
|
package/tests/subdomain_auth.mjs
CHANGED
|
@@ -1,14 +1,46 @@
|
|
|
1
1
|
// tests/subdomain_auth.mjs — the builders-subdomain auth helpers (#726).
|
|
2
2
|
//
|
|
3
3
|
// Locks down the two security-sensitive pure helpers added for the dedicated
|
|
4
|
-
// builders
|
|
4
|
+
// builders.<apex> subdomain:
|
|
5
5
|
// - safeReturnTo: the open-redirect guard on the post-sign-in return target.
|
|
6
6
|
// - sessionCookieDomainOpts: which hosts get an apex-scoped session cookie
|
|
7
7
|
// (so one sign-in spans the subdomains) vs a host-only one (localhost,
|
|
8
8
|
// staging — which must NOT bleed a token across environments).
|
|
9
9
|
// Pure functions, no DB — requiring the route module does not open a pool.
|
|
10
|
+
//
|
|
11
|
+
// Both helpers derive their allowed hosts from the instance branding pack's
|
|
12
|
+
// domains.cookieDomain, so the fixtures inject a SYNTHETIC pack (the
|
|
13
|
+
// GDS_BRANDING_FILE seam, as tests/single_host_hall.mjs does) instead of
|
|
14
|
+
// reading whatever config/branding.json happens to be on disk. That keeps the
|
|
15
|
+
// assertions identical on every instance — the neutral core (cookieDomain "")
|
|
16
|
+
// and a branded host alike. envPrefix is overridden too so an ambient
|
|
17
|
+
// <PREFIX>_COOKIE_DOMAIN in the shell cannot reach the resolver.
|
|
18
|
+
//
|
|
19
|
+
// Run: node tests/subdomain_auth.mjs
|
|
10
20
|
|
|
11
21
|
import { createRequire } from 'node:module';
|
|
22
|
+
import { writeFileSync, mkdtempSync, rmSync } from 'node:fs';
|
|
23
|
+
import { tmpdir } from 'node:os';
|
|
24
|
+
import { join } from 'node:path';
|
|
25
|
+
|
|
26
|
+
// The apex every fixture below is built from. RFC 2606 reserved TLD — clearly
|
|
27
|
+
// synthetic, never routable, and no host brand is baked into this file.
|
|
28
|
+
const APEX = 'bongos-test.example';
|
|
29
|
+
|
|
30
|
+
const brandDir = mkdtempSync(join(tmpdir(), 'cb-subdomain-auth-'));
|
|
31
|
+
const brandFile = join(brandDir, 'branding.json');
|
|
32
|
+
writeFileSync(brandFile, JSON.stringify({
|
|
33
|
+
envPrefix: 'SUBDOMAIN_AUTH_FIXTURE',
|
|
34
|
+
// Leading dot mirrors the real packs; configuredCookieApex() strips it.
|
|
35
|
+
domains: { cookieDomain: `.${APEX}`, publicOrigin: `https://${APEX}` },
|
|
36
|
+
}));
|
|
37
|
+
process.env.GDS_BRANDING_FILE = brandFile;
|
|
38
|
+
// The pack is only read once (auth.js resolves COOKIE_APEX at module load);
|
|
39
|
+
// remove the temp dir on every exit path so runs don't accumulate in tmpdir.
|
|
40
|
+
process.on('exit', () => { try { rmSync(brandDir, { recursive: true, force: true }); } catch { /* best-effort */ } });
|
|
41
|
+
|
|
42
|
+
// Required AFTER the pack is in place — auth.js resolves COOKIE_APEX at module
|
|
43
|
+
// load, and branding() memoizes on its first call.
|
|
12
44
|
const require = createRequire(import.meta.url);
|
|
13
45
|
const authRoute = require('../src/bongos/routes/auth.js');
|
|
14
46
|
|
|
@@ -24,15 +56,15 @@ console.log('\nsafeReturnTo — accepts safe same-city targets:');
|
|
|
24
56
|
ok(safeReturnTo('/builders') === '/builders', 'relative /builders');
|
|
25
57
|
ok(safeReturnTo('/watch') === '/watch', 'relative /watch');
|
|
26
58
|
ok(safeReturnTo('/builders/settings?x=1') === '/builders/settings?x=1', 'relative path with query');
|
|
27
|
-
ok(safeReturnTo(
|
|
28
|
-
ok(safeReturnTo(
|
|
29
|
-
ok(safeReturnTo(
|
|
59
|
+
ok(safeReturnTo(`https://builders.${APEX}/watch`) === `https://builders.${APEX}/watch`, 'absolute builders subdomain');
|
|
60
|
+
ok(safeReturnTo(`https://${APEX}/builders`) === `https://${APEX}/builders`, 'absolute apex');
|
|
61
|
+
ok(safeReturnTo(`https://status.${APEX}/`) === `https://status.${APEX}/`, 'absolute status subdomain');
|
|
30
62
|
|
|
31
63
|
console.log('\nsafeReturnTo — rejects everything else (→ null, caller falls back):');
|
|
32
64
|
ok(safeReturnTo('//evil.com') === null, 'protocol-relative //evil.com');
|
|
33
65
|
ok(safeReturnTo('https://evil.com/builders') === null, 'foreign https origin');
|
|
34
|
-
ok(safeReturnTo(
|
|
35
|
-
ok(safeReturnTo(
|
|
66
|
+
ok(safeReturnTo(`https://${APEX}.evil.com/`) === null, 'lookalike suffix host');
|
|
67
|
+
ok(safeReturnTo(`http://${APEX}/builders`) === null, 'http (non-https) apex');
|
|
36
68
|
ok(safeReturnTo('javascript:alert(1)') === null, 'javascript: scheme');
|
|
37
69
|
ok(safeReturnTo('/path\\with\\backslash') === null, 'backslash path');
|
|
38
70
|
ok(safeReturnTo('') === null, 'empty');
|
|
@@ -43,15 +75,15 @@ ok(safeReturnTo('builders') === null, 'bare word (no leading slash, not a URL)')
|
|
|
43
75
|
// ---- sessionCookieDomainOpts: cross-subdomain scoping ----------------------
|
|
44
76
|
const reqWith = (host) => ({ headers: { host } });
|
|
45
77
|
console.log('\nsessionCookieDomainOpts — apex-scoped on production hosts:');
|
|
46
|
-
ok(sessionCookieDomainOpts(reqWith(
|
|
47
|
-
ok(sessionCookieDomainOpts(reqWith(
|
|
48
|
-
ok(sessionCookieDomainOpts(reqWith(
|
|
49
|
-
ok(sessionCookieDomainOpts(reqWith(
|
|
50
|
-
ok(sessionCookieDomainOpts(reqWith(
|
|
78
|
+
ok(sessionCookieDomainOpts(reqWith(APEX)).domain === `.${APEX}`, `apex → .${APEX}`);
|
|
79
|
+
ok(sessionCookieDomainOpts(reqWith(`www.${APEX}`)).domain === `.${APEX}`, `www → .${APEX}`);
|
|
80
|
+
ok(sessionCookieDomainOpts(reqWith(`builders.${APEX}`)).domain === `.${APEX}`, `builders → .${APEX}`);
|
|
81
|
+
ok(sessionCookieDomainOpts(reqWith(`status.${APEX}`)).domain === `.${APEX}`, `status → .${APEX}`);
|
|
82
|
+
ok(sessionCookieDomainOpts(reqWith(`${APEX}:3000`)).domain === `.${APEX}`, 'apex with port');
|
|
51
83
|
|
|
52
84
|
console.log('\nsessionCookieDomainOpts — host-only elsewhere (no domain key):');
|
|
53
85
|
ok(sessionCookieDomainOpts(reqWith('localhost:3000')).domain === undefined, 'localhost → host-only');
|
|
54
|
-
ok(sessionCookieDomainOpts(reqWith(
|
|
86
|
+
ok(sessionCookieDomainOpts(reqWith(`staging.${APEX}`)).domain === undefined, 'staging → host-only (no cross-env bleed)');
|
|
55
87
|
ok(sessionCookieDomainOpts(reqWith('127.0.0.1')).domain === undefined, 'raw IP → host-only');
|
|
56
88
|
|
|
57
89
|
console.log(`\nSubdomain-auth tests: ${pass} passed, ${fail} failed`);
|