@bongos/core 1.19.420 → 1.19.421
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 +68 -33
- package/docs/copy-inventory.md +282 -281
- package/docs/copy-registry.json +375 -366
- package/docs/design/mocks/hub/mine.html +377 -0
- package/docs/design/projects-hub-direction-v2.md +25 -1
- package/docs/file-map.md +1 -0
- package/docs/module-api-changelog.md +2 -0
- package/modules/hall-ui/public/settings.html +3 -2
- package/modules/hall-ui/public/settings.js +10 -6
- package/modules/platform-identity/account-visibility.js +94 -3
- package/modules/platform-identity/platform-identity.js +45 -50
- package/modules/platform-identity/routes/connections.js +3 -3
- package/modules/ui-design/kit/fixtures/featured-hub.json +1 -0
- package/modules/ui-design/kit/fixtures/provisioning-instances-empty.json +4 -0
- package/modules/ui-design/kit/fixtures/provisioning-instances.json +99 -0
- package/modules/ui-design/kit/lib.js +6 -2
- package/modules/ui-design/kit/probe.js +1 -1
- package/modules/ui-design/kit/render.js +1 -1
- package/modules/ui-design/kit/serve.js +63 -2
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/public-landing/assets/cosmos.css +224 -18
- package/public-landing/projects-empty.states.json +49 -0
- package/public-landing/projects-floor.states.json +63 -0
- package/public-landing/projects.html +112 -51
- package/public-landing/projects.probes.json +32 -7
- package/public-landing/projects.states.json +10 -6
- package/src/module-api.js +1 -1
- package/tests/account_privacy_flags.mjs +17 -10
- package/tests/effective_visibility_predicate.mjs +891 -0
- package/tests/profile_route.mjs +22 -11
- package/tests/projects_hub.mjs +33 -5
- package/tests/projects_hub_pre_uat.mjs +18 -6
- package/tests/ui_design_kit.mjs +32 -2
|
@@ -14,8 +14,13 @@
|
|
|
14
14
|
|
|
15
15
|
const crypto = require('node:crypto');
|
|
16
16
|
const api = require('../../src/module-api');
|
|
17
|
+
// THE effective-visibility predicate (task 1002967, adopting the file ADR 0202
|
|
18
|
+
// D4 named). Every public read below joins it from here; nothing in this file
|
|
19
|
+
// re-derives one (see account-visibility.js).
|
|
20
|
+
const visibility = require('./account-visibility');
|
|
17
21
|
|
|
18
22
|
const { pool: defaultPool } = api;
|
|
23
|
+
const { ACCOUNT_VISIBILITY, accountVisibleSql, rollupVisibleSql, isRollupVisible } = visibility;
|
|
19
24
|
|
|
20
25
|
// Default authorization-code lifetime — deliberately short (~60s). A code is a
|
|
21
26
|
// bearer secret that travels through the browser; a tight TTL bounds the replay
|
|
@@ -717,11 +722,15 @@ async function upsertActivity({ clientId, githubId, credits = 0, tasksShipped =
|
|
|
717
722
|
// disciplines union is computed separately in `disc`. Proven against real SQL by
|
|
718
723
|
// tests/scouting_rollup_fanout.mjs.
|
|
719
724
|
//
|
|
720
|
-
// Consent by construction (ADR 0171 D2/D3
|
|
721
|
-
//
|
|
722
|
-
// live cross-project surfaces, so the
|
|
723
|
-
// caller's memory
|
|
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
734
|
async function listScoutingRollup({ limit = 100, pool = defaultPool } = {}) {
|
|
726
735
|
const { rows } = await pool.query(
|
|
727
736
|
`WITH agg AS (
|
|
@@ -735,8 +744,7 @@ async function listScoutingRollup({ limit = 100, pool = defaultPool } = {}) {
|
|
|
735
744
|
WHERE EXISTS (
|
|
736
745
|
SELECT 1 FROM platform_identity_accounts pa
|
|
737
746
|
WHERE pa.github_id = a.github_id
|
|
738
|
-
AND pa
|
|
739
|
-
AND NOT pa.hide_stats)
|
|
747
|
+
AND ${rollupVisibleSql('pa')})
|
|
740
748
|
GROUP BY a.github_id
|
|
741
749
|
), disc AS (
|
|
742
750
|
SELECT x.github_id, array_agg(DISTINCT x.d) AS disciplines
|
|
@@ -840,20 +848,21 @@ async function getRollupForBuilder(githubId, { pool = defaultPool } = {}) {
|
|
|
840
848
|
};
|
|
841
849
|
}
|
|
842
850
|
|
|
843
|
-
// The PUBLIC read behind the 'platform-identity.hub' port (ADR 0171 D1/D2
|
|
844
|
-
// null unless the account
|
|
845
|
-
//
|
|
846
|
-
//
|
|
847
|
-
//
|
|
848
|
-
//
|
|
851
|
+
// The PUBLIC read behind the 'platform-identity.hub' port (ADR 0171 D1/D2,
|
|
852
|
+
// extended by task 1002967): null unless the account passes the SHARED rollup
|
|
853
|
+
// predicate — exists, consented (profile_state='active'), publicly visible
|
|
854
|
+
// (account_visibility='public') and not hidden (hide_stats). The fail-closed
|
|
855
|
+
// rule every public surface inherits by construction. A null is deliberately
|
|
856
|
+
// indistinguishable between "no account", "provisional", "private" and "hidden"
|
|
857
|
+
// (no partial numbers; a count plus a hidden total is a leak by subtraction).
|
|
849
858
|
async function getPublicProfileExtras(githubId, { pool = defaultPool } = {}) {
|
|
850
859
|
if (!isNumericGithubId(githubId)) return null;
|
|
851
860
|
const { rows } = await pool.query(
|
|
852
|
-
`SELECT profile_state, hide_stats
|
|
861
|
+
`SELECT profile_state, account_visibility, hide_stats
|
|
862
|
+
FROM platform_identity_accounts WHERE github_id = $1`,
|
|
853
863
|
[githubId],
|
|
854
864
|
);
|
|
855
|
-
|
|
856
|
-
if (!acct || acct.profile_state !== 'active' || acct.hide_stats) return null;
|
|
865
|
+
if (!isRollupVisible(rows[0])) return null;
|
|
857
866
|
return getRollupForBuilder(githubId, { pool });
|
|
858
867
|
}
|
|
859
868
|
|
|
@@ -920,32 +929,10 @@ async function setAccountHideStats({ githubId, githubLogin, displayName, avatarU
|
|
|
920
929
|
// Account privacy: visibility + recruiter reach (task 1002966 — privacy spec D1)
|
|
921
930
|
// -------------------------------------------------------------------------
|
|
922
931
|
|
|
923
|
-
//
|
|
924
|
-
//
|
|
925
|
-
//
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
// isRecruiterDiscoverable(account) — THE effective-recruiter-reach predicate
|
|
929
|
-
// (privacy spec D1). One definition, so every reader answers "may a recruiting
|
|
930
|
-
// surface see this account at all?" the same way; task 1002967 adopts this
|
|
931
|
-
// rather than re-deriving it, and nothing else should spell the COALESCE out.
|
|
932
|
-
//
|
|
933
|
-
// The JS twin of COALESCE(recruiter_discoverable, account_visibility='public').
|
|
934
|
-
// The tri-state is the point: NULL = follow visibility (opt-OUT for a public
|
|
935
|
-
// account, opt-IN for a private one), an explicit boolean overrides — an
|
|
936
|
-
// explicit true on a PRIVATE account is exactly the spec's D2 "private,
|
|
937
|
-
// discoverable" row, which is what the recruiter sliver exists to serve.
|
|
938
|
-
//
|
|
939
|
-
// NOT a complete gate on its own. profile_state='active' still floors every
|
|
940
|
-
// recruiter surface (a provisional account is never visible), and hide_stats
|
|
941
|
-
// still empties the sliver down to name-only — both stay the caller's to apply.
|
|
942
|
-
// Fail-closed on a missing/absent account.
|
|
943
|
-
function isRecruiterDiscoverable(account) {
|
|
944
|
-
if (!account) return false;
|
|
945
|
-
const explicit = account.recruiter_discoverable;
|
|
946
|
-
if (typeof explicit === 'boolean') return explicit;
|
|
947
|
-
return account.account_visibility === 'public';
|
|
948
|
-
}
|
|
932
|
+
// ACCOUNT_VISIBILITY and isRecruiterDiscoverable moved to ./account-visibility.js
|
|
933
|
+
// (task 1002967) — every effective-visibility answer now has ONE home, beside
|
|
934
|
+
// the predicate the public reads join. They are re-exported below, so `pi.`
|
|
935
|
+
// callers are unchanged.
|
|
949
936
|
|
|
950
937
|
// The own-scoped privacy WRITE (task 1002966) — callers key this by the
|
|
951
938
|
// AUTHENTICATED session's github_id, never a caller-named one. Validates here,
|
|
@@ -1123,10 +1110,18 @@ async function setAccountHandle({ githubId, handle }, { pool = defaultPool } = {
|
|
|
1123
1110
|
// resolution). Case-insensitive. An exact current-handle match always wins;
|
|
1124
1111
|
// otherwise the single-slot previous_handle matches and answers a move to the
|
|
1125
1112
|
// current handle (the 301 window from P01). Only publicly addressable accounts
|
|
1126
|
-
// resolve
|
|
1127
|
-
// the same
|
|
1128
|
-
//
|
|
1129
|
-
//
|
|
1113
|
+
// resolve — the SHARED accountVisibleSql predicate (task 1002967), so this is
|
|
1114
|
+
// the same "visible" every other public read uses. Provisional, PRIVATE and
|
|
1115
|
+
// unknown are all the same null: the 404 the privacy spec D2 requires, and it is
|
|
1116
|
+
// a 404 to EVERYONE INCLUDING THE OWNER, because this function takes no viewer
|
|
1117
|
+
// and therefore cannot vary by one (public responses stay viewer-independent,
|
|
1118
|
+
// ADR 0171 D4). An owner reaches their own private page through the own-scope
|
|
1119
|
+
// side door instead (GET /me/cross-project → getOwnProfileExtras) — the page
|
|
1120
|
+
// composition varies, the public response never does. A 403 is deliberately
|
|
1121
|
+
// never returned: it would confirm the handle exists.
|
|
1122
|
+
// hide_stats deliberately does NOT gate resolution: hiding removes the rollup,
|
|
1123
|
+
// not the page (D2 — addressability and hide are different invariants), which is
|
|
1124
|
+
// exactly why accountVisibleSql and rollupVisibleSql are two predicates.
|
|
1130
1125
|
// Returns null | { moved_to } | { account: { github_id, github_login,
|
|
1131
1126
|
// display_name, avatar_url, handle, created_at } }.
|
|
1132
1127
|
async function resolveHandle(handle, { pool = defaultPool } = {}) {
|
|
@@ -1136,7 +1131,7 @@ async function resolveHandle(handle, { pool = defaultPool } = {}) {
|
|
|
1136
1131
|
`SELECT github_id, github_login, display_name, avatar_url, handle, bio, links, created_at,
|
|
1137
1132
|
(lower(handle) = $1) AS is_current
|
|
1138
1133
|
FROM platform_identity_accounts
|
|
1139
|
-
WHERE
|
|
1134
|
+
WHERE ${accountVisibleSql()}
|
|
1140
1135
|
AND (lower(handle) = $1 OR lower(previous_handle) = $1)
|
|
1141
1136
|
ORDER BY (lower(handle) = $1) DESC
|
|
1142
1137
|
LIMIT 1`,
|
|
@@ -1467,9 +1462,9 @@ module.exports = {
|
|
|
1467
1462
|
getPublicProfileExtras,
|
|
1468
1463
|
getOwnProfileExtras,
|
|
1469
1464
|
setAccountHideStats,
|
|
1470
|
-
// account privacy: visibility
|
|
1471
|
-
|
|
1472
|
-
|
|
1465
|
+
// account privacy: the effective-visibility predicates (./account-visibility.js
|
|
1466
|
+
// — one home for every effective answer; task 1002967 / ADR 0202 D4, 0207)
|
|
1467
|
+
...visibility,
|
|
1473
1468
|
setAccountPrivacy,
|
|
1474
1469
|
// builder handle (P01)
|
|
1475
1470
|
RESERVED_HANDLES,
|
|
@@ -40,7 +40,7 @@ const express = require('express');
|
|
|
40
40
|
const api = require('../../../src/module-api');
|
|
41
41
|
const pi = require('../platform-identity');
|
|
42
42
|
const conn = require('../connections');
|
|
43
|
-
const { isAccountVisible } = require('../account-visibility');
|
|
43
|
+
const { isAccountVisible, isAccountActive } = require('../account-visibility');
|
|
44
44
|
|
|
45
45
|
const { pool, requireBuilder, validateOrRespond, parsePagination } = api;
|
|
46
46
|
const log = api.logger('platform-identity');
|
|
@@ -64,7 +64,7 @@ async function actingAccount(builder) {
|
|
|
64
64
|
return { fail: ['no_platform_identity', { status: 400, message: 'this session has no platform account' }] };
|
|
65
65
|
}
|
|
66
66
|
const acct = await pi.getAccount(githubId, { pool });
|
|
67
|
-
if (!acct
|
|
67
|
+
if (!isAccountActive(acct)) {
|
|
68
68
|
return { fail: ['profile_provisional', { status: 403, message: 'sign in to the platform once to activate your profile before connecting' }] };
|
|
69
69
|
}
|
|
70
70
|
return { githubId };
|
|
@@ -78,7 +78,7 @@ async function visibleTarget(viewerGithubId, handle) {
|
|
|
78
78
|
const acct = await conn.findAccountByHandle(handle, { pool });
|
|
79
79
|
if (!acct) return null;
|
|
80
80
|
if (isAccountVisible(acct)) return acct;
|
|
81
|
-
if (acct
|
|
81
|
+
if (!isAccountActive(acct)) return null;
|
|
82
82
|
return (await conn.isConnectedTo(viewerGithubId, acct.github_id, { pool })) ? acct : null;
|
|
83
83
|
}
|
|
84
84
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"_":"The hub's catalog (task 1003345): the landing's featured.json plus Tidewater, an all-null row nobody owns — the card-floor state opens ITS card, because Demo is owned by the signed-in stub (provisioning-instances.json) and an owned orb's plain click opens the manage view, never the card. Served under the name \"featured\" by projects.states.json's stub.fixtures alias; the landing keeps featured.json.","projects":[{"origin":"https://amazonprimea.com","name":"Off the Boats","tagline":"A singular shop, and a broader videogame in a persistent ancient Greek world that collides with modern day commerce & products.","art_url":"/assets/world/r2-chrome-blob-one.jpg","type":"game","template":"bead-wash","created_at":"2026-07-15T22:12:57.732Z","builders":11,"tags":["art-pipeline","character-anim","demo","game"],"owner":"lars589","owner_avatar":"https://avatars.githubusercontent.com/u/283020391?v=4","faces":["https://avatars.githubusercontent.com/u/283020391?v=4","https://avatars.githubusercontent.com/u/64553469?v=4","https://avatars.githubusercontent.com/u/290184380?v=4","https://avatars.githubusercontent.com/u/295990394?v=4","https://avatars.githubusercontent.com/u/35337514?v=4"],"shipped_30d":0,"shipped_30d_capped":false,"hall_origin":"https://builders.amazonprimea.com"},{"origin":"https://demo.cloudbongos.com","name":"Demo","tagline":"A live demo instance on Cloud Bongos.","art_url":null,"type":null,"template":"pair-gold-wash","created_at":"2026-07-14T19:16:38.160Z","builders":null,"tags":null,"owner":null,"owner_avatar":null,"faces":null,"shipped_30d":null,"shipped_30d_capped":null,"hall_origin":null},{"origin":"https://emersonian-circles.cloudbongos.com","name":"Emersonian Circles","tagline":"An independent project built on Cloud Bongos.","art_url":null,"type":null,"template":null,"created_at":"2026-07-25T02:21:55.641Z","builders":1,"tags":[],"owner":"lars589","owner_avatar":"https://avatars.githubusercontent.com/u/283020391?v=4","faces":[],"shipped_30d":30,"shipped_30d_capped":true,"hall_origin":null},{"origin":"https://tidewater.cloudbongos.com","name":"Tidewater","tagline":"A project that has declared nothing yet.","art_url":null,"type":null,"template":null,"created_at":"2026-07-20T08:00:00.000Z","builders":null,"tags":null,"owner":null,"owner_avatar":null,"faces":null,"shipped_30d":null,"shipped_30d_capped":null,"hall_origin":null}]}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_": "The owner's projects for the kit (task 1003345): four states the mine list and the manage page render — live, standing up, snagged, torn down. Timestamps written \"$now-3m\" / \"$now-1d\" are resolved by the stub at request time so \"checked 3m ago\" stays true. Mooring (task 1003345, the grade's finding): live but unreachable since its last probe, so the restart door renders and the restart probe has a row to drive.",
|
|
3
|
+
"instances": [
|
|
4
|
+
{
|
|
5
|
+
"id": 101,
|
|
6
|
+
"slug": "demo",
|
|
7
|
+
"status": "active",
|
|
8
|
+
"domain": "demo.cloudbongos.com",
|
|
9
|
+
"target_ref": "builder/demo",
|
|
10
|
+
"hosting_shape": "co-tenant",
|
|
11
|
+
"onboard_mode": "scaffold",
|
|
12
|
+
"created_at": "2026-08-01T10:00:00.000Z",
|
|
13
|
+
"provisioned_at": "2026-08-01T10:20:00.000Z",
|
|
14
|
+
"updated_at": "$now-3m",
|
|
15
|
+
"last_verified_at": "$now-3m",
|
|
16
|
+
"last_verified_up": true,
|
|
17
|
+
"error_note": null,
|
|
18
|
+
"dns_note": null,
|
|
19
|
+
"torn_down_at": null,
|
|
20
|
+
"open_intent": null
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"id": 102,
|
|
24
|
+
"slug": "harbor",
|
|
25
|
+
"status": "provisioning",
|
|
26
|
+
"domain": null,
|
|
27
|
+
"target_ref": "builder/harbor",
|
|
28
|
+
"hosting_shape": "standalone",
|
|
29
|
+
"onboard_mode": "scaffold",
|
|
30
|
+
"created_at": "$now-6m",
|
|
31
|
+
"provisioned_at": null,
|
|
32
|
+
"updated_at": "$now-2m",
|
|
33
|
+
"last_verified_at": null,
|
|
34
|
+
"last_verified_up": null,
|
|
35
|
+
"error_note": null,
|
|
36
|
+
"dns_note": null,
|
|
37
|
+
"torn_down_at": null,
|
|
38
|
+
"open_intent": {
|
|
39
|
+
"action": "standup",
|
|
40
|
+
"state": "running",
|
|
41
|
+
"created_at": "$now-6m"
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"id": 103,
|
|
46
|
+
"slug": "tidepool",
|
|
47
|
+
"status": "error",
|
|
48
|
+
"domain": "tidepool.cloudbongos.com",
|
|
49
|
+
"target_ref": "builder/tidepool",
|
|
50
|
+
"hosting_shape": "co-tenant",
|
|
51
|
+
"onboard_mode": "adopt",
|
|
52
|
+
"created_at": "2026-08-20T09:00:00.000Z",
|
|
53
|
+
"provisioned_at": null,
|
|
54
|
+
"updated_at": "$now-1d",
|
|
55
|
+
"last_verified_at": null,
|
|
56
|
+
"last_verified_up": null,
|
|
57
|
+
"error_note": "The deploy could not read the repository: it is private, and the deploy key only reads public ones.",
|
|
58
|
+
"dns_note": null,
|
|
59
|
+
"torn_down_at": null,
|
|
60
|
+
"open_intent": null
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"id": 104,
|
|
64
|
+
"slug": "old-lantern",
|
|
65
|
+
"status": "torn_down",
|
|
66
|
+
"domain": "old-lantern.cloudbongos.com",
|
|
67
|
+
"target_ref": "builder/old-lantern",
|
|
68
|
+
"hosting_shape": "co-tenant",
|
|
69
|
+
"onboard_mode": "scaffold",
|
|
70
|
+
"created_at": "2026-07-02T09:00:00.000Z",
|
|
71
|
+
"provisioned_at": "2026-07-02T09:30:00.000Z",
|
|
72
|
+
"updated_at": "2026-08-15T12:00:00.000Z",
|
|
73
|
+
"last_verified_at": "2026-08-15T11:50:00.000Z",
|
|
74
|
+
"last_verified_up": true,
|
|
75
|
+
"error_note": null,
|
|
76
|
+
"dns_note": null,
|
|
77
|
+
"torn_down_at": "2026-08-15T12:00:00.000Z",
|
|
78
|
+
"open_intent": null
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
"id": 105,
|
|
82
|
+
"slug": "mooring",
|
|
83
|
+
"status": "active",
|
|
84
|
+
"domain": "mooring.cloudbongos.com",
|
|
85
|
+
"target_ref": "builder/mooring",
|
|
86
|
+
"hosting_shape": "co-tenant",
|
|
87
|
+
"onboard_mode": "scaffold",
|
|
88
|
+
"created_at": "2026-08-10T09:00:00.000Z",
|
|
89
|
+
"provisioned_at": "2026-08-10T09:20:00.000Z",
|
|
90
|
+
"updated_at": "$now-20m",
|
|
91
|
+
"last_verified_at": "$now-20m",
|
|
92
|
+
"last_verified_up": false,
|
|
93
|
+
"error_note": null,
|
|
94
|
+
"dns_note": null,
|
|
95
|
+
"torn_down_at": null,
|
|
96
|
+
"open_intent": null
|
|
97
|
+
}
|
|
98
|
+
]
|
|
99
|
+
}
|
|
@@ -237,6 +237,8 @@ function reducedMotionProblems({ running = [], frames = 0 } = {}) {
|
|
|
237
237
|
// { "page": "/projects", "surface"?: "public-landing", "stub"?: { prefix?, child? },
|
|
238
238
|
// "modes"?: ["dark" | "light", …] (the modes the page renders; default both — a page that pins
|
|
239
239
|
// one mode declares it here), "modeQuery"?: "mode" | false, "ignoreRequests"?: [regex strings], "ignoreConsole"?: [...],
|
|
240
|
+
// stub.fixtures?: { "<fixture name>": "<other fixture file>" } (serve another file under a name — an empty list, a
|
|
241
|
+
// failure shape — for the states in THIS file; task 1003345),
|
|
240
242
|
// "ignoreControls"?: [selectors], "states": { name: { url?, auth?, actions?, expect? } },
|
|
241
243
|
// "reducedMotion"?: { auth?, url?, actions? } }
|
|
242
244
|
function validateStatesFile(json) {
|
|
@@ -269,10 +271,11 @@ function validateStatesFile(json) {
|
|
|
269
271
|
}
|
|
270
272
|
function validateStub(stub, at) {
|
|
271
273
|
if (stub === undefined) return [];
|
|
272
|
-
if (!stub || typeof stub !== 'object' || Array.isArray(stub)) return [`${at}: an object { prefix?, child? }`];
|
|
274
|
+
if (!stub || typeof stub !== 'object' || Array.isArray(stub)) return [`${at}: an object { prefix?, child?, fixtures? }`];
|
|
273
275
|
const errors = [];
|
|
274
276
|
if (stub.prefix !== undefined && (typeof stub.prefix !== 'string' || !stub.prefix.startsWith('/') || !stub.prefix.endsWith('/'))) errors.push(`${at}.prefix: a URL prefix like "/builders/"`);
|
|
275
277
|
if (stub.child !== undefined && typeof stub.child !== 'boolean') errors.push(`${at}.child: boolean (serve the child-instance shape)`);
|
|
278
|
+
if (stub.fixtures !== undefined && !(stub.fixtures && typeof stub.fixtures === 'object' && !Array.isArray(stub.fixtures) && Object.values(stub.fixtures).every((v) => typeof v === 'string' && /^[a-z0-9-]+$/.test(v)))) errors.push(`${at}.fixtures: an object of fixture name → the fixture file to serve under it (kebab-case names)`);
|
|
276
279
|
return errors;
|
|
277
280
|
}
|
|
278
281
|
|
|
@@ -339,11 +342,12 @@ function freePort() {
|
|
|
339
342
|
s.listen(0, '127.0.0.1', () => { const p = s.address().port; s.close(() => resolve(p)); });
|
|
340
343
|
});
|
|
341
344
|
}
|
|
342
|
-
async function startStub({ surface, auth = false, port, prefix, child = false } = {}) {
|
|
345
|
+
async function startStub({ surface, auth = false, port, prefix, child = false, fixtures } = {}) {
|
|
343
346
|
const p = port || await freePort();
|
|
344
347
|
const env = { ...process.env, STUB_PORT: String(p), AUTH: auth ? '1' : '0', CHILD: child ? '1' : '0' };
|
|
345
348
|
if (surface) env.SURFACE = surface;
|
|
346
349
|
if (prefix) env.PREFIX = prefix;
|
|
350
|
+
if (fixtures) env.FIXTURE_ALIAS = JSON.stringify(fixtures);
|
|
347
351
|
const proc = spawn(process.execPath, [path.join(KIT_DIR, 'serve.js')], { env, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
348
352
|
await new Promise((resolve, reject) => {
|
|
349
353
|
let out = '';
|
|
@@ -52,7 +52,7 @@ async function main() {
|
|
|
52
52
|
|
|
53
53
|
const stubs = [];
|
|
54
54
|
let baseOut = lib.argOf(args, '--base-out', null), baseIn = lib.argOf(args, '--base-in', null);
|
|
55
|
-
const stubOpts = { surface, prefix: json.stub && json.stub.prefix, child: json.stub && json.stub.child };
|
|
55
|
+
const stubOpts = { surface, prefix: json.stub && json.stub.prefix, child: json.stub && json.stub.child, fixtures: json.stub && json.stub.fixtures };
|
|
56
56
|
if (!baseOut) { const s = await lib.startStub({ ...stubOpts, auth: false }); stubs.push(s); baseOut = s.base; }
|
|
57
57
|
if (!baseIn) { const s = await lib.startStub({ ...stubOpts, auth: true }); stubs.push(s); baseIn = s.base; }
|
|
58
58
|
const stop = () => stubs.forEach((s) => s.stop());
|
|
@@ -76,7 +76,7 @@ async function main() {
|
|
|
76
76
|
|
|
77
77
|
const stubs = [];
|
|
78
78
|
let baseOut = lib.argOf(args, '--base-out', null), baseIn = lib.argOf(args, '--base-in', null);
|
|
79
|
-
const stubOpts = { surface, prefix: json.stub && json.stub.prefix, child: json.stub && json.stub.child };
|
|
79
|
+
const stubOpts = { surface, prefix: json.stub && json.stub.prefix, child: json.stub && json.stub.child, fixtures: json.stub && json.stub.fixtures };
|
|
80
80
|
if (!baseOut) { const s = await lib.startStub({ ...stubOpts, auth: false }); stubs.push(s); baseOut = s.base; }
|
|
81
81
|
if (!baseIn) { const s = await lib.startStub({ ...stubOpts, auth: true }); stubs.push(s); baseIn = s.base; }
|
|
82
82
|
const stop = () => stubs.forEach((s) => s.stop());
|
|
@@ -40,6 +40,9 @@ const AUTH = process.env.AUTH === '1';
|
|
|
40
40
|
const CHILD = process.env.CHILD === '1';
|
|
41
41
|
const LIVE = process.env.LIVE || '';
|
|
42
42
|
const FAIL = new Set(String(process.env.FAIL || '').split(',').map((s) => s.trim()).filter(Boolean));
|
|
43
|
+
// a states file may point a fixture NAME at another FILE (stub.fixtures, task 1003345) — the
|
|
44
|
+
// way the mine view's empty state is rendered without a second stub shape
|
|
45
|
+
const ALIAS = JSON.parse(process.env.FIXTURE_ALIAS || '{}');
|
|
43
46
|
const FIXTURES = path.join(__dirname, 'fixtures');
|
|
44
47
|
const DOCS = path.join(ROOT, 'docs');
|
|
45
48
|
const TRANSFORM = path.join(ROOT, 'scripts', 'gds', 'ui-kit-transform.js');
|
|
@@ -101,11 +104,59 @@ const UNAUTH = { error: { code: 'unauthenticated', message: 'unauthenticated' }
|
|
|
101
104
|
function json(res, status, body) { res.writeHead(status, { 'content-type': 'application/json', 'cache-control': 'no-store' }); res.end(JSON.stringify(body)); }
|
|
102
105
|
function fixture(name, res, origin) {
|
|
103
106
|
if (FAIL.has(name)) return json(res, 500, { error: { code: 'stub_failure', message: `FAIL=${name}` } });
|
|
104
|
-
const file = path.join(FIXTURES, name + '.json');
|
|
107
|
+
const file = path.join(FIXTURES, (ALIAS[name] || name) + '.json');
|
|
105
108
|
if (!fs.existsSync(file)) return json(res, 404, { error: { code: 'no_fixture', message: name } });
|
|
106
109
|
res.writeHead(200, { 'content-type': 'application/json', 'cache-control': 'no-store' });
|
|
107
110
|
res.end(name === 'featured' ? JSON.stringify(featured(file, origin)) : fs.readFileSync(file));
|
|
108
111
|
}
|
|
112
|
+
// The owner's projects (task 1003345): the list, one project ({ instance, open_intent }: a
|
|
113
|
+
// wrong id answers 404, the route's own shape), and its settings row. "$now-3m" strings are
|
|
114
|
+
// resolved per request so a "live" chip reads "checked 3m ago" on every render.
|
|
115
|
+
function stamp(v) {
|
|
116
|
+
if (Array.isArray(v)) return v.map(stamp);
|
|
117
|
+
if (v && typeof v === 'object') { const o = {}; for (const [k, x] of Object.entries(v)) o[k] = stamp(x); return o; }
|
|
118
|
+
const m = typeof v === 'string' && v.match(/^\$now(?:([+-]\d+)([smhd]))?$/);
|
|
119
|
+
if (!m) return v;
|
|
120
|
+
const unit = { s: 1e3, m: 6e4, h: 36e5, d: 864e5 }[m[2] || 's'];
|
|
121
|
+
return new Date(Date.now() + (m[1] ? Number(m[1]) * unit : 0)).toISOString();
|
|
122
|
+
}
|
|
123
|
+
// the receipt each ask gets: the message the page shows verbatim, so a probe can pin it
|
|
124
|
+
const FLOW_RECEIPTS = {
|
|
125
|
+
teardown: { queued: true, message: 'Teardown queued.' },
|
|
126
|
+
restart: { queued: true, message: 'Restart queued.' },
|
|
127
|
+
domain: { recorded: true, queued: true, message: 'Address recorded.' },
|
|
128
|
+
provision: { queued: true, message: 'Queued. The platform picks it up from here.' },
|
|
129
|
+
settings: { settings: { platform_visibility: 'public', joinability: 'open' }, push: { queued: true, message: 'Saved. The project restarts to apply it.' } },
|
|
130
|
+
};
|
|
131
|
+
// a teardown or restart ask is REMEMBERED for the run: the page re-reads the project right
|
|
132
|
+
// after a receipt (doDelete → loadManage), and what it must then see is the queued intent —
|
|
133
|
+
// the same open_intent shape the route stamps — not the untouched fixture row
|
|
134
|
+
const ASKED = new Map();
|
|
135
|
+
function flowReceipt(rest, method) {
|
|
136
|
+
if (method === 'POST' && rest === '/provisioning/instances') return FLOW_RECEIPTS.provision;
|
|
137
|
+
const m = rest.match(/^\/provisioning\/instances\/(\d+)\/(teardown|restart|domain|settings)$/);
|
|
138
|
+
if (!m) return null;
|
|
139
|
+
if (m[2] === 'settings') return method === 'PATCH' ? FLOW_RECEIPTS.settings : null;
|
|
140
|
+
if (method !== 'POST') return null;
|
|
141
|
+
if (m[2] === 'teardown' || m[2] === 'restart') ASKED.set(m[1], m[2]);
|
|
142
|
+
return FLOW_RECEIPTS[m[2]];
|
|
143
|
+
}
|
|
144
|
+
function withAsk(inst) {
|
|
145
|
+
const action = ASKED.get(String(inst.id));
|
|
146
|
+
return action ? { ...inst, open_intent: { action, state: 'queued', created_at: '$now' } } : inst;
|
|
147
|
+
}
|
|
148
|
+
function instances(res, id, settings) {
|
|
149
|
+
const name = 'provisioning-instances';
|
|
150
|
+
if (FAIL.has(name)) return json(res, 500, { error: { code: 'stub_failure', message: `FAIL=${name}` } });
|
|
151
|
+
const file = path.join(FIXTURES, (ALIAS[name] || name) + '.json');
|
|
152
|
+
if (!fs.existsSync(file)) return json(res, 404, { error: { code: 'no_fixture', message: name } });
|
|
153
|
+
const list = stamp((JSON.parse(fs.readFileSync(file, 'utf8')).instances || []).map(withAsk));
|
|
154
|
+
if (id === null) return json(res, 200, { instances: list });
|
|
155
|
+
const inst = list.find((i) => String(i.id) === String(id));
|
|
156
|
+
if (!inst) return json(res, 404, { error: { code: 'not_found', message: 'no such instance' } });
|
|
157
|
+
if (settings) return json(res, 200, { settings: { platform_visibility: 'public', joinability: 'open' }, options: { platform_visibility: ['public', 'gated'], joinability: ['open', 'apply', 'invite_only'] } });
|
|
158
|
+
return json(res, 200, { instance: inst, open_intent: inst.open_intent || null });
|
|
159
|
+
}
|
|
109
160
|
// A project's art in the featured fixture may be root-relative (the surface's
|
|
110
161
|
// own /assets/…, served by this stub) — the page accepts only an http(s) art
|
|
111
162
|
// URL, so it is absolutised to this origin per request, as the builder's
|
|
@@ -156,7 +207,17 @@ const server = http.createServer((req, res) => {
|
|
|
156
207
|
if (FEEDS[rest]) return LIVE ? proxy(req, res) : fixture(FEEDS[rest], res, origin);
|
|
157
208
|
if (rest === '/me') return AUTH ? json(res, 200, me(origin)) : json(res, 401, UNAUTH);
|
|
158
209
|
if (AUTHED[rest]) return AUTH ? fixture(AUTHED[rest], res) : json(res, 401, UNAUTH);
|
|
159
|
-
if (rest === '/provisioning/instances') return AUTH ?
|
|
210
|
+
if (rest === '/provisioning/instances' && req.method === 'GET') return AUTH ? instances(res, null, false) : json(res, 401, UNAUTH);
|
|
211
|
+
const one = rest.match(/^\/provisioning\/instances\/(\d+)(\/settings)?$/);
|
|
212
|
+
if (one && req.method === 'GET') return AUTH ? instances(res, one[1], !!one[2]) : json(res, 401, UNAUTH);
|
|
213
|
+
// the manage flows (task 1003345): every door on a row and on the manage page only ASKS
|
|
214
|
+
// (ADR 0111) — the stub answers each ask with the route's receipt so the probes can drive
|
|
215
|
+
// attach / restart / teardown / revive / join end to end; nothing changes in the fixture
|
|
216
|
+
if (req.method !== 'GET') {
|
|
217
|
+
const flow = flowReceipt(rest, req.method);
|
|
218
|
+
if (flow) return AUTH ? json(res, 200, flow) : json(res, 401, UNAUTH);
|
|
219
|
+
}
|
|
220
|
+
if (rest.startsWith('/github/repo')) return AUTH ? json(res, 200, { repo: { private: false, admin: true } }) : json(res, 401, UNAUTH);
|
|
160
221
|
if (rest === '/auth/logout') return json(res, 200, { ok: true });
|
|
161
222
|
if (rest.startsWith('/auth/web/start')) { res.writeHead(302, { location: PREFIX }); return res.end(); }
|
|
162
223
|
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.
|
|
3
|
+
"version": "1.19.421",
|
|
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.421",
|
|
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.421",
|
|
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",
|