@bongos/core 1.25.24 → 1.25.25
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 +25 -20
- package/docs/copy-inventory.md +223 -218
- package/docs/copy-registry.json +284 -239
- package/docs/module-api-changelog.md +2 -0
- package/docs/page-readings.json +194 -188
- package/modules/public-landing/public/projects.html +6 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +6 -0
- package/src/bongos/auth-github.js +17 -0
- package/src/bongos/auth.js +2 -1
- package/src/bongos/routes/auth.js +7 -7
- package/src/module-api.js +1 -1
- package/tests/auth_switch_account.mjs +3 -7
- package/tests/github_signup_handoff.mjs +135 -0
|
@@ -1558,6 +1558,12 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
1558
1558
|
<svg class="gh" viewBox="0 0 16 16" aria-hidden="true"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27s1.36.09 2 .27c1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.01 8.01 0 0 0 16 8c0-4.42-3.58-8-8-8z"/></svg>
|
|
1559
1559
|
Continue with GitHub
|
|
1560
1560
|
</a>
|
|
1561
|
+
<!-- No GitHub account yet (task 1005283): the same sign-in, opened on GitHub's
|
|
1562
|
+
sign-up page (signup=1), which offers "Continue with Google" and hands the
|
|
1563
|
+
sign-in straight back here once the account exists. Bongos cannot make a
|
|
1564
|
+
GitHub account for anyone; GitHub can, in one step, with Google. -->
|
|
1565
|
+
<p class="finePrint" id="wizWhyGh">Your project’s files live in a GitHub repository, so it needs a GitHub account.</p>
|
|
1566
|
+
<p class="finePrint" id="wizNoGh">No GitHub account? <a id="wizSignup" href="/api/bongos/auth/web/start?via=github&signup=1&return=/projects%3Fview%3Dnew&repos=1">Make one free with Google</a>. On GitHub’s page choose <strong>Continue with Google</strong>, and you come straight back here.</p>
|
|
1561
1567
|
<!-- Informed consent BEFORE GitHub's own wording (ADR 0155): the scope the
|
|
1562
1568
|
button asks for reads as "all your repositories" on GitHub's screen, so
|
|
1563
1569
|
the reason is stated here, on the path every founder actually takes. -->
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.25.
|
|
3
|
+
"version": "1.25.25",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.25.
|
|
9
|
+
"version": "1.25.25",
|
|
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.25.
|
|
3
|
+
"version": "1.25.25",
|
|
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",
|
package/release-notes.json
CHANGED
|
@@ -11912,5 +11912,11 @@
|
|
|
11912
11912
|
"id": "1005107",
|
|
11913
11913
|
"text": "A founder can now go back and change their Genesis plan after saving it, and change any earlier stage's answers until the Board Room has voted that stage through — a voted stage shows its answers with a plain reason why they a"
|
|
11914
11914
|
}
|
|
11915
|
+
],
|
|
11916
|
+
"1.25.25": [
|
|
11917
|
+
{
|
|
11918
|
+
"id": "1005283",
|
|
11919
|
+
"text": "Someone with no GitHub account can now start a project. The creation page explains why GitHub is needed and offers a 'Make one free with Google' link. GitHub's own sign-up page opens, they continue with their Google account, a"
|
|
11920
|
+
}
|
|
11915
11921
|
]
|
|
11916
11922
|
}
|
|
@@ -291,6 +291,22 @@ function buildAuthorizeUrl(state, redirectUri, { repos = false, switchAccount =
|
|
|
291
291
|
return u.toString();
|
|
292
292
|
}
|
|
293
293
|
|
|
294
|
+
// GitHub's own sign-up page, for a person with no GitHub account (task 1005283). It is the page GitHub's sign-in screen links as "Create an account"
|
|
295
|
+
// during an OAuth sign-in: `return_to` carries the authorize step back, so once the
|
|
296
|
+
// account exists (GitHub's sign-up offers "Continue with Google") GitHub resumes the
|
|
297
|
+
// SAME sign-in — same state, same scope — and our callback lands the person where they
|
|
298
|
+
// started. Only github.com's own authorize path is ever handed on; anything else is null
|
|
299
|
+
// and the caller falls back to the plain authorize redirect.
|
|
300
|
+
function githubSignupUrl(authorizeUrl) {
|
|
301
|
+
let u;
|
|
302
|
+
try { u = new URL(String(authorizeUrl)); } catch { return null; }
|
|
303
|
+
if (u.protocol !== 'https:' || u.host !== 'github.com' || u.pathname !== '/login/oauth/authorize') return null;
|
|
304
|
+
const signup = new URL('https://github.com/signup');
|
|
305
|
+
signup.searchParams.set('return_to', u.pathname + u.search);
|
|
306
|
+
signup.searchParams.set('source', 'oauth');
|
|
307
|
+
return signup.toString();
|
|
308
|
+
}
|
|
309
|
+
|
|
294
310
|
async function completeWebFlow({ code, redirectUri, userAgent, captureRepoToken = false }) {
|
|
295
311
|
const { accessToken, scope: grantedScope } = await exchangeCodeForToken(code, redirectUri);
|
|
296
312
|
const ghUser = await fetchGithubUser(accessToken);
|
|
@@ -386,6 +402,7 @@ async function completeWebFlow({ code, redirectUri, userAgent, captureRepoToken
|
|
|
386
402
|
// slashes on origin are trimmed so `https://hub/` and `https://hub` behave alike.
|
|
387
403
|
|
|
388
404
|
module.exports = {
|
|
405
|
+
githubSignupUrl,
|
|
389
406
|
buildAuthorizeUrl,
|
|
390
407
|
completeWebFlow,
|
|
391
408
|
exchangeCodeForToken,
|
package/src/bongos/auth.js
CHANGED
|
@@ -58,7 +58,7 @@ const { branding, userAgent } = require('../branding');
|
|
|
58
58
|
|
|
59
59
|
const { SCOPE, authConfigured, grantedScopeCoversRepos, idpConfigured, idpEnabled, loadIdpConfig, scopeForWebFlow } = require('./auth-config.js');
|
|
60
60
|
const { firstAdminLogin, firstTimeAdmissionBlocked, instanceNeedsFirstAdmin, joinabilityMode, maybeSeatFirstAdmin, membershipKind, notifyHubOfInvite, openEnrollmentEnabled, readAppStatusFromHub, readApplicantProfileFromHub, readApplicantViewFromHub, webAdmissionStatus } = require('./auth-admission.js');
|
|
61
|
-
const { buildAuthorizeUrl, completeWebFlow, pollDeviceFlow, startDeviceFlow } = require('./auth-github.js');
|
|
61
|
+
const { buildAuthorizeUrl, githubSignupUrl, completeWebFlow, pollDeviceFlow, startDeviceFlow } = require('./auth-github.js');
|
|
62
62
|
const { buildIdpAuthorizeUrl, completeDeviceExchange, completeIdpFlow, idpAuthorizeUrl, loadHubPublicKey, logoutTokenJtiSeenAndRecord, readApplicationVouch, verifyHubAssertion, verifyHubLogoutToken } = require('./auth-idp.js');
|
|
63
63
|
const { addDiscordGuildMember, buildDiscordAuthorizeUrl, discordAuthorizeUrl, discordBotConfigured, discordConfigured, exchangeDiscordCode, fetchDiscordUser, isValidDiscordUserId, loadDiscordBotToken, loadDiscordCreds, removeDiscordGuildMember } = require('./auth-discord.js');
|
|
64
64
|
|
|
@@ -854,6 +854,7 @@ module.exports = {
|
|
|
854
854
|
startDeviceFlow,
|
|
855
855
|
pollDeviceFlow,
|
|
856
856
|
buildAuthorizeUrl,
|
|
857
|
+
githubSignupUrl,
|
|
857
858
|
scopeForWebFlow,
|
|
858
859
|
grantedScopeCoversRepos,
|
|
859
860
|
buildIdpAuthorizeUrl,
|
|
@@ -710,6 +710,7 @@ module.exports = function buildAuthRouter() {
|
|
|
710
710
|
// ?switch=1 — sign in as a DIFFERENT GitHub account (task 1004789): show
|
|
711
711
|
// GitHub's account picker instead of re-using the one logged in there.
|
|
712
712
|
const wantsSwitch = req.query.switch === '1' || req.query.switch === 'true';
|
|
713
|
+
const wantsSignup = req.query.signup === '1' || req.query.signup === 'true'; // no GitHub account yet: its sign-up page (task 1005283)
|
|
713
714
|
// A sign-in begun on a satellite hub host (builders./status.) restarts on
|
|
714
715
|
// the canonical host (task 1004357). The handshake cookies are host-only now,
|
|
715
716
|
// and the callback always lands on the canonical host, so a state cookie set
|
|
@@ -720,8 +721,7 @@ module.exports = function buildAuthRouter() {
|
|
|
720
721
|
const raw = safeReturnTo(req.query.return) || safeReturnTo(req.headers.referer) || '/';
|
|
721
722
|
const back = raw.startsWith('/') ? `https://${host}${raw}` : raw;
|
|
722
723
|
const params = new URLSearchParams({ return: back });
|
|
723
|
-
|
|
724
|
-
if (wantsSwitch) params.set('switch', '1');
|
|
724
|
+
for (const [k, on] of [['repos', wantsRepos], ['switch', wantsSwitch], ['signup', wantsSignup]]) if (on) params.set(k, '1');
|
|
725
725
|
if (req.query.via === 'github') params.set('via', 'github');
|
|
726
726
|
return res.redirect(`${WEB_REDIRECT_ORIGIN.replace(/\/+$/, '')}${apiBase(req)}/auth/web/start?${params}`);
|
|
727
727
|
}
|
|
@@ -729,7 +729,7 @@ module.exports = function buildAuthRouter() {
|
|
|
729
729
|
// host (a builder who signed in on the apex, then opened builders.): no
|
|
730
730
|
// GitHub round-trip, just hand that host its copy. A repo-scope sign-in
|
|
731
731
|
// still goes to GitHub — it exists to ask for the broader grant.
|
|
732
|
-
if (!wantsRepos && !wantsSwitch) {
|
|
732
|
+
if (!wantsRepos && !wantsSwitch && !wantsSignup) {
|
|
733
733
|
const target = safeReturnTo(req.query.return);
|
|
734
734
|
if (target && !target.startsWith('/')) {
|
|
735
735
|
try {
|
|
@@ -746,7 +746,7 @@ module.exports = function buildAuthRouter() {
|
|
|
746
746
|
// lands on one chooser — Continue with GitHub or Continue with Google — so no
|
|
747
747
|
// page needs its own pair of buttons. `via=github` is the GitHub button (and
|
|
748
748
|
// any door that needs GitHub itself); "New account" names a GitHub account.
|
|
749
|
-
if (req.query.via !== 'github' && !wantsSwitch && googleConfigured()) {
|
|
749
|
+
if (req.query.via !== 'github' && !wantsSwitch && !wantsSignup && googleConfigured()) {
|
|
750
750
|
const ret = safeReturnTo(req.query.return) || safeReturnTo(req.headers.referer);
|
|
751
751
|
const gh = new URLSearchParams({ via: 'github' });
|
|
752
752
|
if (ret) gh.set('return', ret);
|
|
@@ -803,9 +803,9 @@ module.exports = function buildAuthRouter() {
|
|
|
803
803
|
// Federate through the hub when configured (ADR 0141 §1); otherwise the direct
|
|
804
804
|
// GitHub authorize, carrying the repo-scope opt-in. Same CSRF state cookie either
|
|
805
805
|
// way — the callback validates it identically, so the hub leg is CSRF-protected too.
|
|
806
|
-
res.redirect(auth.
|
|
807
|
-
|
|
808
|
-
|
|
806
|
+
if (auth.idpConfigured()) return res.redirect(auth.buildIdpAuthorizeUrl(state, redirectUri));
|
|
807
|
+
const authorizeUrl = auth.buildAuthorizeUrl(state, redirectUri, { repos: wantsRepos, switchAccount: wantsSwitch });
|
|
808
|
+
res.redirect((wantsSignup && auth.githubSignupUrl(authorizeUrl)) || authorizeUrl);
|
|
809
809
|
});
|
|
810
810
|
|
|
811
811
|
// GET /auth/web/handoff?code=&return= — the far side of the one-time session
|
package/src/module-api.js
CHANGED
|
@@ -81,7 +81,7 @@ const { sleep } = require('./sleep');
|
|
|
81
81
|
// MAJOR (see allowBoxScope below): passes the request through untouched.
|
|
82
82
|
function deprecatedNoopMiddleware(_req, _res, next) { next(); }
|
|
83
83
|
|
|
84
|
-
const CORE_VERSION = '1.25.
|
|
84
|
+
const CORE_VERSION = '1.25.25'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
85
85
|
|
|
86
86
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
87
87
|
// Usage: const log = api.logger('discord'); log.info('mounted');
|
|
@@ -24,13 +24,9 @@ test('switchAccount adds prompt=select_account; a plain sign-in does not', () =>
|
|
|
24
24
|
assert.equal(sw.searchParams.get('scope'), plain.searchParams.get('scope'));
|
|
25
25
|
});
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
assert.match(src, /switchAccount: wantsSwitch/);
|
|
31
|
-
// a switch must reach GitHub, never short-circuit through the live-session handoff
|
|
32
|
-
assert.match(src, /if \(!wantsRepos && !wantsSwitch\) \{/);
|
|
33
|
-
});
|
|
27
|
+
// The start route itself is now RUN, not read (task 1005283): tests/github_signup_handoff.mjs
|
|
28
|
+
// calls GET /auth/web/start with ?switch=1 and asserts the redirect carries
|
|
29
|
+
// prompt=select_account and goes to GitHub, never through the live-session handoff.
|
|
34
30
|
|
|
35
31
|
test('the landing carries the New account door beside Sign in', () => {
|
|
36
32
|
const html = fs.readFileSync(path.join(ROOT, 'modules/public-landing/public/index.html'), 'utf8');
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// tests/github_signup_handoff.mjs — no GitHub account yet: GitHub's own sign-up page, with
|
|
2
|
+
// "Continue with Google", and back to project creation (task 1005283).
|
|
3
|
+
//
|
|
4
|
+
// WHY. Bongos cannot make a GitHub account for anyone, and every project lives in a GitHub
|
|
5
|
+
// repository. A person with only a Google account used to meet "Continue with GitHub" and
|
|
6
|
+
// nothing else. GitHub's sign-up page offers "Continue with Google", and during an OAuth
|
|
7
|
+
// sign-in GitHub's own screen links it with `return_to` = the authorize step, so the
|
|
8
|
+
// sign-in resumes once the account exists (checked on GitHub's live pages, signed out,
|
|
9
|
+
// 2026-11-13). `?signup=1` on our sign-in start opens THAT page directly.
|
|
10
|
+
//
|
|
11
|
+
// What this pins, by RUNNING the start route's handler (no network, no database):
|
|
12
|
+
// 1. ?signup=1 redirects to https://github.com/signup with return_to = the same
|
|
13
|
+
// authorize step (same state cookie, same client, same repo scope, same callback);
|
|
14
|
+
// 2. it skips the GitHub-or-Google chooser, which would offer a Bongos Google sign-in
|
|
15
|
+
// that cannot make a project;
|
|
16
|
+
// 3. a plain sign-in still goes to the authorize step, and ?switch=1 still adds the
|
|
17
|
+
// account picker (converted here from tests/auth_switch_account.mjs's source read);
|
|
18
|
+
// 4. githubSignupUrl hands on only github.com's own authorize path;
|
|
19
|
+
// 5. the creation page's gate says why GitHub is needed and carries the door, whose
|
|
20
|
+
// link runs through the route above and returns to /projects?view=new.
|
|
21
|
+
//
|
|
22
|
+
// Run: node --test tests/github_signup_handoff.mjs
|
|
23
|
+
|
|
24
|
+
import assert from 'node:assert/strict';
|
|
25
|
+
import { test } from 'node:test';
|
|
26
|
+
import fs from 'node:fs';
|
|
27
|
+
import path from 'node:path';
|
|
28
|
+
import { createRequire } from 'node:module';
|
|
29
|
+
import { fileURLToPath } from 'node:url';
|
|
30
|
+
|
|
31
|
+
process.env.BONGOS_GITHUB_CLIENT_ID = process.env.BONGOS_GITHUB_CLIENT_ID || 'gh-test-client';
|
|
32
|
+
process.env.BONGOS_GITHUB_CLIENT_SECRET = process.env.BONGOS_GITHUB_CLIENT_SECRET || 'gh-test-secret';
|
|
33
|
+
// Google sign-in on, so the chooser would show for a plain sign-in: the door must skip it
|
|
34
|
+
process.env.BONGOS_GOOGLE_CLIENT_ID = 'google-test-client';
|
|
35
|
+
process.env.BONGOS_GOOGLE_CLIENT_SECRET = 'google-test-secret';
|
|
36
|
+
|
|
37
|
+
const require = createRequire(import.meta.url);
|
|
38
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
39
|
+
// "Continue with Google" is offered only when the hub that keeps Google sign-ins is present
|
|
40
|
+
const seams = require(path.join(ROOT, 'src/module-seams.js'));
|
|
41
|
+
if (!seams.hasProvider('platform-identity.hub')) {
|
|
42
|
+
seams.registerProvider('platform-identity.hub', { signInWithGoogle: async () => null, recordHubSignin: async () => {} });
|
|
43
|
+
}
|
|
44
|
+
const buildAuthRouter = require(path.join(ROOT, 'src/bongos/routes/auth.js'));
|
|
45
|
+
const { githubSignupUrl } = require(path.join(ROOT, 'src/bongos/auth.js'));
|
|
46
|
+
const { attachFail } = require(path.join(ROOT, 'src/bongos/middleware/error-envelope.js'));
|
|
47
|
+
|
|
48
|
+
function fakeRes() {
|
|
49
|
+
const res = {
|
|
50
|
+
_status: 200, _body: null, _redirect: null, _cookies: {},
|
|
51
|
+
status(c) { this._status = c; return this; },
|
|
52
|
+
type() { return this; },
|
|
53
|
+
send(b) { this._body = b; return this; },
|
|
54
|
+
json(b) { this._body = b; return this; },
|
|
55
|
+
set() { return this; },
|
|
56
|
+
redirect(a, b) { this._redirect = b === undefined ? a : b; return this; },
|
|
57
|
+
cookie(n, v, o) { this._cookies[n] = { v, o }; return this; },
|
|
58
|
+
clearCookie() { return this; },
|
|
59
|
+
};
|
|
60
|
+
attachFail({}, res, () => {});
|
|
61
|
+
return res;
|
|
62
|
+
}
|
|
63
|
+
const start = (() => {
|
|
64
|
+
const layer = buildAuthRouter().stack.find((l) => l.route?.path === '/auth/web/start' && l.route.methods.get);
|
|
65
|
+
assert.ok(layer, 'GET /auth/web/start exists');
|
|
66
|
+
return layer.route.stack.at(-1).handle;
|
|
67
|
+
})();
|
|
68
|
+
const req = (query) => ({ query, headers: { host: 'localhost' }, baseUrl: '/api/bongos', body: {} });
|
|
69
|
+
async function run(query) { const res = fakeRes(); await start(req(query), res); return res; }
|
|
70
|
+
|
|
71
|
+
test('?signup=1 opens GitHub’s sign-up page, which resumes this same sign-in', async () => {
|
|
72
|
+
const res = await run({ via: 'github', signup: '1', return: '/projects?view=new', repos: '1' });
|
|
73
|
+
assert.ok(res._redirect, 'a redirect, not the chooser page');
|
|
74
|
+
const signup = new URL(res._redirect);
|
|
75
|
+
assert.equal(signup.origin + signup.pathname, 'https://github.com/signup');
|
|
76
|
+
assert.equal(signup.searchParams.get('source'), 'oauth', 'the same marker GitHub’s own "Create an account" link carries');
|
|
77
|
+
const back = new URL(signup.searchParams.get('return_to'), 'https://github.com');
|
|
78
|
+
assert.equal(back.pathname, '/login/oauth/authorize', 'the way back is the authorize step');
|
|
79
|
+
assert.equal(back.searchParams.get('client_id'), process.env.BONGOS_GITHUB_CLIENT_ID);
|
|
80
|
+
const stateCookie = Object.values(res._cookies).find((c) => /^[0-9a-f]{32}$/.test(String(c.v)));
|
|
81
|
+
assert.ok(stateCookie, 'a state cookie is set');
|
|
82
|
+
assert.equal(back.searchParams.get('state'), stateCookie.v,
|
|
83
|
+
'the state GitHub hands back is the one in this browser’s cookie, so the callback accepts it');
|
|
84
|
+
assert.match(back.searchParams.get('scope'), /\brepo\b/, 'the repo access project creation needs is asked once, on the way back');
|
|
85
|
+
assert.match(back.searchParams.get('redirect_uri'), /\/auth\/web\/callback$/);
|
|
86
|
+
assert.ok(Object.values(res._cookies).some((c) => c.v === '/projects?view=new'), 'after the callback, the person lands on project creation');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('the door skips the GitHub-or-Google chooser; a plain sign-in still shows it', async () => {
|
|
90
|
+
const signup = await run({ signup: '1', return: '/projects?view=new', repos: '1' });
|
|
91
|
+
assert.match(signup._redirect || '', /^https:\/\/github\.com\/signup\?/, 'no via=github needed: a GitHub sign-up is GitHub');
|
|
92
|
+
const plain = await run({ return: '/projects?view=new', repos: '1' });
|
|
93
|
+
assert.equal(plain._redirect, null);
|
|
94
|
+
assert.match(plain._body, /Continue with GitHub/);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test('without ?signup a sign-in goes to the authorize step, and ?switch=1 still adds the account picker', async () => {
|
|
98
|
+
const plain = await run({ via: 'github', return: '/projects?view=new', repos: '1' });
|
|
99
|
+
const p = new URL(plain._redirect);
|
|
100
|
+
assert.equal(p.origin + p.pathname, 'https://github.com/login/oauth/authorize');
|
|
101
|
+
assert.equal(p.searchParams.get('prompt'), null);
|
|
102
|
+
const sw = await run({ switch: '1', return: '/' });
|
|
103
|
+
const s = new URL(sw._redirect);
|
|
104
|
+
assert.equal(s.pathname, '/login/oauth/authorize');
|
|
105
|
+
assert.equal(s.searchParams.get('prompt'), 'select_account', 'a switch reaches GitHub, never the live-session handoff');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('githubSignupUrl hands on only github.com’s own authorize path', () => {
|
|
109
|
+
const ok = githubSignupUrl('https://github.com/login/oauth/authorize?client_id=c&state=s&scope=repo');
|
|
110
|
+
assert.equal(new URL(ok).searchParams.get('return_to'), '/login/oauth/authorize?client_id=c&state=s&scope=repo');
|
|
111
|
+
for (const bad of [
|
|
112
|
+
'https://evil.example/login/oauth/authorize?state=s',
|
|
113
|
+
'http://github.com/login/oauth/authorize?state=s',
|
|
114
|
+
'https://github.com/settings/tokens',
|
|
115
|
+
'https://github.com.evil.example/login/oauth/authorize',
|
|
116
|
+
'not a url',
|
|
117
|
+
null,
|
|
118
|
+
]) assert.equal(githubSignupUrl(bad), null, String(bad));
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test('the creation page says why GitHub is needed and offers the door, returning to creation', () => {
|
|
122
|
+
const hub = fs.readFileSync(path.join(ROOT, 'modules/public-landing/public/projects.html'), 'utf8').replace(/\r\n/g, '\n');
|
|
123
|
+
const gate = (hub.match(/<section class="panel" id="panel-gate"[\s\S]*?<\/section>/) || [''])[0];
|
|
124
|
+
assert.ok(gate, 'the gate is findable');
|
|
125
|
+
assert.match(gate, /<p class="finePrint" id="wizWhyGh">Your project’s files live in a GitHub repository, so it needs a GitHub account\.<\/p>/);
|
|
126
|
+
const door = gate.match(/<a id="wizSignup" href="([^"]+)">Make one free with Google<\/a>/);
|
|
127
|
+
assert.ok(door, 'the door sits on the gate');
|
|
128
|
+
assert.ok(gate.indexOf('id="wizSignin"') < gate.indexOf('id="wizSignup"'), 'under Continue with GitHub, not instead of it');
|
|
129
|
+
const href = new URL(door[1].replace(/&/g, '&'), 'https://hub.example');
|
|
130
|
+
assert.equal(href.pathname, '/api/bongos/auth/web/start');
|
|
131
|
+
assert.equal(href.searchParams.get('signup'), '1');
|
|
132
|
+
assert.equal(href.searchParams.get('repos'), '1', 'the same repo access the GitHub button asks for');
|
|
133
|
+
assert.equal(href.searchParams.get('return'), '/projects?view=new');
|
|
134
|
+
assert.match(gate, /choose <strong>Continue with Google<\/strong>, and you come straight back here/);
|
|
135
|
+
});
|