@bongos/core 1.17.3 → 1.18.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (163) hide show
  1. package/.bongos-core.json +212 -332
  2. package/.devcontainer/healthcheck.sh +0 -0
  3. package/.devcontainer/init-firewall.sh +0 -0
  4. package/.devcontainer/post-create.sh +0 -0
  5. package/GOVERNANCE.md +1 -1
  6. package/README.md +26 -22
  7. package/bin/bongos.js +0 -0
  8. package/config/branding.neutral.json +5 -1
  9. package/docker-entrypoint.sh +0 -0
  10. package/docs/adr/0142-project-context-is-prose-only.md +41 -0
  11. package/docs/adr/0143-onboarding-repo-picker-oauth-scope.md +35 -0
  12. package/docs/adr/README.md +6 -0
  13. package/docs/api/openapi.json +1630 -632
  14. package/docs/api-reference.md +50 -15
  15. package/docs/file-map.md +0 -18
  16. package/docs/onboarding/diagrams/05-versioning.mmd +4 -6
  17. package/docs/onboarding/diagrams/assertions.json +5 -9
  18. package/docs/recipes/packaging-the-core.md +13 -0
  19. package/docs/recipes/upgrading-the-core.md +5 -2
  20. package/migrations/039_newcomer_friendly_column.sql +5 -0
  21. package/migrations/102_remove_box_first_provision_approval.sql +4 -0
  22. package/migrations/core_192_github_oauth_tokens.sql +33 -0
  23. package/modules/hall-ui/public/builders.js +37 -0
  24. package/modules/hall-ui/public/goals-page.js +98 -176
  25. package/modules/hall-ui/public/goals.css +183 -0
  26. package/modules/hall-ui/public/goals.html +35 -261
  27. package/modules/hall-ui/public/goals.js +7 -2
  28. package/modules/hall-ui/public/hall-widgets.js +12 -4
  29. package/modules/hall-ui/public/my-projects.html +78 -0
  30. package/modules/hall-ui/public/my-projects.js +142 -0
  31. package/modules/hall-ui/public/scouting.html +45 -0
  32. package/modules/hall-ui/public/scouting.js +73 -0
  33. package/modules/hall-ui/public/settings.css +355 -0
  34. package/modules/hall-ui/public/settings.html +86 -414
  35. package/modules/hall-ui/public/settings.js +96 -35
  36. package/modules/hall-ui/public/style.css +31 -0
  37. package/modules/hall-ui/public/watch.css +184 -0
  38. package/modules/hall-ui/public/watch.html +111 -285
  39. package/modules/hall-ui/public/watch.js +59 -21
  40. package/modules/status-ui/public/explore.html +54 -0
  41. package/modules/status-ui/public/explore.js +67 -0
  42. package/package-lock.json +2 -2
  43. package/package.json +1 -1
  44. package/public-landing/create.html +292 -19
  45. package/public-landing/index.html +11 -76
  46. package/scripts/cron/style_guide_audit.sh +0 -0
  47. package/scripts/gds/api.js +0 -0
  48. package/scripts/gds/architect-audit.js +0 -0
  49. package/scripts/gds/audit-authorship.sh +0 -0
  50. package/scripts/gds/audit-replay.sh +0 -0
  51. package/scripts/gds/box.js +0 -0
  52. package/scripts/gds/capture.js +0 -0
  53. package/scripts/gds/cli-lib.js +11 -5
  54. package/scripts/gds/conductor-dispatch.js +0 -0
  55. package/scripts/gds/coreB-carve-proof.sh +0 -0
  56. package/scripts/gds/db-backup-nightly.sh +0 -0
  57. package/scripts/gds/db-backup-offsite.sh +0 -0
  58. package/scripts/gds/doctor.js +0 -0
  59. package/scripts/gds/grade-smoke.js +0 -0
  60. package/scripts/gds/hooks/pre-push.js +0 -0
  61. package/scripts/gds/install-git-hooks.js +0 -0
  62. package/scripts/gds/lint-migrations.js +102 -0
  63. package/scripts/gds/login.js +42 -10
  64. package/scripts/gds/main-audit.js +0 -0
  65. package/scripts/gds/mirror-redact.js +5 -0
  66. package/scripts/gds/oauth-secret.js +0 -0
  67. package/scripts/gds/onboarding-analyze.js +0 -0
  68. package/scripts/gds/onboarding-ship.js +0 -0
  69. package/scripts/gds/package-core.js +263 -4
  70. package/scripts/gds/paste-token.js +0 -0
  71. package/scripts/gds/portability-smoke-art.sh +0 -0
  72. package/scripts/gds/portability-smoke-memory.sh +0 -0
  73. package/scripts/gds/provision.js +270 -11
  74. package/scripts/gds/publish-manifest.js +15 -0
  75. package/scripts/gds/push-main.js +0 -0
  76. package/scripts/gds/render-ideas.js +0 -0
  77. package/scripts/gds/run-unit-tests.js +38 -14
  78. package/scripts/gds/security-baseline-check.sh +0 -0
  79. package/scripts/gds/session.js +0 -0
  80. package/scripts/gds/setup.js +41 -0
  81. package/scripts/gds/ship.js +126 -32
  82. package/scripts/gds/smoke-batch-claim.sh +0 -0
  83. package/scripts/gds/smoke-batch-release.sh +0 -0
  84. package/scripts/gds/smoke-box.sh +0 -0
  85. package/scripts/gds/smoke-claim-consistency.sh +0 -0
  86. package/scripts/gds/smoke-dependencies.sh +0 -0
  87. package/scripts/gds/smoke-write.sh +0 -0
  88. package/scripts/gds/upgrade.js +147 -5
  89. package/scripts/hall-preview/server.js +18 -1
  90. package/scripts/migrate.sh +10 -0
  91. package/scripts/migrate_cost_ledger.sh +0 -0
  92. package/scripts/render-diagrams.sh +0 -0
  93. package/scripts/security/dep-audit.js +0 -0
  94. package/scripts/setup-builder.js +0 -0
  95. package/scripts/setup-builder.sh +0 -0
  96. package/scripts/snapshots/maybe-render-on-deploy.sh +0 -0
  97. package/src/bongos/auth.js +362 -5
  98. package/src/bongos/db.js +66 -0
  99. package/src/bongos/github-repos.js +93 -0
  100. package/src/bongos/routes/auth.js +93 -11
  101. package/src/bongos/routes/github.js +90 -0
  102. package/src/bongos/routes/instance.js +15 -3
  103. package/src/bongos/routes.js +2 -0
  104. package/src/branding.js +17 -2
  105. package/src/instance-config.js +16 -0
  106. package/src/module-api.js +48 -1
  107. package/tests/branding.mjs +29 -23
  108. package/tests/github_repos_helpers.mjs +91 -0
  109. package/tests/github_token_store.mjs +121 -0
  110. package/tests/idp_device_exchange.mjs +105 -0
  111. package/tests/idp_federation.mjs +100 -0
  112. package/tests/migrations_forward_safe.mjs +86 -0
  113. package/tests/module_api.mjs +1 -1
  114. package/tests/package_core.mjs +246 -1
  115. package/tests/provision.mjs +226 -0
  116. package/tests/pruned_lockfile_integrity.mjs +79 -0
  117. package/tests/ship_ci_deploy.mjs +60 -0
  118. package/tests/upgrade.mjs +254 -5
  119. package/.gitignore +0 -91
  120. package/scripts/hall-preview/fixtures/access-requests.json +0 -1
  121. package/scripts/hall-preview/fixtures/achievements.json +0 -76
  122. package/scripts/hall-preview/fixtures/analytics__builder.json +0 -251
  123. package/scripts/hall-preview/fixtures/audit-log.json +0 -1
  124. package/scripts/hall-preview/fixtures/boxes__cost-ledger.json +0 -1
  125. package/scripts/hall-preview/fixtures/builders__active-claims.json +0 -1
  126. package/scripts/hall-preview/fixtures/builders__directory.json +0 -81
  127. package/scripts/hall-preview/fixtures/builders__roster.json +0 -1
  128. package/scripts/hall-preview/fixtures/goals.json +0 -502
  129. package/scripts/hall-preview/fixtures/goals__25.json +0 -96
  130. package/scripts/hall-preview/fixtures/goals__26.json +0 -67
  131. package/scripts/hall-preview/fixtures/goals__3.json +0 -103
  132. package/scripts/hall-preview/fixtures/goals__35.json +0 -49
  133. package/scripts/hall-preview/fixtures/goals__conflicts.json +0 -154
  134. package/scripts/hall-preview/fixtures/goals__detail.json +0 -96
  135. package/scripts/hall-preview/fixtures/goals__inbox.json +0 -5
  136. package/scripts/hall-preview/fixtures/goals__requests.json +0 -3
  137. package/scripts/hall-preview/fixtures/grades__by-builder.json +0 -1
  138. package/scripts/hall-preview/fixtures/inbox.json +0 -330
  139. package/scripts/hall-preview/fixtures/instance.json +0 -124
  140. package/scripts/hall-preview/fixtures/leaderboard.json +0 -184
  141. package/scripts/hall-preview/fixtures/me.json +0 -476
  142. package/scripts/hall-preview/fixtures/me__recent-ships.json +0 -304
  143. package/scripts/hall-preview/fixtures/override-requests.json +0 -1
  144. package/scripts/hall-preview/fixtures/provisioning__fleet.json +0 -1
  145. package/scripts/hall-preview/fixtures/provisioning__fleet__cost-ledger.json +0 -1
  146. package/scripts/hall-preview/fixtures/provisioning__instances.json +0 -1
  147. package/scripts/hall-preview/fixtures/public__analytics__ecosystem.json +0 -516
  148. package/scripts/hall-preview/fixtures/public__bounty-table.json +0 -37
  149. package/scripts/hall-preview/fixtures/public__cost-summary.json +0 -742
  150. package/scripts/hall-preview/fixtures/public__estimation-drift.json +0 -41
  151. package/scripts/hall-preview/fixtures/public__grades.json +0 -50
  152. package/scripts/hall-preview/fixtures/public__progress.json +0 -124
  153. package/scripts/hall-preview/fixtures/public__recent-shipped.json +0 -426
  154. package/scripts/hall-preview/fixtures/public__repo-health.json +0 -160
  155. package/scripts/hall-preview/fixtures/public__repo-info.json +0 -5
  156. package/scripts/hall-preview/fixtures/security__docs.json +0 -1
  157. package/scripts/hall-preview/fixtures/security__reports.json +0 -1
  158. package/scripts/hall-preview/fixtures/sessions__search.json +0 -1
  159. package/scripts/hall-preview/fixtures/tasks.json +0 -2420
  160. package/scripts/hall-preview/fixtures/tasks__claimable.json +0 -2555
  161. package/scripts/hall-preview/fixtures/tasks__detail.json +0 -111
  162. package/scripts/hall-preview/fixtures/tasks__peer-votes__feed.json +0 -330
  163. package/scripts/hall-preview/fixtures/versions.json +0 -89
@@ -45,20 +45,17 @@ body{
45
45
  .stage{
46
46
  position:relative;z-index:1;min-height:100vh;
47
47
  display:flex;flex-direction:column;align-items:center;justify-content:center;
48
- text-align:center;padding:48px 24px 110px;gap:8px;
48
+ text-align:center;padding:48px 24px 64px;gap:8px;
49
49
  }
50
50
  /* a soft white glow behind the mascot so a white- or transparent-bg PNG blends */
51
51
  .hero-mascot{
52
52
  position:relative;width:min(540px,86vw);margin-bottom:6px;
53
- /* cap by height on short viewports so the hall CTA + sign-in hint always clear
54
- the fixed footer (task 2094); on tall desktops the width cap wins, unchanged */
55
- max-height:50vh;display:flex;justify-content:center;
56
53
  }
57
54
  .hero-mascot::before{
58
55
  content:"";position:absolute;inset:-6% -10%;z-index:-1;border-radius:50%;
59
56
  background:radial-gradient(ellipse at center, rgba(255,255,255,0.95) 0%, rgba(255,255,255,0.7) 45%, rgba(255,255,255,0) 72%);
60
57
  }
61
- .hero-mascot img{display:block;width:100%;height:auto;max-height:50vh;object-fit:contain;}
58
+ .hero-mascot img{display:block;width:100%;height:auto;}
62
59
  /* fallback shown if buddha.png is not present yet */
63
60
  .mascot-fallback{
64
61
  width:min(540px,86vw);aspect-ratio:1.6/1;border-radius:28px;
@@ -80,27 +77,9 @@ body{
80
77
  border:none;border-radius:999px;padding:15px 32px;text-decoration:none;
81
78
  display:inline-flex;align-items:center;gap:9px;
82
79
  background:var(--accent);color:#fff;box-shadow:0 12px 28px -10px var(--accent);
83
- transition:transform .12s ease, box-shadow .12s ease, background .12s ease;
80
+ transition:transform .12s ease, box-shadow .12s ease;
84
81
  }
85
82
  .btn:hover{transform:translateY(-1px);box-shadow:0 16px 34px -10px var(--accent);}
86
- .btn.ghost{background:rgba(255,255,255,0.75);color:var(--ink);box-shadow:0 6px 18px -8px rgba(42,31,23,0.25);}
87
- .btn.ghost:hover{box-shadow:0 10px 24px -8px rgba(42,31,23,0.3);}
88
- /* signed-out: the create door is visible but resting — it hints, and clicking it
89
- starts the GitHub sign-in (returning straight into /create). */
90
- .btn.resting{background:#eeddc8;color:#a08a74;box-shadow:none;}
91
- .btn.resting:hover{background:#ecd6ba;transform:none;box-shadow:none;}
92
- .cta-row{display:flex;gap:14px;flex-wrap:wrap;justify-content:center;}
93
- .signin-hint{font-size:13.5px;font-weight:600;color:var(--ink-soft);margin-top:14px;min-height:19px;}
94
- .signin-hint a{color:var(--accent-deep);text-decoration:none;font-weight:700;}
95
- .signin-hint a:hover{text-decoration:underline;}
96
- .who{
97
- position:fixed;top:16px;right:20px;z-index:2;display:flex;align-items:center;gap:9px;
98
- font-size:14px;font-weight:600;color:var(--ink-soft);min-height:30px;
99
- }
100
- .who img{width:28px;height:28px;border-radius:50%;border:2px solid #fff;box-shadow:0 2px 8px rgba(42,31,23,0.14);}
101
- .who a{color:var(--accent-deep);text-decoration:none;font-weight:700;}
102
- .who a:hover{text-decoration:underline;}
103
- .who .who-act{padding-left:10px;margin-left:2px;border-left:1px solid rgba(42,31,23,0.18);}
104
83
  .foot{
105
84
  position:fixed;bottom:18px;left:0;right:0;z-index:1;text-align:center;
106
85
  color:var(--ink-soft);font-size:13px;opacity:0.8;
@@ -119,68 +98,24 @@ body{
119
98
  </div>
120
99
  <div class="wordmark" data-brand-product>cloud bongos</div>
121
100
  <p class="tagline" data-brand-tagline>Building software is laying down a beat.</p>
122
- <div class="cta-row">
123
- <a class="btn" id="hallBtn" data-link-hall href="https://builders.cloudbongos.com">Enter the Hall →</a>
124
- </div>
125
- <p class="signin-hint" id="signinHint" aria-live="polite"></p>
101
+ <a class="btn" data-link-hall href="https://builders.cloudbongos.com">Enter the Hall →</a>
126
102
  </main>
127
- <div class="who" id="who" aria-live="polite"></div>
128
103
 
129
104
  <div class="foot">A non-profit, capture-resistant commons. Built by agents, on Cloud Bongos.</div>
130
105
 
131
106
  <script>
132
107
  /* Brand-pack fill (R57). The server injects window.__BRANDING__ synchronously
133
- ahead of this script (src/bongos/serve-internal.js); the baked markup above is
108
+ ahead of this script (src/gds/serve-internal.js); the baked markup above is
134
109
  the no-JS / standalone fallback. A differently-branded game-less instance
135
110
  overrides the product name, tagline, and Hall link from its own config. */
136
111
  (function () {
137
112
  var b = (typeof window !== 'undefined' && window.__BRANDING__) || null;
138
- if (b) {
139
- var id = b.identity || {}, copy = b.copy || {}, dom = b.domains || {};
140
- var set = function (sel, val){ if(!val) return; document.querySelectorAll(sel).forEach(function(n){ n.textContent = val; }); };
141
- if (id.productName) set('[data-brand-product]', id.productName.toLowerCase());
142
- if (copy.tagline) set('[data-brand-tagline]', copy.tagline);
143
- if (dom.buildersOrigin) document.querySelectorAll('[data-link-hall]').forEach(function(n){ n.setAttribute('href', dom.buildersOrigin); });
144
- }
145
-
146
- /* Membership-aware hall access (goal 35, task 2094). This is a PROJECT's own
147
- landing page. Signed in as an admitted builder here → "Enter the Hall".
148
- Otherwise → "Request access" (this project's own hall shows the request-
149
- access card), with a quiet "already a builder? sign in" for returning
150
- builders. Sign-in is itself gated to approved builders (auth.js), so a
151
- newcomer who tries it is routed to request access anyway. The identity +
152
- action live top-right. Degrades to the request-access visual when the API
153
- is unreachable (static preview / standalone viewing). */
154
- var hallHref = (b && b.domains && b.domains.buildersOrigin) || 'https://builders.cloudbongos.com';
155
- var signInHref = '/api/bongos/auth/web/start?return=' + encodeURIComponent('/');
156
- function esc(s) {
157
- return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) {
158
- return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c];
159
- });
160
- }
161
- var hallBtn = document.getElementById('hallBtn');
162
- var signinHint = document.getElementById('signinHint');
163
- var who = document.getElementById('who');
164
-
165
- function showSignedOut() {
166
- if (hallBtn) { hallBtn.textContent = 'Request access'; hallBtn.setAttribute('href', hallHref); }
167
- if (signinHint) signinHint.innerHTML = 'Already a builder here? <a href="' + signInHref + '">Sign in with GitHub</a>.';
168
- if (who) who.innerHTML = '<a href="' + signInHref + '">Sign in with GitHub</a>';
169
- }
170
- function showMember(me) {
171
- if (hallBtn) { hallBtn.textContent = 'Enter the Hall →'; hallBtn.setAttribute('href', hallHref); }
172
- if (signinHint) signinHint.textContent = '';
173
- if (who) who.innerHTML =
174
- (me.avatar_url ? '<img src="' + esc(me.avatar_url) + '" alt="">' : '') +
175
- '<span>' + esc(me.github_login || 'signed in') + '</span>' +
176
- '<a class="who-act" href="' + hallHref + '">Enter hall →</a>';
177
- }
178
-
179
- showSignedOut(); // default until /me resolves (and the no-JS request-access intent)
180
- fetch('/api/bongos/me', { credentials: 'same-origin' })
181
- .then(function (res) { if (!res.ok) throw new Error(String(res.status)); return res.json(); })
182
- .then(function (data) { showMember(data.builder || data.me || data); })
183
- .catch(function () { /* signed out / not a member / unreachable: request-access visual stands */ });
113
+ if (!b) return;
114
+ var id = b.identity || {}, copy = b.copy || {}, dom = b.domains || {};
115
+ function set(sel, val){ if(!val) return; document.querySelectorAll(sel).forEach(function(n){ n.textContent = val; }); }
116
+ if (id.productName) set('[data-brand-product]', id.productName.toLowerCase());
117
+ if (copy.tagline) set('[data-brand-tagline]', copy.tagline);
118
+ if (dom.buildersOrigin) document.querySelectorAll('[data-link-hall]').forEach(function(n){ n.setAttribute('href', dom.buildersOrigin); });
184
119
  })();
185
120
  </script>
186
121
  </body>
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -27,10 +27,16 @@ const SESSION_READ_PATHS = [
27
27
  ...ic.configReadPaths('gds-session.json'),
28
28
  ...ic.configReadPaths('pms-session.json'),
29
29
  ];
30
- // Default to live production. Override with <PREFIX>_API_BASE for local dev:
30
+ // The last-resort default API base — THIS instance's own publicOrigin from the
31
+ // branding pack (task 1002201). One source of truth in instance-config so no CLI
32
+ // script hard-codes another instance's URL; pre-ADR-0139 the literal
33
+ // 'https://amazonprimea.com' was baked in at four sites here, so an unconfigured
34
+ // consumer targeted the founder's (game-only) box. Override with <PREFIX>_API_BASE
35
+ // for local dev / staging:
31
36
  // CLOUDBONGOS_API_BASE=http://localhost:3000 node scripts/gds/start.js
32
37
  // Resolves the configured prefix first, then legacy GDS_/OTB_/PMS_API_BASE.
33
- const API_BASE = ic.resolveEnv('API_BASE') || 'https://amazonprimea.com';
38
+ const DEFAULT_API_BASE = ic.defaultApiBase();
39
+ const API_BASE = ic.resolveEnv('API_BASE') || DEFAULT_API_BASE;
34
40
 
35
41
  async function readSessionFile(p) {
36
42
  try {
@@ -102,7 +108,7 @@ function readSessionToken() {
102
108
  if (!j || !j.token) return null;
103
109
  return {
104
110
  token: j.token,
105
- apiBase: ic.resolveEnv('API_BASE') || j.api_base || 'https://amazonprimea.com',
111
+ apiBase: ic.resolveEnv('API_BASE') || j.api_base || DEFAULT_API_BASE,
106
112
  };
107
113
  }
108
114
 
@@ -278,7 +284,7 @@ async function apiCall(method, urlPath, body = null, opts = {}) {
278
284
  // (local dev / staging override), then the logged-in instance, then prod. OTB
279
285
  // sessions already store api_base='https://amazonprimea.com', so this is a no-op
280
286
  // for the OTB instance and only bites once someone logs in to a different one.
281
- const base = ic.resolveEnv('API_BASE') || (session && session.api_base) || 'https://amazonprimea.com';
287
+ const base = ic.resolveEnv('API_BASE') || (session && session.api_base) || DEFAULT_API_BASE;
282
288
  const headers = {
283
289
  'Content-Type': 'application/json',
284
290
  'User-Agent': 'amazonprimea-gds-cli/1.0',
@@ -349,7 +355,7 @@ async function cliClient(opts = {}) {
349
355
  printInstanceMismatchAndExit(mm);
350
356
  }
351
357
  }
352
- const base = ic.resolveEnv('API_BASE') || (session && session.api_base) || 'https://amazonprimea.com';
358
+ const base = ic.resolveEnv('API_BASE') || (session && session.api_base) || DEFAULT_API_BASE;
353
359
  const token = (session && session.token) || null;
354
360
  const key = `${base}|${token || ''}|${allowUnauthenticated}|${allowBoxScope}`;
355
361
  if (_clientCache.has(key)) return _clientCache.get(key);
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -0,0 +1,102 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ // scripts/gds/lint-migrations.js — core-migration FORWARD-SAFETY guard (task 1002223 / audit H10,
4
+ // finding F6).
5
+ //
6
+ // `bongos upgrade` auto-rollback reverts the CODE pin but DELIBERATELY NOT the database
7
+ // (upgrade.js: "the DB is deliberately NOT rolled back … core migrations are additive + forward-
8
+ // safe"). That safety rested on a convention nothing checked. A core migration is FORWARD-SAFE when
9
+ // the PREVIOUS code version still runs against the post-migration schema — so a code-only rollback
10
+ // never strands old code on a schema it can't read. This lints core migrations for operations that
11
+ // BREAK that property and fails unless the migration explicitly acknowledges the exception with a
12
+ // -- forward-unsafe: <reason>
13
+ // pragma (a deliberate, documented decision — e.g. a historical migration, or one paired with a
14
+ // two-step deploy).
15
+ //
16
+ // SAFE (never flagged): CREATE TABLE/INDEX/…, ADD COLUMN, INSERT/UPDATE, and DROP-then-CREATE of an
17
+ // INDEX / TRIGGER / VIEW / FUNCTION / POLICY (the recreate-in-place idempotency pattern) — none of
18
+ // these remove state the old code depends on.
19
+ // UNSAFE (flagged): DROP TABLE, DROP COLUMN, RENAME COLUMN, ALTER TABLE … RENAME TO, ALTER COLUMN …
20
+ // TYPE, SET NOT NULL — each removes / renames / retypes existing state old code reads.
21
+ //
22
+ // Usage:
23
+ // node scripts/gds/lint-migrations.js # STRICT: exit 1 on any unacknowledged unsafe migration
24
+ // node scripts/gds/lint-migrations.js --warn # report only, always exit 0 (migrate.sh uses this)
25
+ // node scripts/gds/lint-migrations.js --dir <path> # scan a specific migrations dir (default: <repo>/migrations)
26
+
27
+ const fs = require('node:fs');
28
+ const path = require('node:path');
29
+
30
+ const REPO_ROOT = path.resolve(__dirname, '..', '..');
31
+
32
+ // The acknowledgement pragma. Lives in a SQL comment; matched on the raw file text.
33
+ const ACK_RE = /^\s*--\s*forward-unsafe\s*:\s*(.+)$/im;
34
+
35
+ // Genuinely forward-unsafe statement shapes. Ordered; a file matching ANY is unsafe.
36
+ const UNSAFE = [
37
+ ['drop-table', /\bdrop\s+table\b/i],
38
+ ['drop-column', /\bdrop\s+column\b/i],
39
+ ['rename-column', /\brename\s+column\b/i],
40
+ ['rename-table', /\balter\s+table\s+[^\s;]+\s+rename\s+to\b/i],
41
+ ['alter-column-type', /\balter\s+column\b[^;]*\btype\b/i],
42
+ ['set-not-null', /\bset\s+not\s+null\b/i],
43
+ ];
44
+
45
+ // Strip block + line comments so a pattern that appears only in prose/comments never counts as an
46
+ // operation. (The ACK pragma is detected separately, on the raw text, since it IS a comment.)
47
+ function stripSqlComments(sql) {
48
+ return String(sql).replace(/\/\*[\s\S]*?\*\//g, ' ').replace(/--[^\n]*/g, ' ');
49
+ }
50
+
51
+ // Classify one migration's SQL: { unsafe: [names], acknowledged: bool, ackReason }.
52
+ function scanMigrationSql(sql) {
53
+ const code = stripSqlComments(sql);
54
+ const unsafe = [];
55
+ for (const [name, re] of UNSAFE) if (re.test(code)) unsafe.push(name);
56
+ const ack = String(sql).match(ACK_RE);
57
+ return { unsafe, acknowledged: !!ack, ackReason: ack ? ack[1].trim() : null };
58
+ }
59
+
60
+ // Scan a migrations directory. Returns { violations, acknowledged } — each entry { file, unsafe, ackReason }.
61
+ // `violations` = unsafe WITHOUT the pragma (fail-closed); `acknowledged` = unsafe WITH it (documented).
62
+ function scanMigrationsDir(dir, fsImpl = fs) {
63
+ let files;
64
+ try { files = fsImpl.readdirSync(dir).filter((f) => f.endsWith('.sql')); }
65
+ catch { return { violations: [], acknowledged: [], scanned: 0 }; }
66
+ const violations = [];
67
+ const acknowledged = [];
68
+ for (const f of files.sort()) {
69
+ const r = scanMigrationSql(fsImpl.readFileSync(path.join(dir, f), 'utf8'));
70
+ if (r.unsafe.length === 0) continue;
71
+ (r.acknowledged ? acknowledged : violations).push({ file: f, unsafe: r.unsafe, ackReason: r.ackReason });
72
+ }
73
+ return { violations, acknowledged, scanned: files.length };
74
+ }
75
+
76
+ function arg(name, fallback = null) {
77
+ const i = process.argv.indexOf(name);
78
+ return i >= 0 && i + 1 < process.argv.length ? process.argv[i + 1] : fallback;
79
+ }
80
+
81
+ function main() {
82
+ const warnOnly = process.argv.includes('--warn');
83
+ const dir = path.resolve(arg('--dir', path.join(REPO_ROOT, 'migrations')));
84
+ const { violations, acknowledged, scanned } = scanMigrationsDir(dir);
85
+
86
+ for (const a of acknowledged) {
87
+ console.error(`[migrations] forward-unsafe (acknowledged): ${a.file} [${a.unsafe.join(', ')}] — ${a.ackReason}`);
88
+ }
89
+ if (violations.length === 0) {
90
+ console.error(`[migrations] forward-safety: OK — ${scanned} core migration(s) scanned, ${acknowledged.length} acknowledged exception(s), 0 unacknowledged.`);
91
+ return 0;
92
+ }
93
+ console.error(`[migrations] forward-safety: ${warnOnly ? 'WARNING' : 'FAILED'} — ${violations.length} core migration(s) contain a forward-UNSAFE operation with no acknowledgement:`);
94
+ for (const v of violations) console.error(` ${v.file} [${v.unsafe.join(', ')}]`);
95
+ console.error(' A code-only `bongos upgrade` rollback does NOT revert the DB, so old code would run on this schema.');
96
+ console.error(' If intentional, add a top-of-file SQL comment: -- forward-unsafe: <why it is safe here / the two-step plan>');
97
+ return warnOnly ? 0 : 1;
98
+ }
99
+
100
+ module.exports = { scanMigrationSql, scanMigrationsDir, stripSqlComments, UNSAFE, ACK_RE };
101
+
102
+ if (require.main === module) process.exit(main());
@@ -80,12 +80,26 @@ function prompt(q) {
80
80
 
81
81
  const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
82
82
 
83
- // Run the GitHub device flow against `base`. Returns one of:
83
+ // Run the device flow and return one of:
84
84
  // { state: 'ok', token, builder } | { state: 'not_approved' } | { state: 'expired' }
85
- async function runDeviceFlow(base, { log }) {
86
- const start = await fetchJson(`${base}/api/gds/auth/device/start`, { method: 'POST' });
87
- if (start.status === 503 || (start.data && start.data.error === 'auth_not_configured')) {
88
- throw new Error('that instance has no GitHub sign-in configured yet — ask its Archon to set it up.');
85
+ //
86
+ // Two modes (ADR 0141 §1). Direct: run GitHub device flow against the instance's
87
+ // own app. Federated (idp = { origin, client_id } from the instance manifest): run
88
+ // the device flow against the HUB, then exchange the hub-signed assertion at the
89
+ // instance's /auth/device/exchange — a hosted instance has no GitHub app of its own.
90
+ // The instance's OWN admission gate runs at poll (direct) or exchange (federated),
91
+ // so the stranger path is identical for both.
92
+ async function runDeviceFlow(base, { log, idp = null }) {
93
+ const startUrl = idp ? `${idp.origin}/api/gds/sso/device/start` : `${base}/api/gds/auth/device/start`;
94
+ const start = await fetchJson(startUrl, {
95
+ method: 'POST',
96
+ // Federated: the hub resolves + binds the client from the USER-TYPED origin
97
+ // (`base`), never a client_id we supply — so this CLI can only ever mint an
98
+ // assertion scoped to the instance the user actually typed (ADR 0141 §1).
99
+ ...(idp ? { body: JSON.stringify({ origin: base }) } : {}),
100
+ });
101
+ if (start.status === 503 || (start.data && (start.data.error === 'auth_not_configured' || (start.data.error && start.data.error.code === 'idp_signing_unconfigured')))) {
102
+ throw new Error('that instance has no sign-in configured yet — ask its Archon to set it up.');
89
103
  }
90
104
  if (!start.ok || !start.data || !start.data.device_code) {
91
105
  throw new Error(`could not start sign-in (HTTP ${start.status}).`);
@@ -103,15 +117,29 @@ async function runDeviceFlow(base, { log }) {
103
117
  openBrowser(verification_uri);
104
118
  log(' Waiting for you to approve in the browser…');
105
119
 
120
+ const pollUrl = idp ? `${idp.origin}/api/gds/sso/device/poll` : `${base}/api/gds/auth/device/poll`;
106
121
  const deadline = Date.now() + expires * 1000;
107
122
  while (Date.now() < deadline) {
108
123
  await sleep(interval * 1000);
109
- const poll = await fetchJson(`${base}/api/gds/auth/device/poll`, {
124
+ const poll = await fetchJson(pollUrl, {
110
125
  method: 'POST',
126
+ // The hub uses the client BOUND to device_code at start; the CLI names no
127
+ // audience at poll either (ADR 0141 §1).
111
128
  body: JSON.stringify({ device_code }),
112
129
  });
113
130
  const st = poll.data && poll.data.state;
114
- if (st === 'ok') return { state: 'ok', token: poll.data.token, builder: poll.data.builder };
131
+ if (st === 'ok') {
132
+ if (!idp) return { state: 'ok', token: poll.data.token, builder: poll.data.builder };
133
+ // Federated: exchange the hub assertion at the instance for a real session.
134
+ const ex = await fetchJson(`${base}/api/gds/auth/device/exchange`, {
135
+ method: 'POST',
136
+ body: JSON.stringify({ assertion: poll.data.assertion }),
137
+ });
138
+ const exState = ex.data && ex.data.state;
139
+ if (exState === 'not_approved' || (ex.status === 403 && exState !== 'pending')) return { state: 'not_approved' };
140
+ if (!ex.ok || !ex.data || !ex.data.token) throw new Error(`sign-in exchange failed (HTTP ${ex.status}).`);
141
+ return { state: 'ok', token: ex.data.token, builder: ex.data.builder };
142
+ }
115
143
  if (st === 'not_approved' || (poll.status === 403 && st !== 'pending')) return { state: 'not_approved' };
116
144
  if (st === 'denied') throw new Error('sign-in was denied in the browser.');
117
145
  if (st === 'expired') return { state: 'expired' };
@@ -146,10 +174,14 @@ async function main() {
146
174
  }
147
175
  const instanceName = man.data.name || base;
148
176
  const admission = (man.data.admission && man.data.admission.policy) || 'invite-only';
149
- log(` Signing in to ${instanceName}${admission === 'invite-only' ? ' (invite-only)' : ''}`);
177
+ // Federation (ADR 0141 §1): a hosted instance advertises auth.idp = { origin,
178
+ // client_id } — sign-in then runs through the Cloud Bongos hub, not a local
179
+ // GitHub app. null → the direct-GitHub path, unchanged.
180
+ const idp = (man.data.auth && man.data.auth.idp) || null;
181
+ log(` Signing in to ${instanceName}${admission === 'invite-only' ? ' (invite-only)' : ''}${idp ? ' — via Cloud Bongos platform sign-in' : ''}`);
150
182
 
151
183
  // 2. Device flow.
152
- let flow = await runDeviceFlow(base, { log });
184
+ let flow = await runDeviceFlow(base, { log, idp });
153
185
 
154
186
  // 3. Stranger path: not admitted → file the request, poll until an Archon 👍s,
155
187
  // then finish sign-in with a fresh device flow (the first code is spent).
@@ -193,7 +225,7 @@ async function main() {
193
225
  }
194
226
  process.stdout.write('.');
195
227
  }
196
- flow = await runDeviceFlow(base, { log });
228
+ flow = await runDeviceFlow(base, { log, idp });
197
229
  }
198
230
 
199
231
  if (flow.state !== 'ok') {
File without changes
@@ -185,6 +185,11 @@ function deriveInstanceLiterals(branding = {}, extra = {}) {
185
185
  add(tokens, company.name);
186
186
  { const label = registrableLabel(company.site); if (label) add(tokens, label); }
187
187
 
188
+ // `extra` also carries a caller-supplied FIXED FLOOR: the package-core neutral-floor gate
189
+ // (task 1002217 / audit H4) injects prior-instance tokens/owners here so published DOCS de-brand
190
+ // against them and the fail-closed gate fires EVEN WITH NO branding.json. Those floor literals
191
+ // live with the CALLER (package-core), never hardcoded in this file — so this redactor's self-leak
192
+ // invariant (tests/mirror_redact.mjs) keeps holding while the floor is still enforced.
188
193
  for (const t of extra.tokens || []) add(tokens, t);
189
194
  for (const o of extra.owners || []) add(owners, o);
190
195
 
File without changes
File without changes
File without changes