@bongos/core 1.19.424 → 1.19.426

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.
Files changed (32) hide show
  1. package/.bongos-core.json +59 -34
  2. package/docs/adr/0210-scouting-graduates-on-an-instance-asserted-owner-claim.md +100 -0
  3. package/docs/adr/README.md +1 -0
  4. package/docs/api/openapi.json +1 -1
  5. package/docs/copy-inventory.md +184 -184
  6. package/docs/copy-registry.json +235 -235
  7. package/docs/design/mocks/hub/wizard.html +421 -0
  8. package/docs/design/projects-hub-direction-v2.md +26 -0
  9. package/docs/file-map.md +2 -0
  10. package/docs/module-api-changelog.md +4 -0
  11. package/modules/hall-ui/public/people.js +5 -5
  12. package/modules/hall-ui/public/settings.html +2 -2
  13. package/modules/hall-ui/public/settings.js +12 -12
  14. package/modules/platform-identity/account-visibility.js +47 -1
  15. package/modules/platform-identity/platform-identity.js +14 -11
  16. package/modules/platform-identity/routes/scouting.js +61 -21
  17. package/modules/platform-identity/routes/sso.js +6 -0
  18. package/modules/platform-identity/scouting-authz.js +139 -0
  19. package/modules/ui-design/kit/serve.js +3 -1
  20. package/package-lock.json +2 -2
  21. package/package.json +1 -1
  22. package/public-landing/assets/cosmos.css +63 -0
  23. package/public-landing/projects.html +207 -199
  24. package/public-landing/projects.probes.json +7 -1
  25. package/public-landing/projects.states.json +7 -1
  26. package/src/bongos/auth.js +59 -3
  27. package/src/module-api.js +1 -1
  28. package/tests/account_privacy_flags.mjs +23 -13
  29. package/tests/effective_visibility_predicate.mjs +49 -4
  30. package/tests/projects_hub.mjs +56 -0
  31. package/tests/scouting_owner_access_and_reach.mjs +544 -0
  32. package/tests/scouting_page_gate.mjs +185 -0
@@ -28,8 +28,11 @@
28
28
  // rollupVisibleSql / isRollupVisible NUMBERS — visible AND NOT
29
29
  // hide_stats. ADR 0171 D2's predicate EXTENDED with the existence axis,
30
30
  // and COMPOSED from accountVisibleSql rather than restated.
31
- // isRecruiterDiscoverable REACH — the COALESCE, privacy
31
+ // recruiterReachSql / isRecruiterDiscoverable REACH — the COALESCE, privacy
32
32
  // spec D1. A reach predicate only: it never buys addressability.
33
+ // scoutingListableSql THE DIRECTORY — numbers AND reach,
34
+ // the ONE predicate the recruiting directory joins (ADR 0210, task
35
+ // 1002290). Composed, never restated.
33
36
  //
34
37
  // The rollup pair was originally left to "the rollup readers" to apply. Task
35
38
  // 1002967 moved it here for the reason this file exists: there are two such
@@ -137,6 +140,47 @@ function isRecruiterDiscoverable(account) {
137
140
  return account.account_visibility === 'public';
138
141
  }
139
142
 
143
+ // The SAME reach answer as a SQL fragment, for the read that filters in the
144
+ // database (task 1002290). It is the ONE spelling of that COALESCE outside the
145
+ // JS twin above — tests/effective_visibility_predicate.mjs fails any other file
146
+ // that writes it — so a directory query and a route can never disagree about who
147
+ // opted out. NULL still means "follow visibility", exactly as the column comment
148
+ // in platform_identity_012_account_privacy.sql says.
149
+ function recruiterReachSql(alias) {
150
+ if (alias && !/^[A-Za-z_][A-Za-z0-9_]*$/.test(String(alias))) {
151
+ throw new Error('recruiterReachSql: alias must be a bare SQL identifier');
152
+ }
153
+ const q = alias ? `${alias}.` : '';
154
+ return `COALESCE(${q}recruiter_discoverable, ${q}account_visibility = 'public')`;
155
+ }
156
+
157
+ // scoutingListableSql — THE predicate the recruiting directory joins: the rollup
158
+ // axis AND the reach axis (ADR 0210, task 1002290). listScoutingRollup is the
159
+ // one reader; /scouting and the hall's /people are its two surfaces.
160
+ //
161
+ // WHY REACH BELONGS HERE AND NOT IN rollupVisibleSql. The two axes answer
162
+ // different questions and only one surface asks both. `hide_stats` suppresses
163
+ // NUMBERS everywhere, for every reader; the recruiting opt-out removes an
164
+ // account from the RECRUITING SURFACE only. The community spec's D3 leaderboards
165
+ // say so out loud — "the recruiting opt-out does not affect leaderboards; it
166
+ // governs the recruiting surface only" — so folding reach into rollupVisibleSql
167
+ // would silently empty a future public leaderboard of every opted-out builder.
168
+ // A leaderboard reader must therefore join rollupVisibleSql, never this.
169
+ //
170
+ // Composed from rollupVisibleSql and recruiterReachSql rather than restated, so
171
+ // changing what "visible", "hidden" or "reachable" means moves this with it.
172
+ function scoutingListableSql(alias) {
173
+ // Validated HERE as well as in both delegates. The delegates do refuse a bad
174
+ // alias, but only because rollupVisibleSql happens to be evaluated first in the
175
+ // template literal below — a security property should not rest on the
176
+ // evaluation order of a string interpolation, and the error a caller sees
177
+ // should name the function they actually called.
178
+ if (alias && !/^[A-Za-z_][A-Za-z0-9_]*$/.test(String(alias))) {
179
+ throw new Error('scoutingListableSql: alias must be a bare SQL identifier');
180
+ }
181
+ return `(${rollupVisibleSql(alias)} AND ${recruiterReachSql(alias)})`;
182
+ }
183
+
140
184
  module.exports = {
141
185
  isAccountVisible,
142
186
  accountVisibleSql,
@@ -146,4 +190,6 @@ module.exports = {
146
190
  isRollupVisible,
147
191
  ACCOUNT_VISIBILITY,
148
192
  isRecruiterDiscoverable,
193
+ recruiterReachSql,
194
+ scoutingListableSql,
149
195
  };
@@ -20,7 +20,7 @@ const api = require('../../src/module-api');
20
20
  const visibility = require('./account-visibility');
21
21
 
22
22
  const { pool: defaultPool } = api;
23
- const { ACCOUNT_VISIBILITY, accountVisibleSql, rollupVisibleSql, isRollupVisible } = visibility;
23
+ const { ACCOUNT_VISIBILITY, accountVisibleSql, rollupVisibleSql, isRollupVisible, scoutingListableSql } = visibility;
24
24
 
25
25
  // Default authorization-code lifetime — deliberately short (~60s). A code is a
26
26
  // bearer secret that travels through the browser; a tight TTL bounds the replay
@@ -722,15 +722,18 @@ async function upsertActivity({ clientId, githubId, credits = 0, tasksShipped =
722
722
  // disciplines union is computed separately in `disc`. Proven against real SQL by
723
723
  // tests/scouting_rollup_fanout.mjs.
724
724
  //
725
- // Consent by construction (ADR 0171 D2/D3, extended by task 1002967): only
726
- // builders whose account passes rollupVisibleSql — active, publicly visible, not
727
- // hidden — appear. /people and /scouting are live cross-project surfaces, so the
728
- // predicate lives IN the query, not in any caller's memory, and it is the SHARED
729
- // one so this list can never disagree with /profiles/:handle about who is
730
- // visible. Provisional, private and hidden builders drop out entirely; the
731
- // projection stays an explicit whitelist (no raw-row spread). A private account
732
- // that is recruiter-discoverable is absent here on purpose: the full rollup is
733
- // never its disclosure — the D3 sliver is (a separate authorization-scoped port).
725
+ // Consent by construction (ADR 0171 D2/D3, extended by tasks 1002967 + 1002290):
726
+ // only builders whose account passes scoutingListableSql — active, publicly
727
+ // visible, not hidden, AND not opted out of recruiting — appear. /people and
728
+ // /scouting are live cross-project surfaces, so the predicate lives IN the query,
729
+ // not in any caller's memory, and it is the SHARED one so this list can never
730
+ // disagree with /profiles/:handle about who is visible. Provisional, private,
731
+ // hidden and opted-out builders drop out entirely; the projection stays an
732
+ // explicit whitelist. A private account that is recruiter-discoverable is absent
733
+ // here on purpose: the full rollup is never its disclosure — the D3 sliver is.
734
+ // THIS IS THE RECRUITING DIRECTORY: a public LEADERBOARD must not reuse this
735
+ // reader (community spec D3 — the opt-out governs the recruiting surface only);
736
+ // it joins rollupVisibleSql instead. Full argument: scoutingListableSql.
734
737
  async function listScoutingRollup({ limit = 100, pool = defaultPool } = {}) {
735
738
  const { rows } = await pool.query(
736
739
  `WITH agg AS (
@@ -744,7 +747,7 @@ async function listScoutingRollup({ limit = 100, pool = defaultPool } = {}) {
744
747
  WHERE EXISTS (
745
748
  SELECT 1 FROM platform_identity_accounts pa
746
749
  WHERE pa.github_id = a.github_id
747
- AND ${rollupVisibleSql('pa')})
750
+ AND ${scoutingListableSql('pa')})
748
751
  GROUP BY a.github_id
749
752
  ), disc AS (
750
753
  SELECT x.github_id, array_agg(DISTINCT x.d) AS disciplines
@@ -1,10 +1,20 @@
1
- // modules/platform-identity/routes/scouting.js — the INTERNAL cross-project
2
- // scouting view (ADR 0141 §4, task #1002114; Metic floor per ADR 0157). Ranks
3
- // builders across ALL hosted projects from platform_identity_builder_project_activity.
4
- // A talent tool for the platform operator — NOT authority (the rollup is
5
- // descriptive; authority stays local, ADR 0141 / ADR 0016). Consent is enforced
6
- // IN listScoutingRollup itself (ADR 0171 D2/D3): only profile_state='active',
7
- // non-hidden builders appear, so this surface honors hide by construction.
1
+ // modules/platform-identity/routes/scouting.js — the cross-project RECRUITING
2
+ // DIRECTORY (ADR 0141 §4, task 1002114; graduated to project owners by ADR 0210,
3
+ // task 1002290). Ranks builders across ALL hosted projects from
4
+ // platform_identity_builder_project_activity. A talent tool — NOT authority (the
5
+ // rollup is descriptive; authority stays local, ADR 0141 / ADR 0016).
6
+ //
7
+ // WHO READS IT: a hub operator holding `project.curate` (floor metic since ADR
8
+ // 0157) OR a project owner — someone whose own project reported them as its
9
+ // owner at check-in and whose sign-in the hub itself witnessed. That OR lives in
10
+ // ../scouting-authz.js, which carries the full argument for why reading an
11
+ // instance-asserted ownership claim is not the ADR 0201 D3 inversion.
12
+ //
13
+ // WHO APPEARS IN IT: consent is enforced IN listScoutingRollup itself, by the
14
+ // shared scoutingListableSql predicate — active, publicly visible, not hidden,
15
+ // and not opted out of recruiting (ADR 0171 D2/D3 + privacy spec D1). So this
16
+ // surface honors both the hide flag and the recruiting opt-out by construction,
17
+ // and widening WHO may read it could not widen WHAT is shown.
8
18
  //
9
19
  // Reaches core only through the doorway (ADR 0083).
10
20
  'use strict';
@@ -12,25 +22,55 @@
12
22
  const express = require('express');
13
23
  const api = require('../../../src/module-api');
14
24
  const pi = require('../platform-identity');
25
+ const scoutingAuthz = require('../scouting-authz');
15
26
 
16
- const { pool, requireBuilder, requirePermission, parsePagination } = api;
27
+ const { pool, requireBuilder, parsePagination } = api;
28
+
29
+ // Resolved at CALL time, never at load. Proofs that exercise this route pin a
30
+ // PARTIAL doorway (tests/profile_route.mjs stubs pool + the two gates and
31
+ // nothing else), and a module-scope api.logger() would make this file
32
+ // unrequireable there — a logging line is not worth breaking a privacy proof
33
+ // over. my-projects.js reaches the doorway logger the same lazy way.
34
+ let resolvedLog = null;
35
+ const log = {
36
+ error: (msg) => {
37
+ if (!resolvedLog) resolvedLog = typeof api.logger === 'function' ? api.logger('platform-identity') : { error: () => {} };
38
+ resolvedLog.error(msg);
39
+ },
40
+ };
17
41
 
18
42
  module.exports = function scoutingRoutes() {
19
43
  const router = express.Router();
20
44
 
21
- // rank: archon — the internal talent directory across every project. The Archon
22
- // gate here is the load-bearing protection ("NOT shown to builders"): a non-Archon
23
- // is 403'd before any cross-builder data is read.
24
- router.get('/scouting', requireBuilder, requirePermission('project.curate'), async (req, res) => {
25
- const { limit } = parsePagination(req, { defaultLimit: 100, maxLimit: 500 });
26
- try {
27
- const builders = await pi.listScoutingRollup({ limit, pool });
28
- res.json({ builders });
29
- } catch (err) {
30
- console.error('[platform-identity] GET /scouting', err);
31
- res.fail('scouting_failed', { status: 500, message: 'internal error' });
32
- }
33
- });
45
+ // The atom is spelled in the middleware list below, not hidden inside
46
+ // requireScoutingReader, so route-rank-check.js and gen-api-docs.js classify
47
+ // this route from its real gate. A helper-hidden gate would record a
48
+ // privileged route as any-builder — wrong in the dangerous direction.
49
+
50
+ // rank: metic+archon — the internal talent directory across every project.
51
+ // Read by a `project.curate` holder OR a project owner (ADR 0210, task 1002290);
52
+ // a caller who is neither gets the unchanged permission refusal before any
53
+ // cross-builder data is read.
54
+ router.get(
55
+ '/scouting',
56
+ requireBuilder,
57
+ // Ownership is asked FIRST — one indexed LIMIT-1 lookup, paid by the operator
58
+ // path too. The alternative is asking the permission gate first, and it is not
59
+ // available: a middleware that has denied has already SENT its response, so
60
+ // there is nothing to fall through from. Paying a round-trip is what keeps the
61
+ // 403 byte-identical to the one this route always produced (ADR 0210 D2).
62
+ scoutingAuthz.requireScoutingReader({ curateGate: api.requirePermission('project.curate'), pool, log }),
63
+ async (req, res) => {
64
+ const { limit } = parsePagination(req, { defaultLimit: 100, maxLimit: 500 });
65
+ try {
66
+ const builders = await pi.listScoutingRollup({ limit, pool });
67
+ res.json({ builders });
68
+ } catch (err) {
69
+ log.error(`GET /scouting failed: ${err && err.message}`);
70
+ res.fail('scouting_failed', { status: 500, message: 'internal error' });
71
+ }
72
+ },
73
+ );
34
74
 
35
75
  return router;
36
76
  };
@@ -17,6 +17,7 @@
17
17
  const express = require('express');
18
18
  const api = require('../../../src/module-api');
19
19
  const pi = require('../platform-identity');
20
+ const scoutingAuthz = require('../scouting-authz');
20
21
  const hubKeys = require('../hub-keys');
21
22
  const hubDevice = require('../hub-device');
22
23
  // task 1003208: structured logging (pino via the doorway) — was console.*.
@@ -114,6 +115,11 @@ function registerSeams() {
114
115
  // THROWS on a bad read by design — an empty set means "nobody is hidden",
115
116
  // which a database error must never be allowed to say.
116
117
  hiddenGithubIds: (githubIds) => pi.hiddenGithubIds(githubIds, { pool }),
118
+ // task 1002290 (ADR 0210): "does this hub account own a project?", the
119
+ // second audience the /people page gate admits beside `project.curate`.
120
+ // The ownership fact lives in this module, so the answer does too; core
121
+ // asks through the port and treats an absent one as a denial.
122
+ isProjectOwner: (githubId) => scoutingAuthz.hasOwnerMembership(githubId, { pool }),
117
123
  });
118
124
  }
119
125
  }
@@ -0,0 +1,139 @@
1
+ // modules/platform-identity/scouting-authz.js — WHO may read the cross-project
2
+ // recruiting directory (GET /scouting; the hall's /people page renders the same
3
+ // data). ADR 0210, task 1002290 — goal 1000045 C5: "project owners (not only
4
+ // Archons) browse builder profiles to recruit".
5
+ //
6
+ // TWO DOORS, DELIBERATELY DIFFERENT KINDS — the modules/lifecycle/goal-authz.js
7
+ // shape (owner OR rank), which the project-admin-console spec names as the
8
+ // design of record:
9
+ //
10
+ // RANK — `project.curate`, floor metic (ADR 0157 lowered it from archon).
11
+ // The hub OPERATOR's door, unchanged: the atom, the 403 body, the
12
+ // floors map. Nothing about it moves here.
13
+ // OWNERSHIP — an instance-asserted, hub-witnessed `membership_kind = 'owner'`
14
+ // project membership. The PROJECT OWNER's door, and the whole of
15
+ // what this file adds.
16
+ //
17
+ // WHY OWNERSHIP IS SAFE TO READ HERE, WHEN ADR 0201 D3 SAYS A HUB-HELD OWNER
18
+ // LIST IS NEVER A GATE. The thing that ADR rejects is `platform_identity_-
19
+ // projects.owner_github_id`: a hub CATALOG column, written by hub curation, with
20
+ // no instance behind it — consulting it before an ADMISSION would be the hub
21
+ // deciding who may admit on someone else's box, the ADR 0141 inversion.
22
+ // `platform_identity_project_memberships.membership_kind` is a different record
23
+ // with a different provenance: the PROJECT ITSELF reports it over the
24
+ // client_id + client_secret channel at `POST /sso/membership/check-in`
25
+ // (src/bongos/auth-admission.js decides the value from its OWN first-admin
26
+ // config), and `touchMembership` refuses to write it unless the hub has already
27
+ // witnessed that person sign in (`signin_verified_at`, ADR 0205 D1/D2). So this
28
+ // gate reads the instance's own claim about its own archon, mirrored — authority
29
+ // stays local, exactly as ADR 0141 requires. And the act being gated is a HUB
30
+ // READ of the hub's own directory, not an admission on anyone else's box.
31
+ //
32
+ // NOT AUTHORITY, IN EITHER DIRECTION (ADR 0016). Owning a project buys exactly
33
+ // one thing: reading a descriptive, consent-filtered list. It confers no rank, no
34
+ // claim right, no admission power, and it is never consulted by anything else.
35
+ // The reverse also holds and is load-bearing for task 1002290's other half: a
36
+ // builder's recruiting opt-out changes who can SEE them and grants or denies
37
+ // them nothing — the two axes never touch.
38
+ //
39
+ // LIVE GAP, NAMED ON PURPOSE (ADR 0210 §Consequences; blocker filed). "Project
40
+ // owner" per the goal spec is "the registrant plus delegates they grant". Today
41
+ // an instance reports `owner` for its FIRST ADMIN only, so the REGISTRANT half
42
+ // works and DELEGATES do not: a second archon on a project holds `member` on the
43
+ // hub and reaches /scouting only via `project.curate`. Nothing here needs to
44
+ // change when that is fixed — this gate reads whatever the instance reports.
45
+ 'use strict';
46
+
47
+ const pi = require('./platform-identity');
48
+
49
+ // The coarse membership value that means "this person is an owner of this
50
+ // project", in the CURRENT vocabulary (ADR 0177 — `role`/'builder' is the legacy
51
+ // spelling platform_identity_010's trigger keeps in lockstep; 'owner' is
52
+ // unchanged across the rename, so there is no legacy value to also accept).
53
+ const OWNER_MEMBERSHIP_KIND = 'owner';
54
+
55
+ // The one `platform_identity_sso_clients.status` value that means the project is
56
+ // still live on the platform (the other is 'suspended' — migration
57
+ // platform_identity_001_tables.sql). Suspending a client is the operator's
58
+ // revocation lever; see hasOwnerMembership for why this gate answers to it.
59
+ const ACTIVE_CLIENT_STATUS = 'active';
60
+
61
+ // isProjectOwnerMembership(row) — the PURE predicate: is this one membership row
62
+ // an owner membership the hub itself witnessed a sign-in for? Fail-closed on an
63
+ // absent row, and on a row the hub never witnessed — an un-witnessed row is a
64
+ // client's own unproven claim (ADR 0205 D4, the `membershipBacksRollup` rule).
65
+ function isProjectOwnerMembership(membership) {
66
+ return !!membership
67
+ && membership.membership_kind === OWNER_MEMBERSHIP_KIND
68
+ && !!membership.signin_verified_at;
69
+ }
70
+
71
+ // hasOwnerMembership(githubId) — does this hub account own at least one project
72
+ // that is still ACTIVE on the platform? EXISTENCE only: which project, and how
73
+ // many, is not this gate's question, so the query stops at the first row. A
74
+ // non-numeric principal (core's 'system:bfg' shape) answers false rather than
75
+ // throwing a bigint cast mid-request — the isNumericGithubId guard every read in
76
+ // this module carries.
77
+ //
78
+ // THE CLIENT JOIN IS THE REVOCATION DIMENSION, and without it this door could
79
+ // never be shut. A membership row is written once and never expires: nothing
80
+ // ages it out, and the only ways it stops saying `owner` are the instance
81
+ // reporting a lower kind at a later check-in, or the builder leaving of their
82
+ // own accord. Neither is available to the platform operator dealing with a rogue
83
+ // or compromised project. Suspending its client (`setClientStatus`) IS that
84
+ // lever — it is what already stops the project signing anyone in — so the read
85
+ // it derives must answer to it too. Otherwise suspending a project would leave
86
+ // its owner reading every builder on the platform, indefinitely.
87
+ async function hasOwnerMembership(githubId, { pool }) {
88
+ if (!pi.isNumericGithubId(githubId)) return false;
89
+ const { rows } = await pool.query(
90
+ `SELECT m.membership_kind, m.signin_verified_at
91
+ FROM platform_identity_project_memberships m
92
+ JOIN platform_identity_sso_clients c ON c.client_id = m.client_id
93
+ WHERE m.github_id = $1 AND m.membership_kind = $2
94
+ AND m.signin_verified_at IS NOT NULL
95
+ AND c.status = $3
96
+ LIMIT 1`,
97
+ [githubId, OWNER_MEMBERSHIP_KIND, ACTIVE_CLIENT_STATUS],
98
+ );
99
+ return isProjectOwnerMembership(rows[0]);
100
+ }
101
+
102
+ // requireScoutingReader({ curateGate, pool, log }) — the OR gate as one
103
+ // middleware, mounted after requireBuilder. `curateGate` is the ALREADY-BUILT
104
+ // `requirePermission('project.curate')` middleware: the atom is spelled in the
105
+ // route's own middleware list, where route-rank-check.js and gen-api-docs.js can
106
+ // see it, so widening the door cannot make the route's recorded rank read looser
107
+ // than it is.
108
+ //
109
+ // ORDER IS THE DESIGN. The ownership question is asked FIRST and, when it
110
+ // answers yes, the request proceeds without the permission gate ever running.
111
+ // When it answers no — or throws — the request falls through to the UNMODIFIED
112
+ // permission gate, so a caller who is neither owner nor operator gets exactly the
113
+ // 403 this route has always produced: the same code, the same `required`/`held`,
114
+ // the same floors map naming the metic floor. The widening can therefore only
115
+ // ever ADMIT; it cannot change, soften, or swallow a refusal.
116
+ //
117
+ // FAIL-CLOSED ON THE OWNERSHIP QUESTION ITSELF. A database blip while asking
118
+ // "does this person own a project" must not admit, and must not 500 either: it
119
+ // falls to the permission gate, which denies unless the caller holds the atom.
120
+ // The failure mode of the new door is "the old door, unchanged".
121
+ function requireScoutingReader({ curateGate, pool, log }) {
122
+ return async function scoutingReaderGate(req, res, next) {
123
+ try {
124
+ if (await hasOwnerMembership(req.builder && req.builder.github_id, { pool })) return next();
125
+ } catch (err) {
126
+ // Message only — a query error can carry row data (the requirePermission
127
+ // precedent). The request continues to the rank door, which denies.
128
+ log.error(`scouting owner check failed, falling back to the permission gate: ${err && err.message}`);
129
+ }
130
+ return curateGate(req, res, next);
131
+ };
132
+ }
133
+
134
+ module.exports = {
135
+ OWNER_MEMBERSHIP_KIND,
136
+ isProjectOwnerMembership,
137
+ hasOwnerMembership,
138
+ requireScoutingReader,
139
+ };
@@ -125,7 +125,8 @@ const FLOW_RECEIPTS = {
125
125
  teardown: { queued: true, message: 'Teardown queued.' },
126
126
  restart: { queued: true, message: 'Restart queued.' },
127
127
  domain: { recorded: true, queued: true, message: 'Address recorded.' },
128
- provision: { queued: true, message: 'Queued. The platform picks it up from here.' },
128
+ // the wizard's filing (task 1003346): the receipt names the instance the page then polls — Harbor, the fixture's standing-up row — and the address the page promises is the one the server assigned
129
+ provision: { queued: true, message: 'Queued. The platform picks it up from here.', instance: { id: 102, domain: 'mercury.cloudbongos.com' } },
129
130
  settings: { settings: { platform_visibility: 'public', joinability: 'open' }, push: { queued: true, message: 'Saved. The project restarts to apply it.' } },
130
131
  };
131
132
  // a teardown or restart ask is REMEMBERED for the run: the page re-reads the project right
@@ -218,6 +219,7 @@ const server = http.createServer((req, res) => {
218
219
  if (flow) return AUTH ? json(res, 200, flow) : json(res, 401, UNAUTH);
219
220
  }
220
221
  if (rest.startsWith('/github/repo')) return AUTH ? json(res, 200, { repo: { private: false, admin: true } }) : json(res, 401, UNAUTH);
222
+ if (rest.startsWith('/provisioning/slug-available')) return json(res, 200, { available: true }); /* the wizard's handle check (task 1003346): every handle is free on the stub */
221
223
  if (rest === '/auth/logout') return json(res, 200, { ok: true });
222
224
  if (rest.startsWith('/auth/web/start')) { res.writeHead(302, { location: PREFIX }); return res.end(); }
223
225
  if (rest === '/instance') { const b = pages.size ? pages.values().next().value.brand : {}; return json(res, 200, { brand: { productName: (b && b.productName) || '', project: {} }, modules: {} }); }
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.424",
3
+ "version": "1.19.426",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.424",
9
+ "version": "1.19.426",
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.424",
3
+ "version": "1.19.426",
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",
@@ -1386,3 +1386,66 @@ p.sec-p{font-size:var(--fs-lede);font-weight:500;color:var(--ink-soft);line-heig
1386
1386
  @media (prefers-reduced-motion:reduce){
1387
1387
  *,*::before,*::after{animation:none !important;transition:none !important;}
1388
1388
  }
1389
+ /* ═══════════════════════════════════════════════════════════════════════
1390
+ THE CREATE WIZARD, IN THE WORLD (task 1003346; direction record §15, the
1391
+ pick: v1 the rail above, the bongo pair kept). The wizard keeps its markup
1392
+ (the panels, the forms, the ids the driver and the tests pin) and takes the
1393
+ world's grammar by SCOPE: no card ground, the six-step strip drawn as the
1394
+ lifecycle rail, the step title lowercase in the display voice, the doors as
1395
+ hairline pills with the accent only on the primary, the GitHub doors
1396
+ inverted. Its own structures (the hairline fields, the choices as cells of
1397
+ one band, the review as a ledger, the human steps, the standup rail) live
1398
+ in projects.html's wizard block. Dark only (ADR 0204); every colour a
1399
+ token; halos are outlines, never box-shadow.
1400
+ ═══════════════════════════════════════════════════════════════════════ */
1401
+ #view-new .wrap{max-width:1180px;}
1402
+ #view-new .card{background:none;border-radius:0;box-shadow:none;padding:34px 0 60px;margin:0;}
1403
+ /* the six-step strip as the rail: six hairlines, a 7px dot, one lit step on the
1404
+ accent with its halo, the number in the display voice, the name in the label
1405
+ face; a step already answered (role=button) stays a way back */
1406
+ #view-new .steps{display:grid;grid-template-columns:repeat(6,1fr);gap:0 10px;align-items:start;margin:0 0 40px;}
1407
+ #view-new .steps li{display:block;position:relative;border-top:1px solid var(--rule);padding:12px 0 0;gap:0;}
1408
+ #view-new .steps li::before{content:"";position:absolute;top:-4px;left:0;right:auto;width:7px;height:7px;margin:0;border-radius:50%;border:1px solid var(--rule);background:var(--bg);}
1409
+ #view-new .steps .snum{display:block;width:auto;height:auto;border:0;border-radius:0;background:none;font-family:var(--font-display);font-weight:300;font-size:22px;letter-spacing:-.02em;line-height:1.1;color:var(--ink-faint);margin-bottom:2px;}
1410
+ #view-new .steps .slbl{display:block;font-family:var(--label-face);font-size:var(--label-size);font-weight:400;letter-spacing:var(--label-space);text-transform:uppercase;line-height:1.3;color:var(--ink-faint);}
1411
+ #view-new .steps li.done{border-top-color:var(--ink-soft);}
1412
+ #view-new .steps li.done::before{background:var(--ink-soft);border-color:var(--ink-soft);}
1413
+ #view-new .steps li.done .snum,#view-new .steps li.done .slbl{color:var(--ink-soft);background:none;}
1414
+ #view-new .steps li.now{border-top-color:var(--accent);}
1415
+ #view-new .steps li.now::before{background:var(--accent);border-color:var(--accent);outline:4px solid color-mix(in srgb,var(--accent) 22%,transparent);}
1416
+ #view-new .steps li.now .snum,#view-new .steps li.now .slbl{color:var(--ink);}
1417
+ #view-new .steps li[role=button]:hover .slbl{color:var(--ink);}
1418
+ /* the step: the title lowercase in the display voice, one clause, the fine
1419
+ print in the body face; the bongo pair beside it (the owner's one mark on
1420
+ every surface), its halo gone */
1421
+ #view-new .createSplit{align-items:start;gap:48px;}
1422
+ #view-new .bongoCell{padding-top:60px;}
1423
+ #view-new .bongos .glow{display:none;}
1424
+ #view-new .panel h1{font-weight:300;font-size:clamp(28px,3.4vw,44px);letter-spacing:-.035em;line-height:1.04;text-transform:lowercase;color:var(--ink);text-shadow:none;margin:0 0 10px;max-width:18ch;text-wrap:balance;}
1425
+ #view-new .panel .lede{font-size:15.5px;font-weight:400;line-height:1.5;color:var(--ink-soft);margin:0 0 26px;max-width:62ch;}
1426
+ #view-new .panel .lede strong,#view-new .panel .lede em{color:var(--ink);font-weight:500;font-style:normal;}
1427
+ /* the done clause's link to Your projects clears the 24px hit floor as a box (an inline's rect is its content box) */
1428
+ #view-new .panel .lede a{display:inline-block;padding:3px 0;min-height:24px;line-height:18px;color:var(--ink);font-weight:400;text-decoration:underline;text-underline-offset:3px;}
1429
+ #view-new .finePrint{font-weight:400;font-size:13.5px;line-height:1.5;color:var(--ink-soft);margin-top:18px;max-width:62ch;}
1430
+ #view-new .finePrint a{color:var(--ink);font-weight:400;text-decoration:underline;text-underline-offset:3px;}
1431
+ #view-new .finePrint em{font-style:normal;color:var(--ink);}
1432
+ #view-new .fieldNote{border:0;border-top:1px solid var(--rule);border-bottom:1px solid var(--rule);border-radius:0;text-align:left;font-weight:400;color:var(--ink-soft);}
1433
+ #view-new .linklike{font-weight:400;color:var(--ink-soft);}
1434
+ #view-new .linklike:hover{color:var(--ink);}
1435
+ /* the doors: pills, lowercase, the accent only on the one that acts; back and
1436
+ skip as hairline pills; the GitHub sign-in inverted; no lift, no shadow */
1437
+ #view-new .btn{min-height:44px;padding:0 22px;font-family:var(--font-body);font-weight:500;font-size:15px;text-transform:lowercase;white-space:nowrap;box-shadow:none;transition:border-color .15s ease,background .15s ease,color .15s ease;}
1438
+ #view-new .btn:hover{transform:none;box-shadow:none;background:color-mix(in srgb,var(--accent) 88%,var(--ink));}
1439
+ #view-new .btn:active{transform:none;}
1440
+ #view-new .btn:disabled{background:none;border:1px solid var(--rule-soft);color:var(--ink-faint);}
1441
+ #view-new .btn.ghost{font-weight:400;color:var(--ink);background:none;border:1px solid var(--rule);}
1442
+ #view-new .btn.ghost:hover{border-color:var(--ink-faint);color:var(--ink);background:none;}
1443
+ #view-new .btn.big{width:auto;min-height:48px;font-size:16px;padding:0 26px;}
1444
+ #view-new #wizSignin{background:var(--ink);color:var(--bg);border:1px solid var(--ink);}
1445
+ #view-new #wizSignin:hover{background:color-mix(in srgb,var(--ink) 88%,var(--bg));}
1446
+ /* the error alert: the error ink between two hairlines, no plate */
1447
+ #view-new .alert{background:none;border:0;border-top:1px solid var(--err);border-bottom:1px solid var(--err);border-radius:0;padding:12px 0;font-weight:400;}
1448
+ @media (max-width:900px){
1449
+ #view-new .steps{grid-template-columns:repeat(3,1fr);gap:18px 10px;margin-bottom:30px;}
1450
+ #view-new .steps .snum{font-size:18px;}
1451
+ }