@bongos/core 1.19.351 → 1.19.353
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 +14 -14
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/provision-repo.js +14 -7
- package/scripts/gds/provision-units.js +6 -6
- package/scripts/gds/provision.js +4 -4
- package/src/bongos/stealth-gate.js +30 -5
- package/src/module-api.js +5 -1
- package/tests/provision.mjs +26 -0
- 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.353",
|
|
6
|
+
"core_contract": "1.19.353",
|
|
7
|
+
"source_commit": "5bb1f8185d48f3d3d53181f4525eca45b0cef665",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-08-
|
|
9
|
+
"built_at": "2026-08-25T13:07:06.857Z",
|
|
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": "3b39dee80eea8539fc0a7b82684369a69c58b09c426cac168f029815bc78270e",
|
|
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": "0b1bb5323c63aefbe9f58e5bd5eba8cfed791c5089bf5dbae18fde4055c31299"
|
|
4791
4791
|
},
|
|
4792
4792
|
{
|
|
4793
4793
|
"path": "package.json",
|
|
4794
4794
|
"mode": "0000644",
|
|
4795
|
-
"sha256": "
|
|
4795
|
+
"sha256": "c524de81eaba481839a9515c321c48d231506f677459d76c495bb51ebfd8b90c"
|
|
4796
4796
|
},
|
|
4797
4797
|
{
|
|
4798
4798
|
"path": "public-docs/index.html",
|
|
@@ -5797,17 +5797,17 @@
|
|
|
5797
5797
|
{
|
|
5798
5798
|
"path": "scripts/gds/provision-repo.js",
|
|
5799
5799
|
"mode": "0000644",
|
|
5800
|
-
"sha256": "
|
|
5800
|
+
"sha256": "c87cccdd63ac6fa6d38771c3ad178e5e919f8bee62ad769b9d127819f84e2750"
|
|
5801
5801
|
},
|
|
5802
5802
|
{
|
|
5803
5803
|
"path": "scripts/gds/provision-units.js",
|
|
5804
5804
|
"mode": "0000644",
|
|
5805
|
-
"sha256": "
|
|
5805
|
+
"sha256": "e8a44143883403c2ff8e20107250d63f6d19a14050279e8d675480582e07c555"
|
|
5806
5806
|
},
|
|
5807
5807
|
{
|
|
5808
5808
|
"path": "scripts/gds/provision.js",
|
|
5809
5809
|
"mode": "0000644",
|
|
5810
|
-
"sha256": "
|
|
5810
|
+
"sha256": "5b4f5edce415dd46db830104eb196de678214730d736ebf3ae26a7671b718c77"
|
|
5811
5811
|
},
|
|
5812
5812
|
{
|
|
5813
5813
|
"path": "scripts/gds/publish-credential-check.js",
|
|
@@ -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": "82bd181f16c9ee484e55c879439a7f61ff056588080a57bd95a3881d89b726f3"
|
|
6656
6656
|
},
|
|
6657
6657
|
{
|
|
6658
6658
|
"path": "src/module-loader/catalog.js",
|
|
@@ -8557,7 +8557,7 @@
|
|
|
8557
8557
|
{
|
|
8558
8558
|
"path": "tests/provision.mjs",
|
|
8559
8559
|
"mode": "0000644",
|
|
8560
|
-
"sha256": "
|
|
8560
|
+
"sha256": "0fc6c6626b43458c2e065322bec5a2ecd87e50a44abd2838d81ff0f0b6abeae7"
|
|
8561
8561
|
},
|
|
8562
8562
|
{
|
|
8563
8563
|
"path": "tests/provisioning_default_domain.mjs",
|
|
@@ -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.353",
|
|
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.353",
|
|
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.353",
|
|
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",
|
|
@@ -323,6 +323,18 @@ function alreadyScaffolded(root, mode, { fsImpl = fs } = {}) {
|
|
|
323
323
|
//
|
|
324
324
|
// Idempotent (see alreadyScaffolded) and dry-run safe (every mutation goes through the
|
|
325
325
|
// injected exec/writeFile). ADR 0108 / 0121 / 0125.
|
|
326
|
+
// The signed-in builder who owns the instance — the founding firstAdmin. Best-effort:
|
|
327
|
+
// no db, no row, or a query error → null, and callers fall back to the repo owner in
|
|
328
|
+
// target_ref. Shared by the scaffold's branding.firstAdmin and web.env's
|
|
329
|
+
// <prefix>_FIRST_ADMIN (task 1003240) so the two can never disagree.
|
|
330
|
+
async function ownerLoginOf(inst, db) {
|
|
331
|
+
if (!inst || !inst.owner_builder_id || !db || typeof db.query !== 'function') return null;
|
|
332
|
+
try {
|
|
333
|
+
const r = await db.query('SELECT github_login FROM builders WHERE id = $1', [inst.owner_builder_id]);
|
|
334
|
+
return r && r.rows && r.rows[0] && r.rows[0].github_login ? r.rows[0].github_login : null;
|
|
335
|
+
} catch { return null; }
|
|
336
|
+
}
|
|
337
|
+
|
|
326
338
|
async function scaffoldStandaloneRepo(inst, deps) {
|
|
327
339
|
// The scaffold BUILDS + pushes the instance's repo (git clone, package-core,
|
|
328
340
|
// `bongos init`, git push) — control-plane work that needs the packaging tools +
|
|
@@ -352,13 +364,7 @@ async function scaffoldStandaloneRepo(inst, deps) {
|
|
|
352
364
|
: ` [scaffold] ${repo.owner}/${repo.name} → ${root} (clone → materialize → pin core ${coreV} → commit → push)`);
|
|
353
365
|
|
|
354
366
|
// Owner login = the founding firstAdmin. Best-effort; the spec falls back to the repo owner.
|
|
355
|
-
|
|
356
|
-
if (inst.owner_builder_id && db) {
|
|
357
|
-
try {
|
|
358
|
-
const r = await db.query('SELECT github_login FROM builders WHERE id = $1', [inst.owner_builder_id]);
|
|
359
|
-
ownerLogin = r && r.rows && r.rows[0] ? r.rows[0].github_login : null;
|
|
360
|
-
} catch { /* fall back to the repo owner */ }
|
|
361
|
-
}
|
|
367
|
+
const ownerLogin = await ownerLoginOf(inst, db);
|
|
362
368
|
|
|
363
369
|
// Resolve the instance owner's OAuth token BEFORE any git network op.
|
|
364
370
|
//
|
|
@@ -820,6 +826,7 @@ module.exports = {
|
|
|
820
826
|
CORE_PKG_DIR,
|
|
821
827
|
INSTANCE_GITIGNORE,
|
|
822
828
|
alreadyScaffolded,
|
|
829
|
+
ownerLoginOf,
|
|
823
830
|
asPostgres,
|
|
824
831
|
buildCorePinRefreshCommit,
|
|
825
832
|
dbCreateCmd,
|
|
@@ -153,7 +153,7 @@ function provisionedEnvPrefix() {
|
|
|
153
153
|
// the hall on this host + OAuth uses the real callback origin — without them the pack
|
|
154
154
|
// stays at neutral localhost and the hall never loads (task 1972). No domain yet
|
|
155
155
|
// (bring-your-own deferred, ADR 0111 §4) ⇒ origins stay neutral until one is set.
|
|
156
|
-
function webEnvBody(inst, { clientId = '', clientSecret = '', appId = null, appPem = null, federation = null } = {}) {
|
|
156
|
+
function webEnvBody(inst, { clientId = '', clientSecret = '', appId = null, appPem = null, federation = null, ownerLogin = null } = {}) {
|
|
157
157
|
const prefix = provisionedEnvPrefix(), { settingsEnvLines } = require(path.join(REPO_ROOT, 'modules/provisioning/provisioning')); // eslint-disable-line global-require
|
|
158
158
|
const origin = inst.domain ? `https://${inst.domain}` : '';
|
|
159
159
|
const originLines = origin
|
|
@@ -191,12 +191,12 @@ ${prefix}_OAUTH_ORIGIN=${origin}
|
|
|
191
191
|
// demo had every origin override but no FIRST_ADMIN, so the owner could not be seated).
|
|
192
192
|
// Derived from the instance repo owner (target_ref = "<owner>/<repo>"), the SAME source
|
|
193
193
|
// instanceInitSpec uses for the scaffold's branding.firstAdmin — so this env override and
|
|
194
|
-
// the scaffolded config/branding.json agree
|
|
195
|
-
//
|
|
194
|
+
// the scaffolded config/branding.json agree — and, like the spec, PREFERS the signed-in
|
|
195
|
+
// builder's login when the caller resolved one (ownerLoginOf). An org-owned repo's
|
|
196
|
+
// owner is the org, which is not a sign-in identity: seating it seats nobody (task 1003240).
|
|
196
197
|
const ownerRepo = parseTargetRef(inst.target_ref);
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
: '';
|
|
198
|
+
const firstAdmin = ownerLogin || (ownerRepo && ownerRepo.owner) || null;
|
|
199
|
+
const firstAdminLine = firstAdmin ? `${prefix}_FIRST_ADMIN=${firstAdmin}\n` : '';
|
|
200
200
|
// Cross-site identity federation block (ADR 0141 §4, task #1002109). Present only for
|
|
201
201
|
// a HOSTED (federated) instance: flips auth.idp ON via the env override branding.js
|
|
202
202
|
// reads (idpEnabled), and carries the hub origin, this instance's per-client secret
|
package/scripts/gds/provision.js
CHANGED
|
@@ -50,7 +50,7 @@ const cp = require('node:child_process');
|
|
|
50
50
|
const crypto = require('node:crypto');
|
|
51
51
|
|
|
52
52
|
const { CONFIG, MANIFEST_UA, MANIFEST_VERIFY_INTERVAL_MS, MANIFEST_VERIFY_TRIES, MAX_INTENT_ATTEMPTS, REPO_ROOT, coreVersionSafe, hasFlag, loadDeps, oauthSecret, provisionerBotEmail } = require('./provision-config.js');
|
|
53
|
-
const { APP_USER, alreadyScaffolded, buildCorePinRefreshCommit, dbCreateCmd, dbName, deployKeyPath, deployKeyTitle, ensurePrivateRepoAccess, installedCorePackDir, installedCoreTarball, instanceInitSpec, instanceRepoRemote, migrateCmd, onboardMode, parseTargetRef, planCorePinRefresh, refreshStandaloneCorePin, resolveOwnerGithubToken, resolveVendorableCoreTarball, safeVersionLabel, scaffoldStandaloneRepo, seedFirstVersionCmd, standaloneInstallCmd, standaloneMigrateCmd, standalonePullCmd, standaloneRegenDocsCmd, standaloneRoot } = require('./provision-repo.js');
|
|
53
|
+
const { APP_USER, alreadyScaffolded, buildCorePinRefreshCommit, dbCreateCmd, dbName, deployKeyPath, deployKeyTitle, ensurePrivateRepoAccess, installedCorePackDir, installedCoreTarball, instanceInitSpec, instanceRepoRemote, migrateCmd, onboardMode, ownerLoginOf, parseTargetRef, planCorePinRefresh, refreshStandaloneCorePin, resolveOwnerGithubToken, resolveVendorableCoreTarball, safeVersionLabel, scaffoldStandaloneRepo, seedFirstVersionCmd, standaloneInstallCmd, standaloneMigrateCmd, standalonePullCmd, standaloneRegenDocsCmd, standaloneRoot } = require('./provision-repo.js');
|
|
54
54
|
const { backupScriptPath, backupService, backupServicePath, backupTimer, backupTimerPath, backupUnitName, serviceUnit, serviceUnitPath, webEnvBody, webEnvPath } = require('./provision-units.js');
|
|
55
55
|
const { caddyBlock, caddySnippetPath, checkDnsTokenScope, classifyZoneScope, dnsUpsertEnv, federateInstance, federationHubOrigin, healthzCmd, shouldFederate, zoneForDomain } = require('./provision-net.js');
|
|
56
56
|
|
|
@@ -381,7 +381,7 @@ async function provisionInstance(inst, deps) {
|
|
|
381
381
|
|
|
382
382
|
// ── step 3: config + per-instance secrets ─────────────────────────────────
|
|
383
383
|
log(' [config] per-instance web.env');
|
|
384
|
-
if (local) boxWriteFile(webEnvPath(inst), webEnvBody(inst, { federation }), { mode: 0o600, sudo: true });
|
|
384
|
+
if (local) boxWriteFile(webEnvPath(inst), webEnvBody(inst, { federation, ownerLogin: await ownerLoginOf(inst, deps.db) }), { mode: 0o600, sudo: true });
|
|
385
385
|
else log(' (baked into cloud-init)');
|
|
386
386
|
|
|
387
387
|
// ── step 4: systemd unit + enable ─────────────────────────────────────────
|
|
@@ -588,7 +588,7 @@ async function domainAttachInstance(inst, deps) {
|
|
|
588
588
|
: CONFIG.publicIp;
|
|
589
589
|
|
|
590
590
|
log(' [config] re-write per-instance web.env (origins now carry the domain)');
|
|
591
|
-
boxWriteFile(webEnvPath(inst), webEnvBody(inst, { federation: null }), { mode: 0o600, sudo: true });
|
|
591
|
+
boxWriteFile(webEnvPath(inst), webEnvBody(inst, { federation: null, ownerLogin: await ownerLoginOf(inst, deps.db) }), { mode: 0o600, sudo: true });
|
|
592
592
|
log(' [service] restart to pick up the new origins');
|
|
593
593
|
boxExec(`${sudoP}systemctl restart ${inst.slug}`);
|
|
594
594
|
|
|
@@ -790,7 +790,7 @@ async function placeManifestCreds(inst, mrow, deps) {
|
|
|
790
790
|
// writeFile logs only a byte count; the value is base64/temp-file placed).
|
|
791
791
|
log(` [oauth-manifest] place creds in ${webEnvPath(inst)} + restart ${inst.slug}`);
|
|
792
792
|
boxWriteFile(webEnvPath(inst), webEnvBody(inst, {
|
|
793
|
-
clientId: creds.clientId, clientSecret: creds.clientSecret,
|
|
793
|
+
clientId: creds.clientId, clientSecret: creds.clientSecret, ownerLogin: await ownerLoginOf(inst, deps.db),
|
|
794
794
|
// The App id + private key double as the ADR 0055 publish credential (task
|
|
795
795
|
// 1003049); older Apps (pre-permission manifests) return a pem too — placing
|
|
796
796
|
// it is harmless and heals once the owner grants the permissions + installs.
|
|
@@ -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,12 @@ 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)
|
|
1137
|
+
// 1.19.353 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1138
|
+
// landed since 1.19.352 with no explicit bump. run 32851392269. (task 1002620)
|
|
1135
1139
|
// ---------------------------------------------------------------------------
|
|
1136
|
-
const CORE_VERSION = '1.19.
|
|
1140
|
+
const CORE_VERSION = '1.19.353'; // CI auto-patch carrier (ADR 0161); see changelog above
|
|
1137
1141
|
|
|
1138
1142
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
1139
1143
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
package/tests/provision.mjs
CHANGED
|
@@ -238,6 +238,17 @@ t('webEnvBody: the repo target_ref seats the founding owner via <prefix>_FIRST_A
|
|
|
238
238
|
assert.doesNotMatch(P.webEnvBody(coTenant), /_FIRST_ADMIN=/);
|
|
239
239
|
});
|
|
240
240
|
|
|
241
|
+
t('webEnvBody: a resolved owner login wins over the repo owner — an org-owned repo seats the signed-in builder (task 1003240)', () => {
|
|
242
|
+
// The org that owns an adopted repo is not a sign-in identity; seating it seats nobody.
|
|
243
|
+
// Same preference instanceInitSpec already applies to the scaffold's branding.firstAdmin.
|
|
244
|
+
const inst = { ...coTenant, hosting_shape: 'standalone', domain: 'mercury.example.com', target_ref: 'acme-org/mercury' };
|
|
245
|
+
const withLogin = P.webEnvBody(inst, { ownerLogin: 'octocat' });
|
|
246
|
+
assert.match(withLogin, new RegExp(`${reEsc(NEUTRAL_PREFIX)}_FIRST_ADMIN=octocat`));
|
|
247
|
+
assert.doesNotMatch(withLogin, /_FIRST_ADMIN=acme-org/);
|
|
248
|
+
// no resolved login → the repo owner stays the fallback (a personal repo's owner IS the builder)
|
|
249
|
+
assert.match(P.webEnvBody(inst), new RegExp(`${reEsc(NEUTRAL_PREFIX)}_FIRST_ADMIN=acme-org`));
|
|
250
|
+
});
|
|
251
|
+
|
|
241
252
|
t('caddyBlock: a domain → on-demand-TLS reverse_proxy to the local port; no domain → a comment', () => {
|
|
242
253
|
const withDomain = P.caddyBlock({ ...coTenant, domain: 'mercury.example.com' });
|
|
243
254
|
assert.match(withDomain, /mercury\.example\.com \{/);
|
|
@@ -408,6 +419,21 @@ await ta('provisionInstance(standalone): the backup unit ExecStart points at the
|
|
|
408
419
|
assert.match(svc, /Environment=GDS_BACKUP_DB=mercury/);
|
|
409
420
|
});
|
|
410
421
|
|
|
422
|
+
await ta('provisionInstance(standalone): web.env seats the signed-in builder, not the org that owns the repo (task 1003240)', async () => {
|
|
423
|
+
const { deps } = recorder();
|
|
424
|
+
const bodies = {};
|
|
425
|
+
deps.writeFile = (p, body) => { bodies[p] = body; return { ok: true }; };
|
|
426
|
+
// the builders lookup answers with the signed-in owner; every other query stays empty
|
|
427
|
+
deps.db = { query: async (sql) => (/FROM builders/.test(String(sql)) ? { rows: [{ github_login: 'octocat' }] } : { rows: [] }) };
|
|
428
|
+
const inst = { ...coTenant, hosting_shape: 'standalone', target_ref: 'acme-org/mercury', owner_builder_id: 7 };
|
|
429
|
+
const r = await P.provisionInstance(inst, deps);
|
|
430
|
+
assert.equal(r.ok, true);
|
|
431
|
+
const envPath = Object.keys(bodies).find((p) => p.endsWith('/web.env'));
|
|
432
|
+
assert.ok(envPath, 'wrote web.env');
|
|
433
|
+
assert.match(bodies[envPath], new RegExp(`${reEsc(NEUTRAL_PREFIX)}_FIRST_ADMIN=octocat`));
|
|
434
|
+
assert.doesNotMatch(bodies[envPath], /_FIRST_ADMIN=acme-org/);
|
|
435
|
+
});
|
|
436
|
+
|
|
411
437
|
await ta('provisionInstance(standalone) SCAFFOLDS its repo, then syncs (pull + npm ci) + migrates from the instance root', async () => {
|
|
412
438
|
const { deps, cmds, writes } = recorder();
|
|
413
439
|
const r = await P.provisionInstance({ ...coTenant, hosting_shape: 'standalone', target_ref: 'lars589/mercury' }, deps);
|
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}` });
|