@bongos/core 1.19.537 → 1.19.539
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 +52 -27
- package/docs/adr/0248-a-hub-project-s-owner-is-the-provisioning-row-carried-on-a-port.md +144 -0
- package/docs/adr/0249-the-invite-step-is-the-done-panel-s-first-act-not-a-rail-step.md +115 -0
- package/docs/adr/README.md +3 -0
- package/docs/api/openapi.json +1 -1
- package/docs/api-reference.md +1 -1
- package/docs/copy-inventory.md +131 -123
- package/docs/copy-registry.json +244 -160
- package/docs/module-api-changelog.md +4 -0
- package/modules/platform-identity/invite-authz.js +117 -0
- package/modules/platform-identity/routes/projects.js +49 -22
- package/modules/provisioning/module.json +1 -1
- package/modules/provisioning/provisioning.js +38 -1
- package/modules/provisioning/routes/provisioning.js +6 -0
- package/modules/public-landing/public/assets/cosmos.css +18 -0
- package/modules/public-landing/public/projects.html +220 -1
- package/modules/public-landing/public/projects.states.json +1 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/src/module-api.js +1 -1
- package/tests/project_catalog_routes.mjs +22 -0
- package/tests/project_invite_ownership.mjs +366 -0
- package/tests/project_invite_ui.mjs +502 -0
- package/tests/projects_hub.mjs +1 -1
- package/tests/projects_hub_pre_uat.mjs +2 -2
|
@@ -1523,5 +1523,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
1523
1523
|
landed since 1.19.535 with no explicit bump. run 33888048777. (task 1002620)
|
|
1524
1524
|
1.19.537 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1525
1525
|
landed since 1.19.536 with no explicit bump. run 33888812605. (task 1002620)
|
|
1526
|
+
1.19.538 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1527
|
+
landed since 1.19.537 with no explicit bump. run 33890191596. (task 1002620)
|
|
1528
|
+
1.19.539 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1529
|
+
landed since 1.19.538 with no explicit bump. run 33892948885. (task 1002620)
|
|
1526
1530
|
---------------------------------------------------------------------------
|
|
1527
1531
|
```
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// modules/platform-identity/invite-authz.js — WHO may invite a builder into a project
|
|
2
|
+
// (POST /projects/invite). Task 1003580 GAP 2, goal 1000046 criterion C6, unblocking the
|
|
3
|
+
// wizard's invite step (BV1.R27, task 1002346).
|
|
4
|
+
//
|
|
5
|
+
// TWO DOORS, DELIBERATELY DIFFERENT KINDS — the scouting-authz.js shape (ADR 0210), which is
|
|
6
|
+
// this codebase's design of record for owner-or-rank:
|
|
7
|
+
//
|
|
8
|
+
// RANK — `project.curate`, floor metic (ADR 0157). The hub CURATOR's door, unchanged:
|
|
9
|
+
// the atom, the 403 body, the floors map. Nothing about it moves here.
|
|
10
|
+
// OWNERSHIP — the project's own owner, for their own project. The whole of what this adds.
|
|
11
|
+
//
|
|
12
|
+
// WHY THE RANK DOOR ALONE WAS A DEAD END. The creation wizard's audience is any signed-in
|
|
13
|
+
// GitHub user — typically a `xenos` on the hub — and `requirePermission` is purely
|
|
14
|
+
// grant/rank-based with no own-scope branch. So the person who created a project got 403 on
|
|
15
|
+
// inviting anyone to it, and no floor low enough to admit them excludes anybody at all.
|
|
16
|
+
//
|
|
17
|
+
// WHERE THE OWNERSHIP PROOF COMES FROM, AND WHERE IT MUST NOT. `platform_identity_sso_clients
|
|
18
|
+
// .owner_github_id` and `platform_identity_projects.owner_github_id` are ATTRIBUTION, not
|
|
19
|
+
// authority — their own migration says so and ADR 0238 restates it. The authority is
|
|
20
|
+
// `provisioning_instances.owner_builder_id`, the fact the manage view already trusts and
|
|
21
|
+
// `instanceAccessDenied` already enforces. This module may not import provisioning (ADR 0083's
|
|
22
|
+
// one-way rule), so the fact travels on the kernel port `provisioning.projectOwnedBy` —
|
|
23
|
+
// resolveOptional and NOT declared in `consumes` (the `provisioning.projectPlanets`
|
|
24
|
+
// precedent): provisioning is default-off, and a hub running without it simply has no
|
|
25
|
+
// control-plane project to own.
|
|
26
|
+
//
|
|
27
|
+
// THE REFUSAL IS NOT AN ORACLE, IN BODY OR IN LATENCY. Ownership is asked before the target is
|
|
28
|
+
// resolved and before the body validator, and every path does the same two reads, so a
|
|
29
|
+
// stranger's 403 is identical — and identically slow — for a real project, a stealth one
|
|
30
|
+
// (ADR 0182 D2a), one that does not exist, and a malformed body.
|
|
31
|
+
//
|
|
32
|
+
// THE PORT IS ASKED ABOUT BOTH HALVES OF THE PROJECTION. ADR 0246 puts a control-plane project
|
|
33
|
+
// on the map as `client_id = slug` at `origin = https://<domain>`, so the question is "do you
|
|
34
|
+
// own the instance carrying BOTH", never "do you own the slug". A hand-seeded catalog row's
|
|
35
|
+
// name is squattable in `provisioning_instances` — the catalog's own claim rule already
|
|
36
|
+
// refuses to overwrite such a row, and this arm answers to the same binding rather than
|
|
37
|
+
// handing its owner's name to whoever registers an instance called that.
|
|
38
|
+
//
|
|
39
|
+
// IT GRANTS NOTHING ELSE (ADR 0016). Owning a project buys exactly one act on the hub:
|
|
40
|
+
// pre-seeding a `pending` membership on your own project. It confers no rank, no curation, no
|
|
41
|
+
// read of anyone else's project, and nothing else consults it.
|
|
42
|
+
// Proof: ../../tests/project_invite_ownership.mjs
|
|
43
|
+
'use strict';
|
|
44
|
+
|
|
45
|
+
const api = require('../../src/module-api');
|
|
46
|
+
const pi = require('./platform-identity');
|
|
47
|
+
|
|
48
|
+
// The kernel port the ownership fact travels on. Resolved BY NAME at call time through the
|
|
49
|
+
// doorway, never imported — provisioning is a sibling module (ADR 0083).
|
|
50
|
+
const OWNERSHIP_PORT = 'provisioning.projectOwnedBy';
|
|
51
|
+
|
|
52
|
+
// The host a catalog origin names, which is the `provisioning_instances.domain` the projection
|
|
53
|
+
// was keyed on. An unparseable origin has no host and therefore no owner. PURE.
|
|
54
|
+
function hostOfOrigin(origin) {
|
|
55
|
+
try { return new URL(String(origin)).hostname.toLowerCase(); } catch { return ''; }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// callerOwnsProject(req, { pool }) — is this caller the owner of the control-plane project the
|
|
59
|
+
// requested catalog row was projected from? Fail-closed on every unhappy path: a non-string
|
|
60
|
+
// key (only a string names a project — `String(['x'])` must not become a claim), an
|
|
61
|
+
// unauthenticated caller, no provisioning module.
|
|
62
|
+
//
|
|
63
|
+
// SAME WORK ON EVERY PATH, which is the timing half of "the refusal is not an oracle". An
|
|
64
|
+
// unknown client is asked about anyway — `hostOfOrigin(null)` is the empty host, which
|
|
65
|
+
// `projectOwnedBy` still queries and which matches nothing — so a stranger cannot separate
|
|
66
|
+
// "this project exists" from "it does not" by measuring how long the identical 403 took to
|
|
67
|
+
// arrive. A body-identical refusal that is measurably faster is still an oracle.
|
|
68
|
+
//
|
|
69
|
+
// The resolved row rides forward on `req.inviteTargetClient` (the `req.builder` /
|
|
70
|
+
// `req.rawPermissionKeys` precedent) so equalising the lookup does not duplicate it: the
|
|
71
|
+
// handler needs this same row, and re-reading it would be pure waste on every admitted call.
|
|
72
|
+
async function callerOwnsProject(req, { pool }) {
|
|
73
|
+
const clientId = req.body && req.body.client_id;
|
|
74
|
+
if (typeof clientId !== 'string' || clientId === '') return false;
|
|
75
|
+
const builderId = req.builder && req.builder.id;
|
|
76
|
+
if (!builderId) return false;
|
|
77
|
+
const ownedBy = api.resolveOptional(OWNERSHIP_PORT);
|
|
78
|
+
if (typeof ownedBy !== 'function') return false;
|
|
79
|
+
const client = await pi.getClient(clientId, { pool });
|
|
80
|
+
if (client) req.inviteTargetClient = client;
|
|
81
|
+
return (await ownedBy({ slug: clientId, host: hostOfOrigin(client && client.origin), builderId })) === true;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// requireProjectInviter({ curateGate, pool, log }) — the OR gate as one middleware, mounted
|
|
85
|
+
// after requireBuilder. `curateGate` is the ALREADY-BUILT `requirePermission('project.curate')`
|
|
86
|
+
// middleware: the atom stays spelled in the route's own middleware list, where
|
|
87
|
+
// route-rank-check.js and gen-api-docs.js can see it, so widening the door cannot make the
|
|
88
|
+
// route's recorded rank read looser than it is.
|
|
89
|
+
//
|
|
90
|
+
// ORDER IS THE DESIGN, and it is what keeps the refusal from becoming an existence oracle.
|
|
91
|
+
// Ownership is asked FIRST; when it answers yes the request proceeds and the permission gate
|
|
92
|
+
// never runs. When it answers no — for ANY reason, including "no such project" — the request
|
|
93
|
+
// falls through to the UNMODIFIED permission gate, so a caller who is neither owner nor
|
|
94
|
+
// curator gets exactly the 403 this route has always produced: same code, same required/held,
|
|
95
|
+
// same floors map. A stranger therefore cannot tell a stealth project (ADR 0182 D2a) from one
|
|
96
|
+
// that does not exist, and cannot tell either from a malformed body — the gate still runs
|
|
97
|
+
// before the body validator, exactly as `requirePermission` always did. The widening can only
|
|
98
|
+
// ever ADMIT; it cannot change, soften, or swallow a refusal.
|
|
99
|
+
//
|
|
100
|
+
// FAIL-CLOSED ON THE OWNERSHIP QUESTION ITSELF. A database blip while asking "is this your
|
|
101
|
+
// project" must not admit, and must not 500 either: it falls to the permission gate, which
|
|
102
|
+
// denies unless the caller holds the atom. The failure mode of the new door is the old door.
|
|
103
|
+
function requireProjectInviter({ curateGate, pool, log }) {
|
|
104
|
+
return async function projectInviterGate(req, res, next) {
|
|
105
|
+
try {
|
|
106
|
+
if (await callerOwnsProject(req, { pool })) return next();
|
|
107
|
+
} catch (err) {
|
|
108
|
+
// Message only — a query error can carry row data (the requirePermission precedent).
|
|
109
|
+
log.error({ err: err && err.message }, 'project invite owner check failed — falling back to the permission gate');
|
|
110
|
+
}
|
|
111
|
+
return curateGate(req, res, next);
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
module.exports = {
|
|
116
|
+
requireProjectInviter,
|
|
117
|
+
};
|
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
// GET /projects — Archon: full curation list (+ featured flags, the stored
|
|
13
13
|
// join door, and the DERIVED publish verdict — ADR 0182)
|
|
14
14
|
// POST /projects — Archon: create / update a catalog entry
|
|
15
|
+
// POST /projects/invite — a `project.curate` holder OR the project's OWN owner:
|
|
16
|
+
// pre-seed a 'pending' membership (invite-authz.js)
|
|
15
17
|
// PATCH /projects/:id/featured — Archon: the feature / unfeature control
|
|
16
18
|
//
|
|
17
19
|
// Reaches core only through the doorway (ADR 0083). Archon routes declared first so
|
|
@@ -23,6 +25,7 @@ const api = require('../../../src/module-api');
|
|
|
23
25
|
const pi = require('../platform-identity');
|
|
24
26
|
const projectCatalog = require('../project-catalog');
|
|
25
27
|
const projectStats = require('../project-stats');
|
|
28
|
+
const inviteAuthz = require('../invite-authz');
|
|
26
29
|
|
|
27
30
|
const { pool, requireBuilder, requirePermission, validateOrRespond, parseId, corsPublicGet } = api;
|
|
28
31
|
const log = api.logger('platform-identity');
|
|
@@ -105,28 +108,52 @@ module.exports = function projectsRoutes() {
|
|
|
105
108
|
}
|
|
106
109
|
});
|
|
107
110
|
|
|
108
|
-
//
|
|
109
|
-
//
|
|
110
|
-
//
|
|
111
|
-
//
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
111
|
+
// The atom is spelled in the middleware list below, not hidden inside
|
|
112
|
+
// requireProjectInviter, so route-rank-check.js and gen-api-docs.js classify this
|
|
113
|
+
// route from its real gate. A helper-hidden gate would record a privileged route
|
|
114
|
+
// as any-builder — wrong in the dangerous direction (the ADR 0210 rule).
|
|
115
|
+
|
|
116
|
+
// rank: metic+archon — own-or-metic in effect: invite a builder into a project
|
|
117
|
+
// (ADR 0141 §5, task #1002115), called by a `project.curate` holder OR the project's OWN
|
|
118
|
+
// OWNER (task 1003580; the ownership proof is `provisioning_instances.owner_builder_id`,
|
|
119
|
+
// reached through a port — see invite-authz.js). The wizard's audience is any signed-in
|
|
120
|
+
// GitHub user, typically a xenos who used to get 403 on their own project. A caller who is
|
|
121
|
+
// neither gets the unchanged permission refusal, identical whether or not the project
|
|
122
|
+
// exists. Pre-seeds a 'pending' membership the invitee accepts from My Projects. V1 invites
|
|
123
|
+
// by github_login and requires the invitee to already have a platform account (they build
|
|
124
|
+
// on some project); inviting someone entirely new to the platform (GitHub-id resolution) is
|
|
125
|
+
// a follow-up.
|
|
126
|
+
router.post(
|
|
127
|
+
'/projects/invite',
|
|
128
|
+
requireBuilder,
|
|
129
|
+
// Ownership is asked FIRST — one client read plus one indexed LIMIT-1 lookup, the SAME two
|
|
130
|
+
// on every path so the refusal is identical in latency as well as in body. Asking the
|
|
131
|
+
// permission gate first is not available: a middleware that has denied has already SENT
|
|
132
|
+
// its response, so there is nothing to fall through from. Paying the round-trip is what
|
|
133
|
+
// keeps the 403 byte-identical to the one this route has always produced (ADR 0210 D2).
|
|
134
|
+
// The gate leaves the resolved client on `req.inviteTargetClient` for the handler below.
|
|
135
|
+
inviteAuthz.requireProjectInviter({ curateGate: api.requirePermission('project.curate'), pool, log }),
|
|
136
|
+
async (req, res) => {
|
|
137
|
+
if (validateOrRespond(req, res, {
|
|
138
|
+
client_id: { required: true, type: 'string', maxLength: 200 },
|
|
139
|
+
github_login: { required: true, type: 'string', maxLength: 200 },
|
|
140
|
+
})) return;
|
|
141
|
+
try {
|
|
142
|
+
// The gate already resolved this exact row (it must, to answer the ownership
|
|
143
|
+
// question); reusing it keeps the equalised lookup from becoming a duplicated one.
|
|
144
|
+
// It is only ever absent when the gate found nothing, so the fallback still reads.
|
|
145
|
+
const client = req.inviteTargetClient || await pi.getClient(req.body.client_id, { pool });
|
|
146
|
+
if (!client || client.status !== 'active') return res.fail('unknown_project', 404);
|
|
147
|
+
const account = await pi.getAccountByLogin(req.body.github_login, { pool });
|
|
148
|
+
if (!account) return res.fail('account_not_found', { status: 404, message: 'That builder has no platform account yet — they must sign in somewhere once before they can be invited.' });
|
|
149
|
+
const out = await pi.inviteToProject({ clientId: req.body.client_id, githubId: account.github_id }, { pool });
|
|
150
|
+
res.status(out.invited ? 201 : 200).json(out);
|
|
151
|
+
} catch (err) {
|
|
152
|
+
console.error('[platform-identity] POST /projects/invite', err);
|
|
153
|
+
res.fail('invite_failed', { status: 500, message: 'internal error' });
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
);
|
|
130
157
|
|
|
131
158
|
// rank: archon — the feature / unfeature control (the ADR 0141 §5 curation gate),
|
|
132
159
|
// BOUNDED BY THE PUBLISH GATE (ADR 0182 D3, task 1002335). Featuring ranks among
|
|
@@ -14,6 +14,6 @@
|
|
|
14
14
|
"migrations": true,
|
|
15
15
|
"pollers": ["liveness-sweep"]
|
|
16
16
|
},
|
|
17
|
-
"provides": ["provisioning.instanceState", "provisioning.projectPlanets"],
|
|
17
|
+
"provides": ["provisioning.instanceState", "provisioning.projectPlanets", "provisioning.projectOwnedBy"],
|
|
18
18
|
"consumes": []
|
|
19
19
|
}
|
|
@@ -1180,6 +1180,43 @@ async function instanceStateForSlug(db, slug) {
|
|
|
1180
1180
|
};
|
|
1181
1181
|
}
|
|
1182
1182
|
|
|
1183
|
+
// The `provisioning.projectOwnedBy` port (task 1003580): is THIS builder the owner of the
|
|
1184
|
+
// control-plane project a hub catalog row names? Lets a hub surface own-scope a project it
|
|
1185
|
+
// did not create — without importing this module (ADR 0083) and without promoting the hub's
|
|
1186
|
+
// own `owner_github_id` columns, which are attribution and never authority (ADR 0238). The
|
|
1187
|
+
// fact returned is the one `instanceAccessDenied` already enforces on every owner-facing
|
|
1188
|
+
// route here: `provisioning_instances.owner_builder_id`.
|
|
1189
|
+
//
|
|
1190
|
+
// BOTH HALVES OF THE PROJECTION MUST AGREE, which is why this takes a slug AND a host rather
|
|
1191
|
+
// than a slug alone. ADR 0246 projects an instance onto the map as `client_id = slug` at
|
|
1192
|
+
// `origin = https://<domain>`, so a caller speaks for a catalog row only when they own the
|
|
1193
|
+
// instance carrying BOTH. Matching the slug by itself would let someone who registers an
|
|
1194
|
+
// instance NAMED after a client the control plane never created — a hand-seeded row, whose
|
|
1195
|
+
// origin the catalog's own claim rule already refuses to overwrite — speak for that project.
|
|
1196
|
+
//
|
|
1197
|
+
// A BOOLEAN, not the owner's id: the comparison is authority and belongs on the plane that
|
|
1198
|
+
// holds the row, so no consumer ever handles an ownership fact it could mis-compare. Read-only
|
|
1199
|
+
// and fail-closed on an unidentifiable key or builder id.
|
|
1200
|
+
//
|
|
1201
|
+
// AN ABSENT HOST IS STILL ASKED, and matches nothing — `domain` is never the empty string (it
|
|
1202
|
+
// is a hostname or NULL, and `lower(NULL) = ''` is NULL, i.e. not true). Short-circuiting it
|
|
1203
|
+
// would make the caller's work depend on whether the catalog row exists, and a refusal that is
|
|
1204
|
+
// byte-identical but measurably FASTER is still an existence oracle. The query is the cheap
|
|
1205
|
+
// half of keeping every caller's path the same shape.
|
|
1206
|
+
async function projectOwnedBy(db, { slug, host, builderId } = {}) {
|
|
1207
|
+
const wantSlug = typeof slug === 'string' ? slug : '';
|
|
1208
|
+
const wantHost = typeof host === 'string' ? host.toLowerCase() : '';
|
|
1209
|
+
const owner = Number(builderId);
|
|
1210
|
+
if (!wantSlug || !Number.isInteger(owner) || owner <= 0) return false;
|
|
1211
|
+
const { rows } = await db.query(
|
|
1212
|
+
`SELECT 1 FROM provisioning_instances
|
|
1213
|
+
WHERE slug = $1 AND lower(domain) = $2 AND owner_builder_id = $3
|
|
1214
|
+
LIMIT 1`,
|
|
1215
|
+
[wantSlug, wantHost, owner],
|
|
1216
|
+
);
|
|
1217
|
+
return rows.length > 0;
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1183
1220
|
// The `provisioning.projectPlanets` port (task 1003347): the owners' planet
|
|
1184
1221
|
// choices for a set of HOSTS (the catalog's origins, host-matched — the join the
|
|
1185
1222
|
// hub's sky makes), so platform-identity's public featured feed can carry them
|
|
@@ -1252,7 +1289,7 @@ module.exports = {
|
|
|
1252
1289
|
createInstance, setInstanceStatus, updateInstanceDomain, updateInstanceSettings, updateInstanceDetail,
|
|
1253
1290
|
createInstance, setInstanceStatus, updateInstanceDomain, clearReleasedDomains, updateInstanceSettings,
|
|
1254
1291
|
enqueueIntent, enqueueTeardown, enqueueSettingsApply, enqueueRestart, getOpenIntent, claimNextIntent, resolveIntent,
|
|
1255
|
-
recordEvent, instanceStateForSlug, planetChoicesForHosts, costLedger,
|
|
1292
|
+
recordEvent, instanceStateForSlug, planetChoicesForHosts, projectOwnedBy, costLedger,
|
|
1256
1293
|
// data access — GitHub App Manifest attempts (task 2080)
|
|
1257
1294
|
createOAuthManifest, getOAuthManifestByState, attachManifestCode,
|
|
1258
1295
|
getLatestOAuthManifestForInstance, claimNextOAuthManifest, resolveOAuthManifest,
|
|
@@ -159,10 +159,16 @@ function badModuleSelection(modules) {
|
|
|
159
159
|
// • PROVIDES port `provisioning.projectPlanets` (hosts[]) → { host: { planet_template,
|
|
160
160
|
// planet_accent } } — the owners' planet choices (task 1003347), so the hub's
|
|
161
161
|
// public featured feed can carry them without importing this module.
|
|
162
|
+
// • PROVIDES port `provisioning.projectOwnedBy` ({ slug, host, builderId }) → boolean —
|
|
163
|
+
// the OWNERSHIP fact behind a hub catalog row (task 1003580), so a hub surface can
|
|
164
|
+
// own-scope a project without importing this module and without treating the hub's
|
|
165
|
+
// `owner_github_id` columns as authority (ADR 0238). See projectOwnedBy for why the
|
|
166
|
+
// question names both halves of the ADR 0246 projection.
|
|
162
167
|
function registerProvisioningSeams() {
|
|
163
168
|
if (api.hasProvider('provisioning.instanceState')) return;
|
|
164
169
|
api.registerProvider('provisioning.instanceState', (slug) => provisioning.instanceStateForSlug(pool, slug));
|
|
165
170
|
api.registerProvider('provisioning.projectPlanets', (hosts) => provisioning.planetChoicesForHosts(pool, hosts));
|
|
171
|
+
api.registerProvider('provisioning.projectOwnedBy', (target) => provisioning.projectOwnedBy(pool, target));
|
|
166
172
|
}
|
|
167
173
|
|
|
168
174
|
// Is this project on the public map, and if not, what is it still missing?
|
|
@@ -411,6 +411,16 @@ h1{font-family:var(--font-display);font-weight:700;font-size:clamp(24px,3.4vw,32
|
|
|
411
411
|
task 1003355 moved it to the project's own page, where the section's heading
|
|
412
412
|
is the disclosure and the row it left carries only its two doors. */
|
|
413
413
|
.attachNote{margin:8px 0 0;font-size:13px;line-height:1.45;color:var(--ink-faint);}
|
|
414
|
+
/* invites filed in this session (task 1002346): a hairline ledger under the pair
|
|
415
|
+
above — one line per person, the name in the body voice, what happened next to
|
|
416
|
+
it in the faint ink. Shared by the wizard's step and the project's own page. */
|
|
417
|
+
.inviteList{list-style:none;display:grid;margin:12px 0 0;padding:0;}
|
|
418
|
+
.inviteList:empty{display:none;}
|
|
419
|
+
.inviteList>li{display:grid;grid-template-columns:auto 1fr;gap:2px 10px;padding:10px 0 11px;border-top:1px solid var(--rule-soft);}
|
|
420
|
+
.inviteList>li:first-child{border-top:0;}
|
|
421
|
+
.inviteList>li::before{content:"";grid-column:1;grid-row:1;width:7px;height:7px;margin-top:7px;border-radius:50%;border:1px solid var(--accent);background:var(--bg);}
|
|
422
|
+
.inviteList b{grid-column:2;font-family:var(--font-body);font-weight:500;font-size:15px;color:var(--ink);}
|
|
423
|
+
.inviteList small{grid-column:2;font-family:var(--font-body);font-size:13px;font-weight:400;line-height:1.45;color:var(--ink-faint);}
|
|
414
424
|
.attachNote code{font-family:var(--font-mono);}
|
|
415
425
|
.projMsg{font-size:13.5px;font-weight:600;margin-top:10px;min-height:18px;color:var(--ink-soft);line-height:1.5;}
|
|
416
426
|
.projMsg.ok{color:var(--ok);}
|
|
@@ -1516,6 +1526,14 @@ p.sec-p{font-size:var(--fs-lede);font-weight:500;color:var(--ink-soft);line-heig
|
|
|
1516
1526
|
#view-new #wizSignin:hover{background:color-mix(in srgb,var(--ink) 88%,var(--bg));}
|
|
1517
1527
|
/* the error alert: the error ink between two hairlines, no plate */
|
|
1518
1528
|
#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;}
|
|
1529
|
+
/* the invite step (task 1002346): the page's own input+button pair, wearing the
|
|
1530
|
+
wizard's hairline field rather than the plated one — the .attach width has to
|
|
1531
|
+
give way to the row's flex, or #view-new's own width:100% overflows it */
|
|
1532
|
+
#view-new .attach{gap:10px 14px;margin-top:14px;align-items:center;}
|
|
1533
|
+
#view-new .attach input[type=text]{width:auto;flex:1;min-width:220px;}
|
|
1534
|
+
#view-new .inviteBox{margin-top:34px;}
|
|
1535
|
+
#view-new .inviteBox .finePrint{margin-top:10px;}
|
|
1536
|
+
#view-new #wizInviteLater{margin-top:14px;}
|
|
1519
1537
|
/* one fold for the whole wizard: 840 is where the page's bands and ledger stack
|
|
1520
1538
|
(cosmos's createSplit collapse), so the rail folds with them, not before them */
|
|
1521
1539
|
@media (max-width:840px){
|
|
@@ -849,6 +849,27 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
849
849
|
</details>
|
|
850
850
|
</div>
|
|
851
851
|
|
|
852
|
+
<!-- live since task 1002346 (BV1.R27): the way to invite a builder AFTER
|
|
853
|
+
creation. The wizard's invite step is skippable, and — the mModules
|
|
854
|
+
argument beside it — an optional step is a default rather than a dead end
|
|
855
|
+
only if there is a way back. The SAME widget on the SAME route
|
|
856
|
+
(POST /projects/invite), admitted here by the project's own ownership
|
|
857
|
+
(ADR 0248); like its neighbour above, nothing is pushed to the project and
|
|
858
|
+
nothing restarts, so there is no "what your project runs" read-back line. -->
|
|
859
|
+
<div class="card mSec" id="mInvite">
|
|
860
|
+
<h2>Invite a builder</h2>
|
|
861
|
+
<div id="mInv">
|
|
862
|
+
<p class="finePrint">By GitHub username. They get an invitation to accept from their own projects page, and they need to have signed in here at least once.</p>
|
|
863
|
+
<div class="attach">
|
|
864
|
+
<label class="vh" for="mInviteWho">GitHub username</label>
|
|
865
|
+
<input type="text" id="mInviteWho" placeholder="GitHub username" autocomplete="off" autocapitalize="off" spellcheck="false" maxlength="200">
|
|
866
|
+
<button type="button" class="btn small" data-invite-send="mInvite">Invite</button>
|
|
867
|
+
</div>
|
|
868
|
+
<ul class="inviteList" id="mInviteList"></ul>
|
|
869
|
+
<div class="projMsg" id="mInviteMsg" role="status" aria-live="polite"></div>
|
|
870
|
+
</div>
|
|
871
|
+
</div>
|
|
872
|
+
|
|
852
873
|
<!-- the planet picker (task 1003347; direction record §16, v1 the bands): the
|
|
853
874
|
owner's own choice of how the platform draws this project — the sky,
|
|
854
875
|
the list, this page's head and the card read the same row. The pick is
|
|
@@ -1203,7 +1224,29 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
1203
1224
|
<!-- the STABLE live region: the strip inside is rebuilt every 5s tick,
|
|
1204
1225
|
and a live region that is itself replaced never announces -->
|
|
1205
1226
|
<div id="liveProgress" role="status" aria-live="polite"></div>
|
|
1206
|
-
|
|
1227
|
+
<!-- the invite step (task 1002346, BV1.R27) — OPTIONAL, and it stands HERE
|
|
1228
|
+
rather than in the numbered rail because an invite needs a project that
|
|
1229
|
+
already exists: the catalog row a client_id resolves is projected at
|
|
1230
|
+
create (ADR 0246), so before the Create step there is nothing to invite
|
|
1231
|
+
anyone TO. The whole reasoning is ADR 0249. Hidden until the created
|
|
1232
|
+
record carries an id and a slug; the script is the only thing that
|
|
1233
|
+
shows it. -->
|
|
1234
|
+
<div class="inviteBox" id="wizInvite" hidden>
|
|
1235
|
+
<h2 class="stepsHead">Invite builders</h2>
|
|
1236
|
+
<p class="finePrint">Optional — invite people by GitHub username. Each one gets an invitation to accept from their own projects page, and they need to have signed in here at least once. Skip this and invite anyone later; nothing about your project waits on it.</p>
|
|
1237
|
+
<div class="attach">
|
|
1238
|
+
<label class="vh" for="wizInviteWho">GitHub username</label>
|
|
1239
|
+
<input type="text" id="wizInviteWho" placeholder="GitHub username" autocomplete="off" autocapitalize="off" spellcheck="false" maxlength="200">
|
|
1240
|
+
<button type="button" class="btn small" data-invite-send="wizInvite">Invite</button>
|
|
1241
|
+
<button type="button" class="btn small ghost" data-invite-skip="wizInvite">Skip</button>
|
|
1242
|
+
</div>
|
|
1243
|
+
<ul class="inviteList" id="wizInviteList"></ul>
|
|
1244
|
+
<div class="projMsg" id="wizInviteMsg" role="status" aria-live="polite"></div>
|
|
1245
|
+
</div>
|
|
1246
|
+
<!-- the way back, the doneModules idiom: an optional step is a default
|
|
1247
|
+
rather than a dead end only if the panel names where it lives after -->
|
|
1248
|
+
<p class="samePath" id="wizInviteLater"></p>
|
|
1249
|
+
<h2 class="stepsHead" id="doneStepsHead">Your steps — the parts only you can play</h2>
|
|
1207
1250
|
<ol class="humanSteps" id="humanSteps"></ol>
|
|
1208
1251
|
<!-- the modules step was optional (task 1002342, BV1.R23) — one line
|
|
1209
1252
|
saying where it lives afterwards, so a skip is never a dead end -->
|
|
@@ -4098,6 +4141,14 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
4098
4141
|
skippable, so this panel names where it lives afterwards rather than
|
|
4099
4142
|
leaving an owner to go looking for it. */
|
|
4100
4143
|
$('doneModules').innerHTML = 'Add or remove modules later in ' + projectPageLink('the project’s settings') + '.';
|
|
4144
|
+
/* the invite step (task 1002346, BV1.R27). The CREATED RECORD is the proof
|
|
4145
|
+
the widget needs and the only one this panel has: no signed-in identity,
|
|
4146
|
+
no filed instance or no slug ⇒ nothing to invite to, and the step stays
|
|
4147
|
+
hidden. It is deliberately NOT keyed on instanceId, which startProgress
|
|
4148
|
+
sets only AFTER this runs. */
|
|
4149
|
+
var created = createdRecord();
|
|
4150
|
+
setInviteSlug('wizInvite', (me && created && created.id && created.slug) ? created.slug : '');
|
|
4151
|
+
$('wizInviteLater').innerHTML = 'You can invite builders any time from ' + projectPageLink('the project’s own page') + '.';
|
|
4101
4152
|
renderSteps();
|
|
4102
4153
|
}
|
|
4103
4154
|
|
|
@@ -5280,6 +5331,11 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
5280
5331
|
progress survives the tick because only a different project re-renders it */
|
|
5281
5332
|
renderPick(inst);
|
|
5282
5333
|
renderCardPick(inst);
|
|
5334
|
+
/* invite a builder (task 1002346): fetch-free too — the widget needs only the
|
|
5335
|
+
slug, and the API is the wall. This read is own-scoped and 404s for anyone
|
|
5336
|
+
who is not the owner, so reaching renderManage at all IS the ownership
|
|
5337
|
+
answer the widget does not ask for itself. */
|
|
5338
|
+
setInviteSlug('mInvite', inst.slug || '');
|
|
5283
5339
|
/* the page names its project — the heading holds #mName; and in the tab */
|
|
5284
5340
|
document.title = (inst.slug ? inst.slug + ' — ' : '') + BASE_TITLE;
|
|
5285
5341
|
/* the card's own story, on the project's own page (task 1003232): the
|
|
@@ -6692,6 +6748,169 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
6692
6748
|
goToManage(m.getAttribute('data-manage'));
|
|
6693
6749
|
});
|
|
6694
6750
|
|
|
6751
|
+
/* ══ invite a builder (task 1002346, BV1.R27; goal 1000046 criterion C6, ADR 0249)
|
|
6752
|
+
ONE widget, mounted twice: the creation wizard's optional invite step (on the
|
|
6753
|
+
done panel — an invite needs a project that already exists) and the project's
|
|
6754
|
+
own page, so a skipped step is a default rather than a dead end.
|
|
6755
|
+
|
|
6756
|
+
THE ROUTE IS USED AS IT STANDS: POST /projects/invite { client_id, github_login },
|
|
6757
|
+
where client_id IS the project's slug — ADR 0246 projects a control-plane project
|
|
6758
|
+
onto the catalog under its slug — and the project's owner is admitted by ADR 0248's
|
|
6759
|
+
ownership arm. Nothing here is a second gate; the server owns the answer.
|
|
6760
|
+
|
|
6761
|
+
TWO QUESTIONS THIS WIDGET DELIBERATELY DOES NOT ASK: "is anyone signed in" and
|
|
6762
|
+
"is this their project". Each caller already knows, and hands over a slug or
|
|
6763
|
+
nothing — which is the ONE door in (setInviteSlug). A widget that re-derived
|
|
6764
|
+
ownership on the page would be a second, weaker copy of a server rule.
|
|
6765
|
+
|
|
6766
|
+
THE REFUSALS ARE SAID IN THE ROUTE'S OWN VOCABULARY, and the 403 is said
|
|
6767
|
+
CAREFULLY: it is byte-identical whether the project is real, stealth, or absent
|
|
6768
|
+
(ADR 0248), so this page may never word it as "it exists but you may not" — that
|
|
6769
|
+
would hand back the membership oracle the route equalised away.
|
|
6770
|
+
|
|
6771
|
+
IT NEVER BLOCKS CREATION. The project is filed before the step is shown at all;
|
|
6772
|
+
a refusal leaves the box exactly as it was, ready to try again.
|
|
6773
|
+
|
|
6774
|
+
WHAT IT PUBLISHES OF AN INVITEE is the 201 row's own membership_kind and the
|
|
6775
|
+
login the inviter typed — never the github_id the row carries, which belongs to
|
|
6776
|
+
someone who has accepted nothing yet.
|
|
6777
|
+
Proof: ../../../tests/project_invite_ui.mjs ══ */
|
|
6778
|
+
|
|
6779
|
+
var INVITE_MOUNTS = {
|
|
6780
|
+
wizInvite: { who: 'wizInviteWho', list: 'wizInviteList', msg: 'wizInviteMsg', after: 'doneStepsHead' },
|
|
6781
|
+
mInvite: { who: 'mInviteWho', list: 'mInviteList', msg: 'mInviteMsg', after: null }
|
|
6782
|
+
};
|
|
6783
|
+
var inviteSlug = {}; /* mount id → the slug it invites to; '' means no step at all */
|
|
6784
|
+
var inviteRows = {}; /* mount id → the invites filed on THIS project this session */
|
|
6785
|
+
var inviteBusy = {}; /* a send is in flight — the mount is inert until it answers */
|
|
6786
|
+
|
|
6787
|
+
/* setInviteSlug(id, slug) — the only door in. A falsy slug closes the mount, which
|
|
6788
|
+
is how "signed out", "not the owner" and "no project yet" all reach this widget.
|
|
6789
|
+
A DIFFERENT slug starts clean: the pending lines belonged to the project they
|
|
6790
|
+
were filed on, and following them to another one would be a lie. */
|
|
6791
|
+
function setInviteSlug(id, slug) {
|
|
6792
|
+
var m = INVITE_MOUNTS[id];
|
|
6793
|
+
if (!m) return;
|
|
6794
|
+
var next = slug || '';
|
|
6795
|
+
if (inviteSlug[id] !== next) {
|
|
6796
|
+
inviteSlug[id] = next;
|
|
6797
|
+
inviteRows[id] = [];
|
|
6798
|
+
inviteSay(id, '', '');
|
|
6799
|
+
}
|
|
6800
|
+
var box = $(id);
|
|
6801
|
+
if (box) box.hidden = !next;
|
|
6802
|
+
renderInviteRows(id);
|
|
6803
|
+
}
|
|
6804
|
+
|
|
6805
|
+
/* One line per invite this session filed: the login the inviter typed, and the
|
|
6806
|
+
WORD THE ROW ITSELF CARRIES — so a membership_kind other than 'pending' shows
|
|
6807
|
+
up here rather than being papered over as one. */
|
|
6808
|
+
function renderInviteRows(id) {
|
|
6809
|
+
var m = INVITE_MOUNTS[id];
|
|
6810
|
+
var list = m && $(m.list);
|
|
6811
|
+
if (!list) return;
|
|
6812
|
+
list.innerHTML = (inviteRows[id] || []).map(function (r) {
|
|
6813
|
+
var said = r.kind === 'pending' ? 'invited — waiting for them to accept.' : r.kind;
|
|
6814
|
+
return '<li><b>' + esc(r.login) + '</b><small>' + esc(said) + '</small></li>';
|
|
6815
|
+
}).join('');
|
|
6816
|
+
}
|
|
6817
|
+
|
|
6818
|
+
function inviteSay(id, text, cls, isHtml) {
|
|
6819
|
+
var m = INVITE_MOUNTS[id];
|
|
6820
|
+
var el = m && $(m.msg);
|
|
6821
|
+
if (!el) return;
|
|
6822
|
+
el.className = 'projMsg' + (cls ? ' ' + cls : '');
|
|
6823
|
+
if (isHtml) el.innerHTML = text; else el.textContent = text;
|
|
6824
|
+
}
|
|
6825
|
+
|
|
6826
|
+
/* inviteRefusal(code, status, message) — the route's refusals as one sentence
|
|
6827
|
+
each. PURE, so the harness reads it directly. A code this page does not know
|
|
6828
|
+
falls through to the SERVER'S own words rather than an invented reason. */
|
|
6829
|
+
function inviteRefusal(code, status, message) {
|
|
6830
|
+
if (status === 404 && code === 'account_not_found') return 'That builder hasn’t signed in here yet — ask them to sign in once, then invite them.';
|
|
6831
|
+
if (status === 404 && code === 'unknown_project') return 'This project isn’t on the map yet — give it a moment and try again.';
|
|
6832
|
+
/* deliberately says nothing about the project: see the header */
|
|
6833
|
+
if (status === 403) return 'This account can’t invite people here.';
|
|
6834
|
+
return message || 'The invitation couldn’t be sent — try again in a moment.';
|
|
6835
|
+
}
|
|
6836
|
+
|
|
6837
|
+
/* doInvite(id) — one builder, one request. */
|
|
6838
|
+
function doInvite(id) {
|
|
6839
|
+
var m = INVITE_MOUNTS[id];
|
|
6840
|
+
var slug = inviteSlug[id] || '';
|
|
6841
|
+
if (!m || !slug || inviteBusy[id]) return;
|
|
6842
|
+
var field = $(m.who);
|
|
6843
|
+
var login = String((field && field.value) || '').replace(/^\s+|\s+$/g, '');
|
|
6844
|
+
if (!login) {
|
|
6845
|
+
inviteSay(id, 'Type the GitHub username of the person you want to invite.', 'err');
|
|
6846
|
+
if (field && field.focus) field.focus();
|
|
6847
|
+
return;
|
|
6848
|
+
}
|
|
6849
|
+
inviteBusy[id] = true;
|
|
6850
|
+
inviteSay(id, 'Sending the invitation…', '');
|
|
6851
|
+
fetch(API + '/projects/invite', {
|
|
6852
|
+
method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json' },
|
|
6853
|
+
body: JSON.stringify({ client_id: slug, github_login: login })
|
|
6854
|
+
}).then(function (res) {
|
|
6855
|
+
return jsonOrEmpty(res).then(function (data) {
|
|
6856
|
+
inviteBusy[id] = false;
|
|
6857
|
+
if (res.ok) {
|
|
6858
|
+
if (data && data.invited && data.row) {
|
|
6859
|
+
inviteRows[id] = (inviteRows[id] || []).concat([{ login: login, kind: String(data.row.membership_kind || 'pending') }]);
|
|
6860
|
+
renderInviteRows(id);
|
|
6861
|
+
if (field) { field.value = ''; if (field.focus) field.focus(); }
|
|
6862
|
+
inviteSay(id, login + ' is invited — they accept it from their own projects page.', 'ok');
|
|
6863
|
+
return;
|
|
6864
|
+
}
|
|
6865
|
+
/* 200 { invited: false, alreadyMember: true } — not a failure, and not a
|
|
6866
|
+
pending invite either: they are already on the project */
|
|
6867
|
+
if (field) field.value = '';
|
|
6868
|
+
inviteSay(id, login + ' is already on this project.', 'ok');
|
|
6869
|
+
return;
|
|
6870
|
+
}
|
|
6871
|
+
if (res.status === 401) { inviteSay(id, signInAgain(), 'err', true); return; }
|
|
6872
|
+
var e = apiError(data, res.status);
|
|
6873
|
+
inviteSay(id, inviteRefusal(e.code, res.status, e.message), 'err');
|
|
6874
|
+
});
|
|
6875
|
+
}).catch(function () {
|
|
6876
|
+
inviteBusy[id] = false;
|
|
6877
|
+
inviteSay(id, 'Couldn’t reach the platform — check your connection and try again.', 'err');
|
|
6878
|
+
});
|
|
6879
|
+
}
|
|
6880
|
+
|
|
6881
|
+
/* skipInvite(id) — the step steps aside having sent NOTHING. Focus moves on
|
|
6882
|
+
rather than being stranded inside the box that just went away. */
|
|
6883
|
+
function skipInvite(id) {
|
|
6884
|
+
var m = INVITE_MOUNTS[id];
|
|
6885
|
+
if (!m || inviteBusy[id]) return;
|
|
6886
|
+
var box = $(id);
|
|
6887
|
+
if (box) box.hidden = true;
|
|
6888
|
+
inviteSay(id, '', '');
|
|
6889
|
+
var after = m.after && $(m.after);
|
|
6890
|
+
if (after && after.focus) { after.setAttribute('tabindex', '-1'); after.focus(); }
|
|
6891
|
+
}
|
|
6892
|
+
|
|
6893
|
+
/* One delegated pair per mount: the buttons name their own mount, so the two
|
|
6894
|
+
surfaces share every line of behaviour above without sharing state. */
|
|
6895
|
+
Object.keys(INVITE_MOUNTS).forEach(function (id) {
|
|
6896
|
+
var box = $(id);
|
|
6897
|
+
if (box) {
|
|
6898
|
+
box.addEventListener('click', function (ev) {
|
|
6899
|
+
var t = ev.target.closest && ev.target.closest('[data-invite-send],[data-invite-skip]');
|
|
6900
|
+
if (!t) return;
|
|
6901
|
+
if (t.hasAttribute('data-invite-skip')) skipInvite(id); else doInvite(id);
|
|
6902
|
+
});
|
|
6903
|
+
}
|
|
6904
|
+
var field = $(INVITE_MOUNTS[id].who);
|
|
6905
|
+
if (field) {
|
|
6906
|
+
field.addEventListener('keydown', function (ev) {
|
|
6907
|
+
if (ev.key !== 'Enter') return;
|
|
6908
|
+
ev.preventDefault();
|
|
6909
|
+
doInvite(id);
|
|
6910
|
+
});
|
|
6911
|
+
}
|
|
6912
|
+
});
|
|
6913
|
+
|
|
6695
6914
|
/* ── boot: who am I → my instances ────────────────────────────────────── */
|
|
6696
6915
|
|
|
6697
6916
|
function renderWho() {
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
"in-manage-address": { "auth": true, "url": "/projects?view=manage&id=101", "expect": { "visible": ["#mAddress", "#mAddress h2", "#mDom .plain", "#mDomName", "#mDom [data-m-attach]", "#mDomWhat summary"], "count": [["#mDom .attach input[type=text]", 1], ["#mDom .attach button", 1]] } },
|
|
23
23
|
"in-manage-planet": { "auth": true, "url": "/projects?view=manage&id=101", "expect": { "visible": ["#mPlanetPick", "#mPick .pick .pickPreview .worldOrb .disc", "#mPick [data-pick-save]", "#mPick [data-pick-reset]", "#mPickWhat summary"], "count": [["#mPick .pickCells[role=\"radiogroup\"]", 3], ["#mPick .pickCell", 10], ["#mPick .pickCell.on", 3], ["#mPick .pickSw", 5], ["#mPick .pickSw.on", 1]], "attr": [["#mPick .pickPreview .worldOrb", "data-template", "pair-obsidian-tint"], ["#mOrb .worldOrb", "data-template", "pair-obsidian-tint"]], "checked": "#mPick input[name=\"mPickAccent\"][value=\"sea\"]", "text": [["#mPlanet .v small", "your choice."]] } },
|
|
24
24
|
"in-manage-card": { "auth": true, "url": "/projects?view=manage&id=101", "expect": { "visible": ["#mCard", "#mCardPick .pick .pickPreview .cardBand", "#mCardPick [data-card-save]", "#mCardWhat summary"], "count": [["#mCardPick .pickCells[role=\"radiogroup\"]", 1], ["#mCardPick .pickCell", 5], ["#mCardPick .pickCell.on", 1]], "checked": "#mCardPick input[name=\"mCardBackdrop\"][value=\"\"]", "attr": [["#mCardPick .cardBand", "data-own", ""]] } },
|
|
25
|
+
"in-manage-invite": { "auth": true, "url": "/projects?view=manage&id=101", "expect": { "visible": ["#mInvite", "#mInvite h2", "#mInviteWho", "#mInvite [data-invite-send]"] } },
|
|
25
26
|
"in-manage-snag": { "auth": true, "url": "/projects?view=manage&id=103", "expect": { "visible": ["#mMain", "#mErr", "#mActs"], "hidden": ["#mAddress"], "text": [["#mName", "tidepool"]] } },
|
|
26
27
|
"in-manage-gone": { "auth": true, "url": "/projects?view=manage&id=104", "expect": { "visible": ["#mMain", "#mActs"], "hidden": ["#mErr", "#mAddress"], "text": [["#mName", "old-lantern"]] } },
|
|
27
28
|
"in-manage-missing": { "auth": true, "url": "/projects?view=manage&id=999", "expect": { "visible": ["#mMissing"], "hidden": ["#mMain", "#mGate"] } },
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.539",
|
|
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.539",
|
|
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.539",
|
|
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",
|