@bongos/core 1.21.57 → 1.21.59
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 +91 -51
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +6 -0
- package/clients/bongos-client/index.cjs +6 -0
- package/clients/bongos-client/index.d.ts +13 -2
- package/clients/bongos-client/index.mjs +6 -0
- package/docs/adr/0336-a-guilds-record-is-the-sum-of-what-its-members-already-show.md +2 -0
- package/docs/adr/0365-a-guild-engages-a-project-through-each-members-own-door.md +58 -0
- package/docs/adr/README.md +1 -0
- package/docs/api/openapi.json +233 -6
- package/docs/api-reference.md +8 -5
- package/docs/copy-inventory.md +384 -376
- package/docs/copy-registry.json +514 -432
- package/docs/file-map.md +3 -0
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-inventory.json +7 -1
- package/docs/page-readings.json +930 -919
- package/modules/hall-ui/public/goals-page.js +20 -1
- package/modules/hall-ui/public/task-lib.js +13 -0
- package/modules/hall-ui/public/work.js +22 -1
- package/modules/platform-identity/application-echo.js +11 -5
- package/modules/platform-identity/guild-engagements.js +255 -0
- package/modules/platform-identity/guilds.js +3 -1
- package/modules/platform-identity/migrations/platform_identity_030_guild_engagements.sql +64 -0
- package/modules/platform-identity/routes/guilds.js +35 -1
- package/modules/platform-identity/routes/my-projects.js +27 -3
- package/modules/platform-identity/routes/projects.js +44 -0
- package/modules/platform-identity/routes/sso.js +21 -3
- package/modules/public-landing/public/assets/cosmos.css +7 -0
- package/modules/public-landing/public/guild.html +64 -2
- package/modules/public-landing/public/guild.probes.json +13 -3
- package/modules/public-landing/public/guild.states.json +10 -5
- package/modules/public-landing/public/projects.html +252 -16
- package/modules/public-landing/public/projects.probes.json +26 -1
- package/modules/public-landing/public/projects.states.json +4 -0
- package/modules/ui-design/kit/fixtures/me__guild-engagements.json +7 -0
- package/modules/ui-design/kit/serve.js +54 -5
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +12 -0
- package/scripts/gds/run-unit-tests.js +5 -0
- package/src/module-api.js +1 -1
- package/tests/applicant_profile_boundary.mjs +104 -1
- package/tests/application_echo.mjs +91 -5
- package/tests/focus_version_filter_default.mjs +47 -0
- package/tests/guild_engagement.mjs +881 -0
- package/tests/guild_engagement_db.mjs +240 -0
- package/tests/hall_mine_lens.mjs +1 -0
- package/tests/hub_engagement_cards.mjs +307 -0
- package/tests/hub_guild_page.mjs +94 -7
- package/tests/privacy_matrix.mjs +99 -9
- package/tests/project_invite_ui.mjs +187 -3
|
@@ -963,6 +963,11 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
963
963
|
<section id="mineInvites" hidden aria-labelledby="mineInvitesH">
|
|
964
964
|
<h2 id="mineInvitesH">Invitations</h2>
|
|
965
965
|
<ul class="mineRows" id="invitesList"></ul>
|
|
966
|
+
<!-- a guild you are in, engaging a project (task 1002303, R27; ADR 0365):
|
|
967
|
+
"G is applying to P" and "P invited G", from GET /me/guild-engagements.
|
|
968
|
+
Each card's Apply is the ordinary join request with `guild` added, so
|
|
969
|
+
you apply as yourself, never as the guild. -->
|
|
970
|
+
<ul class="mineRows" id="engageList"></ul>
|
|
966
971
|
</section>
|
|
967
972
|
|
|
968
973
|
<section aria-labelledby="mineBuildingH">
|
|
@@ -1281,6 +1286,17 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
1281
1286
|
<ul class="suggList" id="mInviteSugg"></ul>
|
|
1282
1287
|
<ul class="inviteList" id="mInviteList"></ul>
|
|
1283
1288
|
<div class="projMsg" id="mInviteMsg" role="status" aria-live="polite"></div>
|
|
1289
|
+
<!-- invite a guild (task 1002303, R27; ADR 0365 D3): POST /projects/invite-guild
|
|
1290
|
+
{ client_id, guild } records that the project invited the guild and
|
|
1291
|
+
answers one hall hand-off link per member on the guild's public page,
|
|
1292
|
+
drawn as the same lines as a single invite above. -->
|
|
1293
|
+
<p class="finePrint">Or invite a guild, by its name or its /g/ address. Each member gets their own invite, and only the members shown on the guild’s public page are listed.</p>
|
|
1294
|
+
<div class="attach">
|
|
1295
|
+
<label class="vh" for="mInviteGuild">Guild name or address</label>
|
|
1296
|
+
<input type="text" id="mInviteGuild" placeholder="Guild name or /g/ address" autocomplete="off" autocapitalize="off" spellcheck="false" maxlength="200">
|
|
1297
|
+
<button type="button" class="btn small ghost" data-invite-guild="mInvite">Invite the guild</button>
|
|
1298
|
+
</div>
|
|
1299
|
+
<div class="projMsg" id="mInviteGuildMsg" role="status" aria-live="polite"></div>
|
|
1284
1300
|
</div>
|
|
1285
1301
|
</div>
|
|
1286
1302
|
|
|
@@ -3616,10 +3632,9 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
3616
3632
|
hub has no accept route at all. Decline still works; it only clears the
|
|
3617
3633
|
hub's own pending row. */
|
|
3618
3634
|
function renderInvitesList(invites) {
|
|
3619
|
-
var section = $('mineInvites');
|
|
3620
3635
|
var ul = $('invitesList');
|
|
3621
3636
|
ul.textContent = '';
|
|
3622
|
-
if (!invites || !invites.length) {
|
|
3637
|
+
if (!invites || !invites.length) { paintInvitesSection(); return; }
|
|
3623
3638
|
invites.forEach(function (inv) {
|
|
3624
3639
|
var row = mineRow(inv);
|
|
3625
3640
|
var hint = mineEl('p', 'mineRow__hint');
|
|
@@ -3643,21 +3658,151 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
3643
3658
|
decline.addEventListener('click', function () {
|
|
3644
3659
|
postThenRemoveRow(
|
|
3645
3660
|
API + '/my-projects/invites/' + encodeURIComponent(inv.client_id) + '/decline', row, decline, null,
|
|
3646
|
-
|
|
3661
|
+
paintInvitesSection
|
|
3647
3662
|
);
|
|
3648
3663
|
});
|
|
3649
3664
|
actions.appendChild(decline);
|
|
3650
3665
|
row.appendChild(actions);
|
|
3651
3666
|
ul.appendChild(row);
|
|
3652
3667
|
});
|
|
3653
|
-
|
|
3668
|
+
paintInvitesSection();
|
|
3669
|
+
}
|
|
3670
|
+
|
|
3671
|
+
/* the Invitations section holds two lists — the projects' own invites and the
|
|
3672
|
+
guild cards below — and shows while either has a row */
|
|
3673
|
+
function paintInvitesSection() {
|
|
3674
|
+
$('mineInvites').hidden = !$('invitesList').children.length && !$('engageList').children.length;
|
|
3654
3675
|
}
|
|
3655
3676
|
|
|
3656
3677
|
function loadInvites() {
|
|
3657
|
-
return
|
|
3658
|
-
|
|
3659
|
-
|
|
3660
|
-
|
|
3678
|
+
return Promise.all([
|
|
3679
|
+
fetch(API + '/my-projects/invites', { credentials: 'same-origin' })
|
|
3680
|
+
.then(function (res) { return res.ok ? res.json() : { invites: [] }; })
|
|
3681
|
+
.then(function (d) { renderInvitesList(d.invites || []); })
|
|
3682
|
+
.catch(function () { renderInvitesList([]); }),
|
|
3683
|
+
loadEngagements()
|
|
3684
|
+
]);
|
|
3685
|
+
}
|
|
3686
|
+
|
|
3687
|
+
/* ── a guild you are in, engaging a project (task 1002303, R27; ADR 0365) ──
|
|
3688
|
+
GET /me/guild-engagements names what the guilds you are in have going: "G is
|
|
3689
|
+
applying to P" (the guild's owner put it forward) and "P invited G" (the
|
|
3690
|
+
project invited the guild; its hall sends each member on the guild's public
|
|
3691
|
+
page their own invite, and the rest of the guild hears it here). The read only
|
|
3692
|
+
names a project that is on the map and public, and leaves out one you already
|
|
3693
|
+
build on, are invited to, or have applied to. Each card's Apply is the SAME
|
|
3694
|
+
relay as everything else in this view — postJoinRequest — with `guild` added:
|
|
3695
|
+
you apply as yourself, never as the guild, with your own note if you like. A
|
|
3696
|
+
guild that no longer holds for you (you left it, or its 30 days ran out) is a
|
|
3697
|
+
409 and nothing is relayed; the card then offers the plain application.
|
|
3698
|
+
textContent / createElement only. Proof: tests/hub_engagement_cards.mjs */
|
|
3699
|
+
var ENGAGE_GONE = 'That guild application has ended — you can still apply on your own.';
|
|
3700
|
+
function engageLine(e) {
|
|
3701
|
+
var g = e.guild.name || e.guild.slug;
|
|
3702
|
+
var p = (e.project && e.project.name) || hostOf(e.project.origin);
|
|
3703
|
+
return e.kind === 'invited' ? p + ' invited ' + g : g + ' is applying to ' + p + ' — apply with them?';
|
|
3704
|
+
}
|
|
3705
|
+
function sessionExpired(el) {
|
|
3706
|
+
el.className = 'projMsg err';
|
|
3707
|
+
el.textContent = 'Your session expired — ';
|
|
3708
|
+
var a = document.createElement('a');
|
|
3709
|
+
a.href = API + '/auth/web/start?return=/projects%3Fview%3Dmine&repos=1';
|
|
3710
|
+
a.textContent = 'sign in again';
|
|
3711
|
+
el.appendChild(a);
|
|
3712
|
+
el.appendChild(document.createTextNode('.'));
|
|
3713
|
+
}
|
|
3714
|
+
function engageRow(e) {
|
|
3715
|
+
var origin = e.project.origin;
|
|
3716
|
+
var slug = e.guild.slug;
|
|
3717
|
+
var row = mineRow({ name: engageLine(e), origin: origin });
|
|
3718
|
+
row.className = 'mineRow engageRow';
|
|
3719
|
+
var main = row.querySelector('.mineRow__main');
|
|
3720
|
+
var hint = mineEl('p', 'mineRow__hint');
|
|
3721
|
+
hint.textContent = 'You apply as yourself — add a note for the owner if you like.';
|
|
3722
|
+
main.appendChild(hint);
|
|
3723
|
+
var note = document.createElement('textarea');
|
|
3724
|
+
note.className = 'mineJoinNote';
|
|
3725
|
+
note.rows = 2;
|
|
3726
|
+
note.maxLength = 400;
|
|
3727
|
+
note.placeholder = 'A note for the owner (optional) — what you’d like to work on, who you are.';
|
|
3728
|
+
note.setAttribute('aria-label', 'A note for the owner (optional)');
|
|
3729
|
+
note.hidden = true;
|
|
3730
|
+
main.appendChild(note);
|
|
3731
|
+
var msg = mineEl('p', 'projMsg');
|
|
3732
|
+
msg.setAttribute('role', 'status');
|
|
3733
|
+
msg.setAttribute('aria-live', 'polite');
|
|
3734
|
+
main.appendChild(msg);
|
|
3735
|
+
var actions = mineEl('div', 'mineRow__actions');
|
|
3736
|
+
var noteBtn = document.createElement('button');
|
|
3737
|
+
noteBtn.type = 'button';
|
|
3738
|
+
noteBtn.className = 'linklike';
|
|
3739
|
+
noteBtn.textContent = 'Add a note';
|
|
3740
|
+
noteBtn.addEventListener('click', function () { note.hidden = false; noteBtn.hidden = true; note.focus(); });
|
|
3741
|
+
var apply = document.createElement('button');
|
|
3742
|
+
apply.type = 'button';
|
|
3743
|
+
apply.className = 'btn small';
|
|
3744
|
+
apply.textContent = 'Apply';
|
|
3745
|
+
/* with the guild until the server says it no longer holds, then on your own */
|
|
3746
|
+
var withGuild = true;
|
|
3747
|
+
function idle() { apply.disabled = false; apply.textContent = withGuild ? 'Apply' : 'Apply on my own'; }
|
|
3748
|
+
apply.addEventListener('click', function () {
|
|
3749
|
+
apply.disabled = true;
|
|
3750
|
+
apply.textContent = 'Applying…';
|
|
3751
|
+
msg.className = 'projMsg';
|
|
3752
|
+
msg.textContent = '';
|
|
3753
|
+
postJoinRequest(origin, (note.value || '').trim() || null, withGuild ? slug : null).then(function (res) {
|
|
3754
|
+
if (res.unauthenticated) { idle(); sessionExpired(msg); return; }
|
|
3755
|
+
if (res.ok && res.data && res.data.status === 'requested') {
|
|
3756
|
+
/* no longer pressable, so no longer the accent (the One Warm Thing rule) */
|
|
3757
|
+
apply.className = 'btn small ghost';
|
|
3758
|
+
apply.textContent = 'Applied';
|
|
3759
|
+
hint.hidden = true;
|
|
3760
|
+
note.hidden = true;
|
|
3761
|
+
noteBtn.hidden = true;
|
|
3762
|
+
msg.className = 'projMsg ok';
|
|
3763
|
+
msg.textContent = res.data.message || 'Requested.';
|
|
3764
|
+
return;
|
|
3765
|
+
}
|
|
3766
|
+
var err = apiError(res.data, res.status);
|
|
3767
|
+
if (res.status === 409 && err.code === 'guild_engagement_gone') {
|
|
3768
|
+
withGuild = false;
|
|
3769
|
+
idle();
|
|
3770
|
+
msg.textContent = ENGAGE_GONE;
|
|
3771
|
+
return;
|
|
3772
|
+
}
|
|
3773
|
+
idle();
|
|
3774
|
+
/* the relay's own answer (invite_only, already_member …) in its own words */
|
|
3775
|
+
msg.className = 'projMsg' + (res.ok ? '' : ' err');
|
|
3776
|
+
msg.textContent = (res.ok && res.data && res.data.message) || err.message || 'Could not reach that project — try again in a moment.';
|
|
3777
|
+
}).catch(function () {
|
|
3778
|
+
idle();
|
|
3779
|
+
msg.className = 'projMsg err';
|
|
3780
|
+
msg.textContent = 'Couldn’t reach the platform — check your connection and try again.';
|
|
3781
|
+
});
|
|
3782
|
+
});
|
|
3783
|
+
actions.appendChild(noteBtn);
|
|
3784
|
+
actions.appendChild(apply);
|
|
3785
|
+
row.appendChild(actions);
|
|
3786
|
+
return row;
|
|
3787
|
+
}
|
|
3788
|
+
/* a row this page cannot act on (an unknown kind, no guild to name, no address
|
|
3789
|
+
to apply to) is dropped rather than drawn as a card whose button fails */
|
|
3790
|
+
function renderEngagements(rows) {
|
|
3791
|
+
var ul = $('engageList');
|
|
3792
|
+
ul.textContent = '';
|
|
3793
|
+
(rows || []).forEach(function (e) {
|
|
3794
|
+
if (!e || (e.kind !== 'apply' && e.kind !== 'invited')) return;
|
|
3795
|
+
if (!e.guild || typeof e.guild.slug !== 'string' || !e.guild.slug) return;
|
|
3796
|
+
if (!e.project || !safeHttpUrl(e.project.origin)) return;
|
|
3797
|
+
ul.appendChild(engageRow(e));
|
|
3798
|
+
});
|
|
3799
|
+
paintInvitesSection();
|
|
3800
|
+
}
|
|
3801
|
+
function loadEngagements() {
|
|
3802
|
+
return fetch(API + '/me/guild-engagements', { credentials: 'same-origin' })
|
|
3803
|
+
.then(function (res) { return res.ok ? res.json() : { engagements: [] }; })
|
|
3804
|
+
.then(function (d) { renderEngagements(d.engagements || []); })
|
|
3805
|
+
.catch(function () { renderEngagements([]); });
|
|
3661
3806
|
}
|
|
3662
3807
|
|
|
3663
3808
|
/* the one relay both the map's per-card Apply (submitApply) and the
|
|
@@ -3665,11 +3810,17 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
3665
3810
|
the 'requested' echo update kept in ONE place (task 1003774). Keys the
|
|
3666
3811
|
echo off the server's OWN resolved origin where the response carries one:
|
|
3667
3812
|
it need not match what the visitor typed byte-for-byte (a trailing
|
|
3668
|
-
slash, a different case) to be the same project the map's catalog names.
|
|
3669
|
-
|
|
3813
|
+
slash, a different case) to be the same project the map's catalog names.
|
|
3814
|
+
`guild` (a slug, optional) is a guild card's: the application is still the
|
|
3815
|
+
caller's own, and the hub records which guild they came with (task 1002303,
|
|
3816
|
+
R27; ADR 0365 D1/D2). */
|
|
3817
|
+
function postJoinRequest(origin, note, guild) {
|
|
3818
|
+
var body = { origin: origin };
|
|
3819
|
+
if (note) body.note = note;
|
|
3820
|
+
if (guild) body.guild = guild;
|
|
3670
3821
|
return fetch(API + '/my-projects/join', {
|
|
3671
3822
|
method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json' },
|
|
3672
|
-
body: JSON.stringify(
|
|
3823
|
+
body: JSON.stringify(body),
|
|
3673
3824
|
}).then(function (res) {
|
|
3674
3825
|
if (res.status === 401) return { unauthenticated: true };
|
|
3675
3826
|
return jsonOrEmpty(res).then(function (data) {
|
|
@@ -10296,11 +10447,17 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
10296
10447
|
here, because the project notifies the hub (POST /sso/invites/notify).
|
|
10297
10448
|
|
|
10298
10449
|
WHAT IT PUBLISHES OF AN INVITEE is the login the inviter typed and nothing else.
|
|
10450
|
+
|
|
10451
|
+
A GUILD (task 1002303, R27; ADR 0365 D3), on the project's own page only: the
|
|
10452
|
+
inviter names a public guild and POST /projects/invite-guild { client_id, guild }
|
|
10453
|
+
answers one hand-off link per member on the guild's PUBLIC page — the people the
|
|
10454
|
+
inviter could see, and their handle and display name, which that page already
|
|
10455
|
+
shows. They become the same lines, each finished on the hall like any other.
|
|
10299
10456
|
Proof: ../../../tests/project_invite_ui.mjs ══ */
|
|
10300
10457
|
|
|
10301
10458
|
var INVITE_MOUNTS = {
|
|
10302
10459
|
wizInvite: { who: 'wizInviteWho', list: 'wizInviteList', msg: 'wizInviteMsg', after: 'doneStepsHead', why: 'wizInviteWhy', sugg: 'wizInviteSugg' },
|
|
10303
|
-
mInvite: { who: 'mInviteWho', list: 'mInviteList', msg: 'mInviteMsg', after: null, why: 'mInviteWhy', sugg: 'mInviteSugg' }
|
|
10460
|
+
mInvite: { who: 'mInviteWho', list: 'mInviteList', msg: 'mInviteMsg', after: null, why: 'mInviteWhy', sugg: 'mInviteSugg', guild: 'mInviteGuild', guildMsg: 'mInviteGuildMsg' }
|
|
10304
10461
|
};
|
|
10305
10462
|
var inviteSlug = {}; /* mount id → the slug it invites to; '' means no step at all */
|
|
10306
10463
|
var inviteRows = {}; /* mount id → the invites filed on THIS project this session */
|
|
@@ -10320,6 +10477,7 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
10320
10477
|
inviteRows[id] = [];
|
|
10321
10478
|
inviteSugg[id] = null;
|
|
10322
10479
|
inviteSay(id, '', '');
|
|
10480
|
+
inviteSay(id, '', '', false, 'guildMsg');
|
|
10323
10481
|
}
|
|
10324
10482
|
var box = $(id);
|
|
10325
10483
|
if (box) box.hidden = !next;
|
|
@@ -10389,14 +10547,15 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
10389
10547
|
}
|
|
10390
10548
|
|
|
10391
10549
|
/* One line per name handed to the hall this session: the login the inviter
|
|
10392
|
-
typed,
|
|
10550
|
+
typed (or, for a guild's member, the display name its public page shows), and
|
|
10551
|
+
the link that finishes its invite there. The line says the invite is
|
|
10393
10552
|
NOT sent yet, because it is not — the hall's Invite button is what sends it. */
|
|
10394
10553
|
function renderInviteRows(id) {
|
|
10395
10554
|
var m = INVITE_MOUNTS[id];
|
|
10396
10555
|
var list = m && $(m.list);
|
|
10397
10556
|
if (!list) return;
|
|
10398
10557
|
list.innerHTML = (inviteRows[id] || []).map(function (r) {
|
|
10399
|
-
return '<li><b>' + esc(r.login) + '</b><small>not sent yet — finish it on your project’s hall.</small>'
|
|
10558
|
+
return '<li><b>' + esc(r.name || r.login) + '</b><small>not sent yet — finish it on your project’s hall.</small>'
|
|
10400
10559
|
+ '<a class="btn small" href="' + esc(r.url) + '" target="_blank" rel="noopener">Send the invite →</a></li>';
|
|
10401
10560
|
}).join('');
|
|
10402
10561
|
/* the two lists are one answer: a name that just became a pending row is no
|
|
@@ -10404,9 +10563,10 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
10404
10563
|
renderInviteSugg(id);
|
|
10405
10564
|
}
|
|
10406
10565
|
|
|
10407
|
-
|
|
10566
|
+
/* `key` names the line: the mount's message (the default) or the guild row's */
|
|
10567
|
+
function inviteSay(id, text, cls, isHtml, key) {
|
|
10408
10568
|
var m = INVITE_MOUNTS[id];
|
|
10409
|
-
var el = m && $(m
|
|
10569
|
+
var el = m && m[key || 'msg'] ? $(m[key || 'msg']) : null;
|
|
10410
10570
|
if (!el) return;
|
|
10411
10571
|
el.className = 'projMsg' + (cls ? ' ' + cls : '');
|
|
10412
10572
|
if (isHtml) el.innerHTML = text; else el.textContent = text;
|
|
@@ -10461,6 +10621,73 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
10461
10621
|
});
|
|
10462
10622
|
}
|
|
10463
10623
|
|
|
10624
|
+
/* inviteGuildSlug(raw) — a guild named the ways a person pastes one: its name in
|
|
10625
|
+
the address ("forge-crew"), "/g/forge-crew", or the guild page's whole address.
|
|
10626
|
+
PURE. Anything else is sent as typed, and the route answers for it. */
|
|
10627
|
+
function inviteGuildSlug(raw) {
|
|
10628
|
+
var s = String(raw == null ? '' : raw).replace(/^\s+|\s+$/g, '');
|
|
10629
|
+
var m = /(?:^|\/)g\/([^\/?#\s]+)\/?(?:[?#].*)?$/i.exec(s);
|
|
10630
|
+
if (m) {
|
|
10631
|
+
s = m[1];
|
|
10632
|
+
try { s = decodeURIComponent(s); } catch (e) { /* sent as typed */ }
|
|
10633
|
+
}
|
|
10634
|
+
return s.toLowerCase();
|
|
10635
|
+
}
|
|
10636
|
+
|
|
10637
|
+
/* doInviteGuild(id) — one public guild, one request: a pending line per member on
|
|
10638
|
+
its public page, each finished on the hall like a single invite. The route
|
|
10639
|
+
records that the project invited the guild and sends nothing, and nor does
|
|
10640
|
+
this. Only a mount that names a guild field has this control. */
|
|
10641
|
+
function doInviteGuild(id) {
|
|
10642
|
+
var m = INVITE_MOUNTS[id];
|
|
10643
|
+
var slug = inviteSlug[id] || '';
|
|
10644
|
+
if (!m || !m.guild || !slug || inviteBusy[id]) return;
|
|
10645
|
+
var field = $(m.guild);
|
|
10646
|
+
var guild = inviteGuildSlug(field && field.value);
|
|
10647
|
+
if (!guild) {
|
|
10648
|
+
inviteSay(id, 'Type the guild’s name or its /g/ address.', 'err', false, 'guildMsg');
|
|
10649
|
+
if (field && field.focus) field.focus();
|
|
10650
|
+
return;
|
|
10651
|
+
}
|
|
10652
|
+
inviteBusy[id] = true;
|
|
10653
|
+
inviteSay(id, 'Finding the guild’s members…', '', false, 'guildMsg');
|
|
10654
|
+
fetch(API + '/projects/invite-guild', {
|
|
10655
|
+
method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json' },
|
|
10656
|
+
body: JSON.stringify({ client_id: slug, guild: guild })
|
|
10657
|
+
}).then(function (res) {
|
|
10658
|
+
return jsonOrEmpty(res).then(function (data) {
|
|
10659
|
+
inviteBusy[id] = false;
|
|
10660
|
+
if (res.status === 401) { inviteSay(id, signInAgain(), 'err', true, 'guildMsg'); return; }
|
|
10661
|
+
var g = res.ok && data && data.guild;
|
|
10662
|
+
if (g) {
|
|
10663
|
+
/* a member already on a line (the same guild twice) is not drawn again */
|
|
10664
|
+
var listed = 0;
|
|
10665
|
+
(data.invites || []).forEach(function (r) {
|
|
10666
|
+
var url = r ? safeHttpUrl(r.invite_url) : '';
|
|
10667
|
+
if (!url || !r.handle) return;
|
|
10668
|
+
listed++;
|
|
10669
|
+
if ((inviteRows[id] || []).some(function (x) { return x.url === url; })) return;
|
|
10670
|
+
inviteRows[id] = (inviteRows[id] || []).concat([{ login: String(r.handle), name: r.display_name ? String(r.display_name) : '', url: url }]);
|
|
10671
|
+
});
|
|
10672
|
+
renderInviteRows(id);
|
|
10673
|
+
if (field) field.value = '';
|
|
10674
|
+
var name = g.name || guild;
|
|
10675
|
+
inviteSay(id, listed
|
|
10676
|
+
? name + ': ' + listed + (listed === 1 ? ' member' : ' members') + ' to invite. Send each invite from your project’s hall — every line above opens it with their name filled in.'
|
|
10677
|
+
: 'Nobody is listed on ' + name + '’s public page yet, so there is no one to invite from here.', listed ? 'ok' : '', false, 'guildMsg');
|
|
10678
|
+
return;
|
|
10679
|
+
}
|
|
10680
|
+
var e = apiError(data, res.status);
|
|
10681
|
+
/* an unknown guild and an unlisted one are one answer on the server */
|
|
10682
|
+
if (res.status === 404 && e.code === 'guild_not_found') { inviteSay(id, 'No public guild at that name.', 'err', false, 'guildMsg'); return; }
|
|
10683
|
+
inviteSay(id, inviteRefusal(e.code, res.status, e.message), 'err', false, 'guildMsg');
|
|
10684
|
+
});
|
|
10685
|
+
}).catch(function () {
|
|
10686
|
+
inviteBusy[id] = false;
|
|
10687
|
+
inviteSay(id, 'Couldn’t reach the platform — check your connection and try again.', 'err', false, 'guildMsg');
|
|
10688
|
+
});
|
|
10689
|
+
}
|
|
10690
|
+
|
|
10464
10691
|
/* skipInvite(id) — the step steps aside having sent NOTHING. Focus moves on
|
|
10465
10692
|
rather than being stranded inside the box that just went away. */
|
|
10466
10693
|
function skipInvite(id) {
|
|
@@ -10479,6 +10706,7 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
10479
10706
|
var box = $(id);
|
|
10480
10707
|
if (box) {
|
|
10481
10708
|
box.addEventListener('click', function (ev) {
|
|
10709
|
+
if (ev.target.closest && ev.target.closest('[data-invite-guild]')) { doInviteGuild(id); return; }
|
|
10482
10710
|
var t = ev.target.closest && ev.target.closest('[data-invite-send],[data-invite-skip],[data-invite-pick]');
|
|
10483
10711
|
if (!t) return;
|
|
10484
10712
|
/* a picked suggestion fills the field and takes the SAME path as a typed
|
|
@@ -10501,6 +10729,14 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
10501
10729
|
doInvite(id);
|
|
10502
10730
|
});
|
|
10503
10731
|
}
|
|
10732
|
+
var guildField = INVITE_MOUNTS[id].guild ? $(INVITE_MOUNTS[id].guild) : null;
|
|
10733
|
+
if (guildField) {
|
|
10734
|
+
guildField.addEventListener('keydown', function (ev) {
|
|
10735
|
+
if (ev.key !== 'Enter') return;
|
|
10736
|
+
ev.preventDefault();
|
|
10737
|
+
doInviteGuild(id);
|
|
10738
|
+
});
|
|
10739
|
+
}
|
|
10504
10740
|
});
|
|
10505
10741
|
|
|
10506
10742
|
/* ── boot: who am I → my instances ────────────────────────────────────── */
|
|
@@ -175,6 +175,31 @@
|
|
|
175
175
|
"expect": {"visible":["#panel-fork",".doorPill","#wizDemo","#wizStart"]} },
|
|
176
176
|
{ "name": "the wizard driver at 390: the review rows stack and the walk still reaches Create", "auth": true, "width": 390, "url": "/projects?view=new",
|
|
177
177
|
"steps": [["wait",600],["click","#termsAck"],["wait",100],["click","#wizStart"],["wait",400],["fill","#newName","Mercury"],["click","#typeCards .tpill[data-type=\"game\"]"],["click","#panel-what button[type=submit]"],["wait",400],["select","#physCountry","US"],["wait",100],["click","[data-phys=\"builder_geography\"][data-val=\"global\"]"],["click","[data-phys=\"minors_on_board\"][data-val=\"false\"]"],["click","[data-phys=\"entity\"][data-val=\"intent-solo\"]"],["click","#physNext"],["wait",700],["click","#reviewRows .setRow[data-row=\"address\"] [data-row-chg]"],["wait",200],["fill","#f-slug","mercury"],["wait",900],["fill","#f-domain","mercury.example.com"],["wait",200],["click","#reviewRows .setRow[data-row=\"address\"] [data-row-chg]"],["wait",200],["click","#reviewGo"],["wait",500]],
|
|
178
|
-
"expect": {"visible":["#panel-look","#wizLook","#doCreate"],"hidden":["#panel-review"],"text":[["#look-h","Give it a look."]]} }
|
|
178
|
+
"expect": {"visible":["#panel-look","#wizLook","#doCreate"],"hidden":["#panel-review"],"text":[["#look-h","Give it a look."]]} },
|
|
179
|
+
{ "name": "guild cards (task 1002303, R27): the mine view lists what your guilds have going, each card with its own Apply", "auth": true, "width": 1440, "url": "/projects?view=mine",
|
|
180
|
+
"steps": [["wait",900]],
|
|
181
|
+
"expect": { "visible": ["#mineInvites", "#engageList"], "count": [["#engageList .engageRow", 2], ["#engageList .engageRow .mineJoinNote:not([hidden])", 0]],
|
|
182
|
+
"text": [["#engageList .engageRow:nth-child(1) .mineRow__name", "Forge Crew is applying to Tidepool — apply with them?"], ["#engageList .engageRow:nth-child(2) .mineRow__name", "Lantern Works invited Forge Crew"]] } },
|
|
183
|
+
{ "name": "guild cards: Add a note opens the card's own note; Apply sends it with the guild (the stub refuses a guild with no engagement) and the card says it went through", "continue": true,
|
|
184
|
+
"steps": [["click","#engageList .engageRow:first-child .linklike"],["fill","#engageList .engageRow:first-child .mineJoinNote","Happy to take the migrations."],["click","#engageList .engageRow:first-child .btn"],["wait",400]],
|
|
185
|
+
"expect": { "hidden": ["#engageList .engageRow:first-child .mineJoinNote"], "hasClass": [["#engageList .engageRow:first-child .projMsg", "ok"]],
|
|
186
|
+
"text": [["#engageList .engageRow:first-child .btn", "Applied"], ["#engageList .engageRow:first-child .projMsg", "Requested access. The project’s owner admits new builders; once approved, sign in to enter."]] } },
|
|
187
|
+
{ "name": "guild cards: the invited card applies on its own, with no note", "continue": true,
|
|
188
|
+
"steps": [["click","#engageList .engageRow:nth-child(2) .btn"],["wait",400]],
|
|
189
|
+
"expect": { "text": [["#engageList .engageRow:nth-child(2) .btn", "Applied"]], "hasClass": [["#engageList .engageRow:nth-child(2) .projMsg", "ok"]] } },
|
|
190
|
+
{ "name": "invite a guild (task 1002303, R27): a /g/ address gives one pending line per member on its public page, each a link to the hall", "auth": true, "width": 1440, "url": "/projects?view=manage&id=101",
|
|
191
|
+
"steps": [["wait",900],["fill","#mInviteGuild","/g/forge-crew"],["click","#mInvite [data-invite-guild]"],["wait",400]],
|
|
192
|
+
"expect": { "count": [["#mInviteList li", 4]], "value": [["#mInviteGuild", ""]],
|
|
193
|
+
"href": [["#mInviteList li:first-child a", "https://mercury.example.test/builders/watch?invite=mara-quinn"]],
|
|
194
|
+
"text": [["#mInviteGuildMsg", "Forge Crew: 4 members to invite. Send each invite from your project’s hall — every line above opens it with their name filled in."]] } },
|
|
195
|
+
{ "name": "invite a guild: the same guild again draws no second line", "continue": true,
|
|
196
|
+
"steps": [["fill","#mInviteGuild","forge-crew"],["click","#mInvite [data-invite-guild]"],["wait",400]],
|
|
197
|
+
"expect": { "count": [["#mInviteList li", 4]] } },
|
|
198
|
+
{ "name": "invite a guild: an unlisted guild is the same one line as an unknown one", "continue": true,
|
|
199
|
+
"steps": [["fill","#mInviteGuild","night-shift"],["click","#mInvite [data-invite-guild]"],["wait",400]],
|
|
200
|
+
"expect": { "text": [["#mInviteGuildMsg", "No public guild at that name."]], "hasClass": [["#mInviteGuildMsg", "err"]], "count": [["#mInviteList li", 4]], "value": [["#mInviteGuild", "night-shift"]] } },
|
|
201
|
+
{ "name": "invite a guild: an empty field is a question, not a request; Enter in the field sends too", "continue": true,
|
|
202
|
+
"steps": [["fill","#mInviteGuild",""],["focus","#mInviteGuild"],["press","Enter"],["wait",150]],
|
|
203
|
+
"expect": { "text": [["#mInviteGuildMsg", "Type the guild’s name or its /g/ address."]], "activeId": "mInviteGuild" } }
|
|
179
204
|
]
|
|
180
205
|
}
|
|
@@ -26,6 +26,10 @@
|
|
|
26
26
|
"in-manage-kind": { "auth": true, "url": "/projects?view=manage&id=101", "expect": {"visible":["#mKind","#mKindPick .modBand","#mKindWhat summary"],"count":[["#mKindPick .modOpt",5],["#mKindPick .modOpt[aria-pressed=\"true\"]",0]],"text":[["#mKindPick .modOpt b","Game"],["#mKindPick .finePrint","This project has never said what kind it is. Saying so puts the word beside its name wherever it is listed; it changes nothing else."]]} },
|
|
27
27
|
"in-manage-invite": { "auth": true, "url": "/projects?view=manage&id=101", "expect": { "visible": ["#mInvite", "#mInvite h2", "#mInviteWho", "#mInvite [data-invite-send]"] } },
|
|
28
28
|
"in-manage-invite-handoff": { "auth": true, "url": "/projects?view=manage&id=101", "actions": [["wait",900],["fill","#mInviteWho","octo-builder"],["click","#mInvite [data-invite-send]"],["wait",400]], "expect": {"visible":["#mInviteList a","#mInviteMsg"],"hidden":[]} },
|
|
29
|
+
"in-manage-invite-guild": { "auth": true, "url": "/projects?view=manage&id=101", "actions": [["wait",900],["fill","#mInviteGuild","https://cloudbongos.com/g/forge-crew"],["click","#mInvite [data-invite-guild]"],["wait",400]], "expect": { "visible": ["#mInviteGuild", "#mInviteList a", "#mInviteGuildMsg"], "count": [["#mInviteList li", 4]], "value": [["#mInviteGuild", ""]], "hasClass": [["#mInviteGuildMsg", "ok"]], "text": [["#mInviteList li:first-child b", "Mara Quinn"], ["#mInviteGuildMsg", "Forge Crew: 4 members to invite. Send each invite from your project’s hall — every line above opens it with their name filled in."]] } },
|
|
30
|
+
"in-mine-engagements": { "auth": true, "url": "/projects?view=mine", "actions": [["wait",900],["focus","#engageList .engageRow:first-child .btn"],["wait",200]], "expect": { "visible": ["#mineInvites", "#invitesList", "#engageList"], "count": [["#engageList .engageRow", 2], ["#engageList .engageRow .mineJoinNote:not([hidden])", 0]], "text": [["#engageList .engageRow:nth-child(1) .mineRow__name", "Forge Crew is applying to Tidepool — apply with them?"], ["#engageList .engageRow:nth-child(1) .mineRow__host", "tidepool.cloudbongos.com"], ["#engageList .engageRow:nth-child(2) .mineRow__name", "Lantern Works invited Forge Crew"], ["#engageList .engageRow:nth-child(1) .mineRow__hint", "You apply as yourself — add a note for the owner if you like."], ["#engageList .engageRow:nth-child(1) .btn", "Apply"]] } },
|
|
31
|
+
"in-mine-engagement-note": { "auth": true, "url": "/projects?view=mine", "actions": [["wait",900],["click","#engageList .engageRow:first-child .linklike"],["fill","#engageList .engageRow:first-child .mineJoinNote","I like the boring parts too: migrations and the deploy path."],["wait",150]], "expect": { "visible": ["#engageList .engageRow:first-child .mineJoinNote"], "hidden": ["#engageList .engageRow:first-child .linklike"], "value": [["#engageList .engageRow:first-child .mineJoinNote", "I like the boring parts too: migrations and the deploy path."]] } },
|
|
32
|
+
"in-mine-engagement-applied": { "auth": true, "url": "/projects?view=mine", "actions": [["wait",900],["click","#engageList .engageRow:first-child .linklike"],["fill","#engageList .engageRow:first-child .mineJoinNote","I like the boring parts too: migrations and the deploy path."],["click","#engageList .engageRow:first-child .btn"],["wait",400]], "expect": { "hidden": ["#engageList .engageRow:first-child .mineJoinNote", "#engageList .engageRow:first-child .linklike"], "hasClass": [["#engageList .engageRow:first-child .projMsg", "ok"], ["#engageList .engageRow:first-child .btn", "ghost"]], "text": [["#engageList .engageRow:first-child .btn", "Applied"], ["#engageList .engageRow:first-child .projMsg", "Requested access. The project’s owner admits new builders; once approved, sign in to enter."], ["#engageList .engageRow:nth-child(2) .btn", "Apply"]] } },
|
|
29
33
|
"in-manage-app-live": { "auth": true, "url": "/projects?view=manage&id=101", "actions": [["wait",900],["focus","#mApp a"]], "expect": {"visible":["#mAppCard","#mApp .appStat","#mApp .appVars"],"hidden":[]} },
|
|
30
34
|
"in-manage-app-building": { "auth": true, "url": "/projects?view=manage&id=102", "actions": [["wait",900],["focus","#mApp a"]], "expect": {"visible":["#mAppCard","#mApp .appStages"],"hidden":[]} },
|
|
31
35
|
"in-manage-app-failed": { "auth": true, "url": "/projects?view=manage&id=105", "actions": [["wait",900],["focus","#mApp .appRetryBtn"]], "expect": {"visible":["#mAppCard","#mApp p.err","#mApp .appRetryBtn"],"hidden":["#mApp .appRetry"]} },
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"engagements": [
|
|
3
|
+
{ "guild": { "slug": "forge-crew", "name": "Forge Crew" }, "kind": "apply", "project": { "name": "Tidepool", "origin": "https://tidepool.cloudbongos.com" }, "created_at": "2026-10-03T12:00:00.000Z" },
|
|
4
|
+
{ "guild": { "slug": "forge-crew", "name": "Forge Crew" }, "kind": "invited", "project": { "name": "Lantern Works", "origin": "https://lantern-works.cloudbongos.com" }, "created_at": "2026-10-02T09:30:00.000Z" }
|
|
5
|
+
],
|
|
6
|
+
"count": 2
|
|
7
|
+
}
|
|
@@ -111,9 +111,12 @@ const FEEDS = {
|
|
|
111
111
|
// /founding/genesis and /government/constitution (task 1004423) ship no default file: the
|
|
112
112
|
// genesis home asks for them only in founding mode, so every existing render is unchanged,
|
|
113
113
|
// and the genesis variants point them at their samples.
|
|
114
|
+
// /me/guild-engagements (task 1002303, R27) is the mine view's guild cards: a guild applying
|
|
115
|
+
// to a project, and a project that invited a guild, for the guilds the builder is in.
|
|
114
116
|
const AUTHED = {
|
|
115
117
|
'/my-projects': 'my-projects', '/my-projects/invites': 'my-projects__invites', '/founding/builders': 'founding-builders',
|
|
116
118
|
'/founding/genesis': 'founding-genesis', '/government/constitution': 'government-constitution',
|
|
119
|
+
'/me/guild-engagements': 'me__guild-engagements',
|
|
117
120
|
};
|
|
118
121
|
const UNAUTH = { error: { code: 'unauthenticated', message: 'unauthenticated' } };
|
|
119
122
|
|
|
@@ -474,6 +477,47 @@ function guildMembershipPatch(res, slug, body) {
|
|
|
474
477
|
return json(res, 200, { shown_publicly: g.members.you.shown_publicly, counted_in_totals: g.members.you.counted_in_totals });
|
|
475
478
|
}
|
|
476
479
|
|
|
480
|
+
// Guild engagements (task 1002303, R27; ADR 0365). The stub mirrors the routes' refusals so
|
|
481
|
+
// a probe cannot pass on a body or a caller the server would refuse:
|
|
482
|
+
// POST /guilds/:slug/engagements { origin } the owner files "this guild is applying to P":
|
|
483
|
+
// a non-member's 404, a member's owner-only 403, and ONE 404 for a project that is
|
|
484
|
+
// unknown or not (on the map AND public) — here, a featured-feed row whose orb is a planet;
|
|
485
|
+
// POST /projects/invite-guild { client_id, guild } one hall hand-off link per member on the
|
|
486
|
+
// guild's PUBLIC roster; an unknown or unlisted guild is one 404;
|
|
487
|
+
// POST /my-projects/join's optional `guild` must name a held guild the builder is in with a
|
|
488
|
+
// held engagement (me__guild-engagements.json) for that project, else 409 and nothing relayed.
|
|
489
|
+
const UNKNOWN_PROJECT = { error: { code: 'unknown_project', message: 'unknown_project' } };
|
|
490
|
+
const sameOrigin = (a, b) => String(a || '').trim().toLowerCase().replace(/\/+$/, '') === String(b || '').trim().toLowerCase().replace(/\/+$/, '');
|
|
491
|
+
function heldFixture(name) {
|
|
492
|
+
return JSON.parse(fs.readFileSync(path.join(FIXTURES, (ALIAS[name] || name) + '.json'), 'utf8'));
|
|
493
|
+
}
|
|
494
|
+
function guildEngagementPost(res, slug, body) {
|
|
495
|
+
const g = guildsHeld()[String(slug).toLowerCase()];
|
|
496
|
+
if (!g || !g.members) return json(res, 404, GUILD_NOT_FOUND);
|
|
497
|
+
if (g.members.you.membership_kind !== 'owner') return refuse(res, 403, 'guild_owner_only', 'only the guild owner can do that');
|
|
498
|
+
const bad = Object.keys(body).find((k) => k !== 'origin');
|
|
499
|
+
if (bad) return refuse(res, 400, 'validation_failed', `unknown field: ${bad}`);
|
|
500
|
+
if (typeof body.origin !== 'string' || !body.origin || body.origin.length > 300) return refuse(res, 400, 'validation_failed', 'origin must be a string');
|
|
501
|
+
const p = (heldFixture('featured').projects || []).find((x) => x.orb === 'planet' && sameOrigin(x.origin, body.origin));
|
|
502
|
+
if (!p) return json(res, 404, UNKNOWN_PROJECT);
|
|
503
|
+
return json(res, 200, { engagement: { guild: { slug: g.members.guild.slug, name: g.members.guild.name }, kind: 'apply', project: { name: p.name, origin: p.origin }, created_at: new Date().toISOString() } });
|
|
504
|
+
}
|
|
505
|
+
function guildInvitePost(res, body) {
|
|
506
|
+
const bad = Object.keys(body).find((k) => !['client_id', 'guild'].includes(k));
|
|
507
|
+
if (bad) return refuse(res, 400, 'validation_failed', `unknown field: ${bad}`);
|
|
508
|
+
if (typeof body.client_id !== 'string' || !body.client_id) return refuse(res, 400, 'validation_failed', 'client_id must be a string');
|
|
509
|
+
if (typeof body.guild !== 'string' || !body.guild || body.guild.length > 39) return refuse(res, 400, 'validation_failed', 'guild must be a string of at most 39 characters');
|
|
510
|
+
const g = guildsHeld()[body.guild.toLowerCase()];
|
|
511
|
+
if (!g || !g.public) return json(res, 404, GUILD_NOT_FOUND);
|
|
512
|
+
const invites = (g.public.members || []).map((m) => ({ handle: m.handle, display_name: m.display_name || null, invite_url: 'https://mercury.example.test/builders/watch?invite=' + encodeURIComponent(m.handle) }));
|
|
513
|
+
return json(res, 200, { invited: false, guild: { slug: g.public.guild.slug, name: g.public.guild.name }, invites, count: invites.length });
|
|
514
|
+
}
|
|
515
|
+
function guildEngagementLive(slug, origin) {
|
|
516
|
+
const g = typeof slug === 'string' && guildsHeld()[slug.toLowerCase()];
|
|
517
|
+
if (!g || !g.members) return false;
|
|
518
|
+
return (heldFixture('me__guild-engagements').engagements || []).some((e) => e.guild && e.guild.slug === slug.toLowerCase() && e.project && sameOrigin(e.project.origin, origin));
|
|
519
|
+
}
|
|
520
|
+
|
|
477
521
|
const server = http.createServer((req, res) => {
|
|
478
522
|
const u = new URL(req.url, 'http://x');
|
|
479
523
|
const p = u.pathname;
|
|
@@ -491,10 +535,11 @@ const server = http.createServer((req, res) => {
|
|
|
491
535
|
if (rest === '/me/cross-project' && req.method === 'PATCH') return AUTH ? readBody(req, (body) => crossProjectPatch(res, body)) : json(res, 401, UNAUTH);
|
|
492
536
|
if (rest === '/me/profile' && req.method === 'PATCH') return AUTH ? readBody(req, (body) => profilePatch(res, body)) : json(res, 401, UNAUTH);
|
|
493
537
|
if (rest === '/me/handle' && req.method === 'PATCH') return AUTH ? readBody(req, (body) => handlePatch(res, body)) : json(res, 401, UNAUTH);
|
|
494
|
-
const guild = rest.match(/^\/guilds\/([^/]+)(\/members|\/membership)?$/);
|
|
538
|
+
const guild = rest.match(/^\/guilds\/([^/]+)(\/members|\/membership|\/engagements)?$/);
|
|
495
539
|
if (guild && !guild[2] && req.method === 'GET') return guildRead(res, guild[1], 'public', origin);
|
|
496
540
|
if (guild && guild[2] === '/members' && req.method === 'GET') return AUTH ? guildRead(res, guild[1], 'members', origin) : json(res, 401, UNAUTH);
|
|
497
541
|
if (guild && guild[2] === '/membership' && req.method === 'PATCH') return AUTH ? readBody(req, (body) => guildMembershipPatch(res, guild[1], body)) : json(res, 401, UNAUTH);
|
|
542
|
+
if (guild && guild[2] === '/engagements' && req.method === 'POST') return AUTH ? readBody(req, (body) => guildEngagementPost(res, guild[1], body)) : json(res, 401, UNAUTH);
|
|
498
543
|
if (rest === '/provisioning/instances' && req.method === 'GET') return AUTH ? instances(res, null, false) : json(res, 401, UNAUTH);
|
|
499
544
|
// the planet-physics questions' pre-create check (task 1004418): nothing stops here —
|
|
500
545
|
// the kit carries no screen. Of the soft-limit table it mirrors only the three hide
|
|
@@ -533,10 +578,13 @@ const server = http.createServer((req, res) => {
|
|
|
533
578
|
// decline receipts a real render or probe of the my-projects block drives,
|
|
534
579
|
// so those buttons are not stuck picturing only their failure branch.
|
|
535
580
|
if (rest === '/my-projects/join' && req.method === 'POST') {
|
|
536
|
-
return AUTH ? readBody(req, (body) =>
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
581
|
+
return AUTH ? readBody(req, (body) => {
|
|
582
|
+
if (body.guild !== undefined && !guildEngagementLive(body.guild, body.origin)) return refuse(res, 409, 'guild_engagement_gone', 'that guild engagement has ended');
|
|
583
|
+
return json(res, 200, {
|
|
584
|
+
ok: true, status: 'requested', project: { name: body.origin || 'a project', origin: body.origin || null },
|
|
585
|
+
message: 'Requested access. The project’s owner admits new builders; once approved, sign in to enter.',
|
|
586
|
+
});
|
|
587
|
+
}) : json(res, 401, UNAUTH);
|
|
540
588
|
}
|
|
541
589
|
if (/^\/my-projects\/[^/]+\/leave$/.test(rest) && req.method === 'POST') return AUTH ? json(res, 200, { ok: true, left: true }) : json(res, 401, UNAUTH);
|
|
542
590
|
if (/^\/my-projects\/invites\/[^/]+\/decline$/.test(rest) && req.method === 'POST') return AUTH ? json(res, 200, { ok: true, accepted: false }) : json(res, 401, UNAUTH);
|
|
@@ -549,6 +597,7 @@ const server = http.createServer((req, res) => {
|
|
|
549
597
|
invite_url: 'https://mercury.example.test/builders/watch?invite=' + encodeURIComponent(body.github_login || ''),
|
|
550
598
|
})) : json(res, 401, UNAUTH);
|
|
551
599
|
}
|
|
600
|
+
if (rest === '/projects/invite-guild' && req.method === 'POST') return AUTH ? readBody(req, (body) => guildInvitePost(res, body)) : json(res, 401, UNAUTH);
|
|
552
601
|
// the manage flows (task 1003345): every door on a row and on the manage page only ASKS
|
|
553
602
|
// (ADR 0111) — the stub answers each ask with the route's receipt so the probes can drive
|
|
554
603
|
// attach / restart / teardown / revive / join end to end; nothing changes in the fixture
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.21.
|
|
3
|
+
"version": "1.21.59",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.21.
|
|
9
|
+
"version": "1.21.59",
|
|
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.21.
|
|
3
|
+
"version": "1.21.59",
|
|
4
4
|
"description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"main": "src/platform-server.js",
|
package/release-notes.json
CHANGED
|
@@ -9114,5 +9114,17 @@
|
|
|
9114
9114
|
"id": "1002301",
|
|
9115
9115
|
"text": "Guilds now have their own page on cloudbongos.com — who is in it and what it has shipped together — and members can choose whether they appear on it and count toward its totals."
|
|
9116
9116
|
}
|
|
9117
|
+
],
|
|
9118
|
+
"1.21.58": [
|
|
9119
|
+
{
|
|
9120
|
+
"id": "1002303",
|
|
9121
|
+
"text": "Guilds can now put themselves forward for a project, and a project can invite a whole guild — but every member still applies or accepts as themselves, so nobody joins a project without their own say."
|
|
9122
|
+
}
|
|
9123
|
+
],
|
|
9124
|
+
"1.21.59": [
|
|
9125
|
+
{
|
|
9126
|
+
"id": "1004552",
|
|
9127
|
+
"text": "The Goals and Tasks pages now open on the version the project owner has starred on the Roadmap. With no star, they show every version as before."
|
|
9128
|
+
}
|
|
9117
9129
|
]
|
|
9118
9130
|
}
|
|
@@ -183,6 +183,11 @@ const INTEGRATION = new Set([
|
|
|
183
183
|
// proves the one-owner partial index, the expiry CTE drained before its insert,
|
|
184
184
|
// the cascade on dissolve and the two-statement transfer.
|
|
185
185
|
'guilds_db',
|
|
186
|
+
// task 1002303 (R27): guild engagements against real SQL — the FK that cascades
|
|
187
|
+
// a dissolved guild's engagements and the one that SETs NULL on the echo, the
|
|
188
|
+
// upsert restarting the 30-day window in place, and the joined_at rule. The
|
|
189
|
+
// DB-free sibling (guild_engagement) pins the routes and statement shapes.
|
|
190
|
+
'guild_engagement_db',
|
|
186
191
|
// task 1002302 (R26): a guild's weekly rounded totals against real SQL. Who is
|
|
187
192
|
// counted (three predicates and a FILTERed k count) and the upsert's week guard,
|
|
188
193
|
// re-evaluated against a racing writer's committed row, are planner facts; the
|
package/src/module-api.js
CHANGED
|
@@ -75,7 +75,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
|
|
|
75
75
|
// MAJOR (see allowBoxScope below): passes the request through untouched.
|
|
76
76
|
function deprecatedNoopMiddleware(_req, _res, next) { next(); }
|
|
77
77
|
|
|
78
|
-
const CORE_VERSION = '1.21.
|
|
78
|
+
const CORE_VERSION = '1.21.59'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
79
79
|
|
|
80
80
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
81
81
|
// Usage: const log = api.logger('discord'); log.info('mounted');
|