@bongos/core 1.19.351 → 1.19.352
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 +10 -10
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/src/bongos/stealth-gate.js +30 -5
- package/src/module-api.js +3 -1
- package/tests/stealth_gate.mjs +65 -0
package/.bongos-core.json
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
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.352",
|
|
6
|
+
"core_contract": "1.19.352",
|
|
7
|
+
"source_commit": "e9a9e6873568180427946951f116e153cdef056f",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-08-
|
|
9
|
+
"built_at": "2026-08-25T13:00:42.177Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 329,
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
19
|
"file_count": 1857,
|
|
20
|
-
"tree_sha256": "
|
|
20
|
+
"tree_sha256": "0a7c5bcf1b690464bce9877959778b5f611f54fedb8a6c7b9c51459d47ad7a04",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/blocker-review/SKILL.md",
|
|
@@ -4787,12 +4787,12 @@
|
|
|
4787
4787
|
{
|
|
4788
4788
|
"path": "package-lock.json",
|
|
4789
4789
|
"mode": "0000644",
|
|
4790
|
-
"sha256": "
|
|
4790
|
+
"sha256": "a82b85a09d4f6e7259d1f11a8468e50b82a94a346f7cada1a1c63e09217d4a81"
|
|
4791
4791
|
},
|
|
4792
4792
|
{
|
|
4793
4793
|
"path": "package.json",
|
|
4794
4794
|
"mode": "0000644",
|
|
4795
|
-
"sha256": "
|
|
4795
|
+
"sha256": "f371019e624fd7befd3ef764ae4cac6d7f98dc8ba3058a2ed552f361e48c5568"
|
|
4796
4796
|
},
|
|
4797
4797
|
{
|
|
4798
4798
|
"path": "public-docs/index.html",
|
|
@@ -6622,7 +6622,7 @@
|
|
|
6622
6622
|
{
|
|
6623
6623
|
"path": "src/bongos/stealth-gate.js",
|
|
6624
6624
|
"mode": "0000644",
|
|
6625
|
-
"sha256": "
|
|
6625
|
+
"sha256": "a1b771c103e332b93978e80f16760f80f7ab0260157db31c47d4655d69977344"
|
|
6626
6626
|
},
|
|
6627
6627
|
{
|
|
6628
6628
|
"path": "src/bongos/uptime-poller.js",
|
|
@@ -6652,7 +6652,7 @@
|
|
|
6652
6652
|
{
|
|
6653
6653
|
"path": "src/module-api.js",
|
|
6654
6654
|
"mode": "0000644",
|
|
6655
|
-
"sha256": "
|
|
6655
|
+
"sha256": "7433ae86603ef217160393fd3b36bb847092f0c040473a5bba43a21c14444be1"
|
|
6656
6656
|
},
|
|
6657
6657
|
{
|
|
6658
6658
|
"path": "src/module-loader/catalog.js",
|
|
@@ -9112,7 +9112,7 @@
|
|
|
9112
9112
|
{
|
|
9113
9113
|
"path": "tests/stealth_gate.mjs",
|
|
9114
9114
|
"mode": "0000644",
|
|
9115
|
-
"sha256": "
|
|
9115
|
+
"sha256": "bb264253e2a9c233734cf9273928db203f8f441c4e7a53f5f9ae5ea0477e20e3"
|
|
9116
9116
|
},
|
|
9117
9117
|
{
|
|
9118
9118
|
"path": "tests/stop_refcheck.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.352",
|
|
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.352",
|
|
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.352",
|
|
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",
|
|
@@ -107,11 +107,30 @@ function isExempt(path) {
|
|
|
107
107
|
return EXEMPT.some((re) => re.test(path));
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
+
// The path the visitor knocked on, carried through the form so the password
|
|
111
|
+
// lands them where they were going instead of on '/' (task 1003241). RELATIVE
|
|
112
|
+
// PATHS ONLY: one leading slash, no '//' (protocol-relative), no backslash
|
|
113
|
+
// (browsers read '/\evil' as '//evil'), no control bytes, not the gate's own
|
|
114
|
+
// path — anything else is null and the redirect falls back to '/'. The same
|
|
115
|
+
// shape as the relative branch of auth.js's safeReturnTo, kept local because
|
|
116
|
+
// the gate mounts ahead of every router and loads nothing it does not need.
|
|
117
|
+
function relativeReturn(raw) {
|
|
118
|
+
if (typeof raw !== 'string' || !raw.length || raw.length > 512) return null;
|
|
119
|
+
if (!raw.startsWith('/') || raw.startsWith('//') || raw.includes('\\')) return null;
|
|
120
|
+
if (/[\x00-\x1f\x7f]/.test(raw)) return null;
|
|
121
|
+
if (raw === '/' || raw.startsWith(SUBMIT_PATH)) return null;
|
|
122
|
+
return raw;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function escAttr(s) {
|
|
126
|
+
return String(s).replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>');
|
|
127
|
+
}
|
|
128
|
+
|
|
110
129
|
// The form. Self-contained and token-free on purpose: it is served BEFORE the
|
|
111
130
|
// visitor is allowed to see anything, so it must not pull the branding bundle,
|
|
112
131
|
// the stylesheet, or any other asset that would leak the design of an
|
|
113
132
|
// unannounced product to whoever knocks.
|
|
114
|
-
function formHtml({ productName, wrong }) {
|
|
133
|
+
function formHtml({ productName, wrong, returnTo }) {
|
|
115
134
|
const name = String(productName || 'This instance').replace(/[<>&"]/g, '');
|
|
116
135
|
return `<!doctype html><html lang="en"><head><meta charset="utf-8">
|
|
117
136
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
@@ -144,6 +163,7 @@ button{padding:11px 20px;border:0;border-radius:999px;background:#ce4806;color:#
|
|
|
144
163
|
<h1>${name}</h1>
|
|
145
164
|
<p>Not open to the world yet. Enter the password to have a look.</p>
|
|
146
165
|
<form method="POST" action="${SUBMIT_PATH}">
|
|
166
|
+
${returnTo ? `<input type="hidden" name="return" value="${escAttr(returnTo)}">` : ''}
|
|
147
167
|
<input type="password" name="password" placeholder="Password" aria-label="Password" autofocus required>
|
|
148
168
|
<button type="submit">Enter</button>
|
|
149
169
|
</form>
|
|
@@ -175,8 +195,10 @@ function stealthGate() {
|
|
|
175
195
|
const want = password();
|
|
176
196
|
const ok = given.length === want.length
|
|
177
197
|
&& crypto.timingSafeEqual(Buffer.from(given), Buffer.from(want));
|
|
198
|
+
// re-validated on the way back: the field is visitor-supplied
|
|
199
|
+
const returnTo = relativeReturn(params.get('return'));
|
|
178
200
|
if (!ok) {
|
|
179
|
-
return res.status(401).type('html').send(formHtml({ productName: productName(), wrong: true }));
|
|
201
|
+
return res.status(401).type('html').send(formHtml({ productName: productName(), wrong: true, returnTo }));
|
|
180
202
|
}
|
|
181
203
|
res.cookie(COOKIE, expectedToken(), {
|
|
182
204
|
httpOnly: true,
|
|
@@ -186,7 +208,7 @@ function stealthGate() {
|
|
|
186
208
|
...domainOpts(req),
|
|
187
209
|
// no maxAge/expires — a SESSION cookie, by the owner's choice
|
|
188
210
|
});
|
|
189
|
-
res.redirect(303, '/');
|
|
211
|
+
res.redirect(303, returnTo || '/');
|
|
190
212
|
});
|
|
191
213
|
return undefined;
|
|
192
214
|
}
|
|
@@ -195,8 +217,11 @@ function stealthGate() {
|
|
|
195
217
|
if (tokenMatches(readCookie(req, COOKIE))) return next();
|
|
196
218
|
|
|
197
219
|
// 401 rather than 200: this is an unauthenticated request being refused, and
|
|
198
|
-
// a crawler that ignores the noindex should still be told so.
|
|
199
|
-
|
|
220
|
+
// a crawler that ignores the noindex should still be told so. A GET's own
|
|
221
|
+
// path rides the form so the password lands the visitor back on it; any
|
|
222
|
+
// other method has nothing safe to replay as a GET.
|
|
223
|
+
const returnTo = (req.method === 'GET' || req.method === 'HEAD') ? relativeReturn(req.originalUrl) : null;
|
|
224
|
+
return res.status(401).type('html').send(formHtml({ productName: productName(), wrong: false, returnTo }));
|
|
200
225
|
};
|
|
201
226
|
}
|
|
202
227
|
|
package/src/module-api.js
CHANGED
|
@@ -1132,8 +1132,10 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
1132
1132
|
// landed since 1.19.349 with no explicit bump. run 32810944115. (task 1002620)
|
|
1133
1133
|
// 1.19.351 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1134
1134
|
// landed since 1.19.350 with no explicit bump. run 32850120472. (task 1002620)
|
|
1135
|
+
// 1.19.352 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1136
|
+
// landed since 1.19.351 with no explicit bump. run 32850767673. (task 1002620)
|
|
1135
1137
|
// ---------------------------------------------------------------------------
|
|
1136
|
-
const CORE_VERSION = '1.19.
|
|
1138
|
+
const CORE_VERSION = '1.19.352'; // CI auto-patch carrier (ADR 0161); see changelog above
|
|
1137
1139
|
|
|
1138
1140
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
1139
1141
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
package/tests/stealth_gate.mjs
CHANGED
|
@@ -135,6 +135,71 @@ test('a wrong password is refused and says so', async () => {
|
|
|
135
135
|
assert.ok(!String(res.headers['set-cookie'] || '').includes(gate.COOKIE), 'no cookie on failure');
|
|
136
136
|
});
|
|
137
137
|
|
|
138
|
+
/* ── the deep link survives the password (task 1003241) ── */
|
|
139
|
+
const unlock = (fields) => request(gate.SUBMIT_PATH, {
|
|
140
|
+
method: 'POST',
|
|
141
|
+
body: new URLSearchParams({ password: PASSWORD, ...fields }).toString(),
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test('the form carries the path the visitor knocked on, and the password lands them there', async () => {
|
|
145
|
+
// the pre-UAT walk found a stranger who typed /create landing on the front
|
|
146
|
+
// door after the password — the deep link rides the form, attribute-escaped
|
|
147
|
+
const knock = await request('/projects?view=new&repos=1');
|
|
148
|
+
assert.equal(knock.status, 401);
|
|
149
|
+
assert.match(knock.body, /<input type="hidden" name="return" value="\/projects\?view=new&repos=1">/);
|
|
150
|
+
const res = await unlock({ return: '/projects?view=new&repos=1' });
|
|
151
|
+
assert.equal(res.status, 303);
|
|
152
|
+
assert.equal(res.headers.location, '/projects?view=new&repos=1');
|
|
153
|
+
assert.match(String(res.headers['set-cookie'] || ''), new RegExp(gate.COOKIE), 'the cookie still lands');
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test('the front door carries no return field, and a bare password still lands on /', async () => {
|
|
157
|
+
const knock = await request('/');
|
|
158
|
+
assert.doesNotMatch(knock.body, /name="return"/);
|
|
159
|
+
const res = await unlock({});
|
|
160
|
+
assert.equal(res.status, 303);
|
|
161
|
+
assert.equal(res.headers.location, '/');
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test('a wrong password keeps the deep link for the next try', async () => {
|
|
165
|
+
const res = await request(gate.SUBMIT_PATH, {
|
|
166
|
+
method: 'POST',
|
|
167
|
+
body: new URLSearchParams({ password: 'not-it', return: '/create' }).toString(),
|
|
168
|
+
});
|
|
169
|
+
assert.equal(res.status, 401);
|
|
170
|
+
assert.match(res.body, /name="return" value="\/create"/);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test('the return target is relative-path-only — never an absolute or off-site redirect', async () => {
|
|
174
|
+
const bad = [
|
|
175
|
+
'https://evil.example/', // absolute, even https
|
|
176
|
+
'//evil.example', // protocol-relative
|
|
177
|
+
'/\\evil.example', // browsers read the backslash as a slash
|
|
178
|
+
'javascript:alert(1)',
|
|
179
|
+
'/ok\r\nSet-Cookie: x=1', // header injection
|
|
180
|
+
'/' + 'a'.repeat(600), // oversize
|
|
181
|
+
gate.SUBMIT_PATH + '?again', // the gate's own path would loop
|
|
182
|
+
];
|
|
183
|
+
for (const r of bad) {
|
|
184
|
+
const res = await unlock({ return: r });
|
|
185
|
+
assert.equal(res.status, 303, `${JSON.stringify(r)} still opens the gate`);
|
|
186
|
+
assert.equal(res.headers.location, '/', `${JSON.stringify(r)} must fall back to /`);
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test('a knock that is not a GET is not carried back', async () => {
|
|
191
|
+
const res = await request('/projects', { method: 'POST', body: 'x=1' });
|
|
192
|
+
assert.equal(res.status, 401);
|
|
193
|
+
assert.doesNotMatch(res.body, /name="return"/);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test('a hostile path is escaped in the form, never echoed raw', async () => {
|
|
197
|
+
const res = await request('/x?q="><script>alert(1)</script>');
|
|
198
|
+
assert.equal(res.status, 401);
|
|
199
|
+
assert.doesNotMatch(res.body, /<script/);
|
|
200
|
+
assert.match(res.body, /value="\/x\?q="><script>/);
|
|
201
|
+
});
|
|
202
|
+
|
|
138
203
|
test('a forged or stale cookie does not open it', async () => {
|
|
139
204
|
for (const bad of ['x', 'deadbeef', gate.expectedToken().slice(0, -1) + '0']) {
|
|
140
205
|
const res = await request('/', { cookie: `${gate.COOKIE}=${bad}` });
|