@bongos/core 1.19.68 → 1.19.70

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 CHANGED
@@ -2,22 +2,22 @@
2
2
  "artifact": "bongos-core",
3
3
  "manifest_schema": 1,
4
4
  "generator": "scripts/gds/package-core.js",
5
- "core_version": "1.19.68",
6
- "core_contract": "1.19.68",
7
- "source_commit": "0b0e7e636f0a28d01e1bc8f354a06981d6f1ef70",
5
+ "core_version": "1.19.70",
6
+ "core_contract": "1.19.70",
7
+ "source_commit": "b92ae4a4d4458ff81409d2aff12bfc88bd0ef50d",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-08-12T14:42:16.480Z",
9
+ "built_at": "2026-08-12T15:26:39.115Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
- "docs_redacted": 292,
12
+ "docs_redacted": 293,
13
13
  "agent_docs_stubbed": 21,
14
- "functional_verbatim": 1222,
14
+ "functional_verbatim": 1223,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 1535,
20
- "tree_sha256": "cc9278ac9df146953fa33e771313791e8ce82f5d69b05a307763c48bae6207e9",
19
+ "file_count": 1537,
20
+ "tree_sha256": "56c1de962c6965257fb63c72e0201b2a0652833acf9ade499ed57c96e9650adb",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/blocker-review/SKILL.md",
@@ -1304,10 +1304,15 @@
1304
1304
  "mode": "0000644",
1305
1305
  "sha256": "3d260a52d4aa494e2df58f0db1c038c091786fbea99bad77d35b86b1a2024cf3"
1306
1306
  },
1307
+ {
1308
+ "path": "docs/adr/0167-gate-trust-link-agpl-default.md",
1309
+ "mode": "0000644",
1310
+ "sha256": "bb187a29953ba5c316977f09fe86e26ca200693b23cf5c41ece93e9dd24f337e"
1311
+ },
1307
1312
  {
1308
1313
  "path": "docs/adr/README.md",
1309
1314
  "mode": "0000644",
1310
- "sha256": "59110e7260facffd2771e1b3a4c411f238cd95d49ff07f1ac2be9541b9fda4c2"
1315
+ "sha256": "b0387cdc43a67c4b9834691155d1fa9454930cb70b0660bb55a0f7e819c7ec90"
1311
1316
  },
1312
1317
  {
1313
1318
  "path": "docs/api-reference.md",
@@ -1337,7 +1342,7 @@
1337
1342
  {
1338
1343
  "path": "docs/canonical-permissions.md",
1339
1344
  "mode": "0000644",
1340
- "sha256": "b7f66c6f1fbed274e5ba3295c06db8fc8edb84edd44f6ce92f6734e18267e3d9"
1345
+ "sha256": "96ae791fc131dc389158f326e5fc698283de3d67d3396003bae2632f24a148b0"
1341
1346
  },
1342
1347
  {
1343
1348
  "path": "docs/design-contract.md",
@@ -2977,7 +2982,7 @@
2977
2982
  {
2978
2983
  "path": "modules/hall-ui/public/builders.js",
2979
2984
  "mode": "0000644",
2980
- "sha256": "5bdd907f10ae15f8efdb55adce3ce23991a376122eb79c549e025f8f4845fae5"
2985
+ "sha256": "076103658308541ef6b976bc333c67a2dc0069913fadbd5551c6241cc320bda4"
2981
2986
  },
2982
2987
  {
2983
2988
  "path": "modules/hall-ui/public/chime.wav",
@@ -3282,7 +3287,7 @@
3282
3287
  {
3283
3288
  "path": "modules/hall-ui/public/shell.js",
3284
3289
  "mode": "0000644",
3285
- "sha256": "56c1450dd72fcda19841f3a0a98b918b1b875f49971e93d60218fb62e8a21dad"
3290
+ "sha256": "ac31de1ecb51a0899d043b7fa098693ae69cbc3ebe858c97e6c8df2c4cef8015"
3286
3291
  },
3287
3292
  {
3288
3293
  "path": "modules/hall-ui/public/style.css",
@@ -4042,12 +4047,12 @@
4042
4047
  {
4043
4048
  "path": "package-lock.json",
4044
4049
  "mode": "0000644",
4045
- "sha256": "9319b9acd693e6a9804c215db5ed78ae38f4caf1a698e0596972bfef9555b91c"
4050
+ "sha256": "68b3bc7974e4f90d5291ba89dc3451d1cde4db4cff174ba3ed1347e4599c77da"
4046
4051
  },
4047
4052
  {
4048
4053
  "path": "package.json",
4049
4054
  "mode": "0000644",
4050
- "sha256": "147790e86972f4273aa18bd24bf508797c4470a4491a20a9614207edeb2e3478"
4055
+ "sha256": "505474194374951dfc13ffca64f10c469da2782e1756214c3efb3a60b8151b68"
4051
4056
  },
4052
4057
  {
4053
4058
  "path": "public-docs/index.html",
@@ -4292,7 +4297,7 @@
4292
4297
  {
4293
4298
  "path": "public-landing/create.html",
4294
4299
  "mode": "0000644",
4295
- "sha256": "1874575c2241df1e868d158f3fc4c6c123b8c35531a5b1425de9974230da6510"
4300
+ "sha256": "419539862b5453d613a5ed3758a7d8fa677d16e463895ae64e6268a8d59372ba"
4296
4301
  },
4297
4302
  {
4298
4303
  "path": "public-landing/gate/README.md",
@@ -5642,7 +5647,7 @@
5642
5647
  {
5643
5648
  "path": "src/bongos/serve-internal.js",
5644
5649
  "mode": "0000644",
5645
- "sha256": "2f88796346b1c3a6dc49e25766b223fbf2d99e34b38b9a0f1e64a6fb9a59856d"
5650
+ "sha256": "006a51d3bd71cc496742e9399c53dc5b1ebd18c87a54e675aabda6ac312b4b96"
5646
5651
  },
5647
5652
  {
5648
5653
  "path": "src/bongos/uptime-poller.js",
@@ -5672,7 +5677,7 @@
5672
5677
  {
5673
5678
  "path": "src/module-api.js",
5674
5679
  "mode": "0000644",
5675
- "sha256": "28675b76775e2936b6ba819fa37c6628f96b259b850248f2ce1033e9bcd7fe20"
5680
+ "sha256": "993ad9f8c4be17c840767dff8c4e092a2ea55c5cbe73a09a53371e31cccfa720"
5676
5681
  },
5677
5682
  {
5678
5683
  "path": "src/module-loader/loader.js",
@@ -6697,7 +6702,12 @@
6697
6702
  {
6698
6703
  "path": "tests/hall_nav.mjs",
6699
6704
  "mode": "0000644",
6700
- "sha256": "c2456b6fe8845c5778e9e57323cd7a0f2f9d6e96ef9d3a9d4f47f0312da07964"
6705
+ "sha256": "b0da8fb0e46c57419821dbf8cb5ddeda3c98da76d1b2918c3ce77798ee2768fb"
6706
+ },
6707
+ {
6708
+ "path": "tests/hall_page_gate_map.mjs",
6709
+ "mode": "0000644",
6710
+ "sha256": "1e59d5608c205aa6f271c5efd0acb7899f4996dfbaecbf35c387737f19c735f5"
6701
6711
  },
6702
6712
  {
6703
6713
  "path": "tests/hall_palette.mjs",
@@ -7037,7 +7047,7 @@
7037
7047
  {
7038
7048
  "path": "tests/onboard.mjs",
7039
7049
  "mode": "0000644",
7040
- "sha256": "6a66477263031b252d6a92d5036304a78e11cbbd9e7230d836c4ec96a9297fdd"
7050
+ "sha256": "40d4656e2170370cd3b662911502579fed595fd66fd25b8add65d488a28bb834"
7041
7051
  },
7042
7052
  {
7043
7053
  "path": "tests/onboarding_gate.mjs",
@@ -0,0 +1,24 @@
1
+ # 0167 — The /create gate trust link defaults to the AGPL license text, not the npm package page
2
+
3
+ - **Status:** Accepted — built in task 1002705.
4
+ - **Date:** 2026-08-12
5
+ - **Deciders:** jaxri (Archon) — autonomous session; the residual choice is the owner's (idea 1000700).
6
+ - **Task:** task 1002705 · **Goal:** 1000059 (projects surface revamp) · gap 1 of [`docs/audits/project-creation-gap-list.md`](../audits/project-creation-gap-list.md).
7
+
8
+ ## Context
9
+
10
+ The /create gate's "open source" trust link pointed at `github.com/example-owner/Claude-OTB` — a 404 even authenticated (the real core repo is private). Task 1002705's brief said to "default to the npm link so the 404 dies now," on the premise that `@cloudbongos/core` is public on the registry.
11
+
12
+ That premise is false, verified 2026-08-12 against `registry.npmjs.org`: **both `@cloudbongos/core` and `@bongos/core` 404 anonymously** — they are private scoped packages (ADR 0134 publishes the core privately; npm shows private packages as 404 to signed-out visitors). The [ADR 0099](<redacted>.md) public mirror is deliberately dormant until history clears the 180-day redaction window (~2026-11). So today there is **no browsable public code artifact at all** — following the brief literally would swap one dead link for another.
13
+
14
+ ## Decision
15
+
16
+ Point the gate's "open source" link at the **AGPL-3.0 license text** (`https://www.gnu.org/licenses/agpl-3.0.html`) and tighten the copy to "open source (AGPL-3.0)". The license is the one live, instance-neutral artifact behind the claim today ([ADR 0065](<redacted>.md) — AGPL is the governance keystone), and the link carries no instance identity into the portable core.
17
+
18
+ The upgrade to a *browsable-code* link is the owner's call, filed as **idea 1000700** with three options: flip the repo public now (forfeits the redaction window — weigh ADR 0065 §4), publish a public npm package, or wait for the ADR 0099 mirror (~2026-11, zero extra work). When one lands, repoint the `finePrint` link in `public-landing/create.html`; the pin is `tests/onboard.mjs` "the /create gate trust link points at a live artifact".
19
+
20
+ ## Alternatives considered
21
+
22
+ - **The npm package page (the brief's default)** — rejected: verified 404 for anonymous visitors; the gate's audience is exactly the signed-out visitor.
23
+ - **Make the repo public now** — not a builder's call; forfeits the deliberate 6-month redaction window (ADR 0065/0099). Surfaced to the owner instead (idea 1000700).
24
+ - **Drop the link, keep plain text** — dies the 404 but weakens the claim; the license link is strictly more information at the same cost.
@@ -239,5 +239,6 @@ This keeps the decision history honest and traceable.
239
239
  | 0164 | [**The implausible-pass guard reads substance + evidence; the Metic+ honor-system residual is explicit** ([task 1002658](https://cloudbongos.com/builders#/task/1002658); grader-hardening goal 1000056). The guard fired only on `issues.length===0` — one fabricated `{}` defeated it, its magnitude inputs are client-supplied, and every in-sample firing (eval 2-for-2, tasks 1002643 ×2, 1002663) was a false positive on an honest unanimous zero-issue panel PASS. Redesign: count only SUBSTANTIVE issues (known severity + non-empty summary — padding trips the guard regardless), and waive the zero-issue pass when `signals.workers` carries the full &ge;3-worker panel shape, stamped `implausible_pass_waived` for /grade-audit. The residual is named, not hidden: every grade-time input is client-authored, so plausibility at grade time is honest-mistake hygiene; the binding authorities on the real diff are land-time (CI unit, gate-review, main audit) — Metic+ grade-time honesty is accepted honor-system per this ADR. The ADR-0042 CI grade-gate (land it vs record permanent deferral) goes to the owner as a blocker, not decided here. No approval step added or removed (ADR 0162).](<redacted>.md) | GDS / grading |
240
240
  | 0165 | [**Instances are hookless by default: no scaffolded `.husky/`; local hooks stay an instance-owned opt-in** ([task 1002641](https://cloudbongos.com/builders#/task/1002641); idea 1000655). Task 1002608 delivered the ADR 0043 Layer 1 pre-push gate for the CORE repo, but nothing materializes `.husky/` on a scaffolded standalone instance (`publish-manifest.js` excludes it by design). Decision: that is the accepted posture, not a gap. Sub-Metic instance builders hold no push credential at all (server-mediated publish, ADR 0053/0055) — a client pre-push hook guards a push that cannot happen; a client hook is not a security boundary (ADR 0016); the core cannot maintain hooks it deliberately does not ship (scaffolded hooks would be born orphans — the audit-authorship rot lesson, task 1002640); and ADR 0162 puts gates in CI, rank-consistent and hands-free. Secrets hooks follow the same rule (the PR-time CI secret scan enforces). The opt-in stays real and instance-owned: an instance commits its own `.husky/` and the core's `install-git-hooks.js` (already run by `/builder-setup`) activates it. Rejected: scaffolding wrappers at init, an `--with-hooks` flag (speculative), treating hooklessness as a regression. Revisit if an instance model has builders pushing directly with their own credentials.](<redacted>.md) | provisioning / trust boundary |
241
241
  | 0166 | [**Module maintenance & deprecation policy: upkeep is a declared `maintenance` block, and `bongos upgrade` surfaces rot in DISABLED modules too** ([task 1001773](https://cloudbongos.com/builders#/task/1001773)). Implements [ADR 0107](<redacted>.md) §7, which named the catalog-rot problem and deferred the mechanics. `module.json` gains an optional-in-schema, required-to-upstream `maintenance` block — `status` one of `core-maintained` / `maintained` / `deprecated` / `orphaned`, plus `since` / `removeAfter` / `successor` / `note`. Each status owes something the validator enforces: `maintained` requires a named top-level `maintainer` (an unnamed owner IS the orphaning failure), a sunset status requires a deprecation path, and `removeAfter` is rejected on a non-sunset status. `orphaned` is deliberately first-class — a policy that only permits happy statuses gets satisfied by lying. The load-bearing fix: accepted modules ship `default:false` (§6), so every rotted catalog entry is DISABLED by construction — and `preflightModules()` returned only `blocking`, discarding `warnings`, so a real `bongos upgrade --to X` reported `"all enabled modules satisfy the target"` while saying nothing about them. It now returns + prints `warnings`/`deprecations`/`expired` with the maintainer NAMED, before the blocking check returns (so a refused bump reports them too). Enabled mismatches still refuse; disabled ones still never block — report, don't block, because blocking an instance's bump on a catalog entry it never opted into makes the shared catalog a liability. `bongos module check`/`submit` refuse an undeclared posture or an already-sunset module; `GET /modules` + the hall Modules tab show an `upkeep:` chip (incl. `undeclared`); all 17 bundled modules stamped `core-maintained` with a unit test holding the line. Rejected: blocking on disabled rot, a required manifest field (breaks every existing manifest), DB-tracked upkeep (must travel with the module, ADR 0107 §2), auto-deprecating on incompatibility (a fact is not a decision), a separate MAINTAINERS file.](<redacted>.md) | modules / upstreaming |
242
+ | 0167 | [**The /create gate trust link defaults to the AGPL-3.0 license text, not the npm package page** ([task 1002705](https://cloudbongos.com/builders#/task/1002705); gap 1 of the project-creation audit). The gate's "open source" link pointed at a nonexistent repo (404); the task brief's suggested default — the npm package page — is ALSO dead for the gate's actual audience (signed-out visitors): `@cloudbongos/core` and `@bongos/core` both 404 anonymously (private scopes, ADR 0134), and the ADR 0099 public mirror stays dormant until ~2026-11. The license text is the one live, instance-neutral artifact behind the claim today. The browsable-code upgrade is the owner's filed choice (idea 1000700): public repo now (forfeits the ADR 0065 §4 redaction window) / public npm package / wait for the mirror. Rejected: the npm link (verified dead), flipping the repo public (not a builder's call), plain unlinked text (less information, same cost).](<redacted>.md) | public landing / governance |
242
243
 
243
244
  > ⚠️ **Numbering collision (known):** two ADRs share `0008`. The numbers were assigned in parallel sessions before the lock could be enforced. The files are kept under their existing filenames to preserve git history, but the table disambiguates them as `0008-a` / `0008-b`. New ADRs should resume at `0040` to avoid further collisions. (`0024` is also a real on-disk duplicate: the cloneable-repo/local-first-memory ADR and the multi-agent-system-architecture ADR were both numbered `0024` in parallel; both keep their filenames, disambiguated as `0024-a` / `0024-b` like `0008`. Likewise `0025` ended up with two on-disk files — the offsite-backup ADR and the structured-criterion↔task-link ADR — both kept under their existing filenames and disambiguated as `0025-a` / `0025-b`; separately, the BFG ADR had also claimed `0025` on 2026-05-30 and yielded to `0026`. A third happened 2026-06-06: `0036` was claimed in parallel by the builders-subdomain ADR ([#726](https://example.com/builders#/task/726)) and this Discord-channels ADR ([#727](https://example.com/builders#/task/727)); the Discord-channels ADR yielded to `0037`. A fourth landed 2026-06-10: `0044` was claimed in parallel by the palette-replacement ADR and the per-box game-preview ADR ([#897](https://example.com/builders#/task/897)); both keep their filenames, disambiguated as `0044-a` / `0044-b` like `0008`. A fifth was caught pre-land 2026-06-13: `0050` was claimed in parallel by the device-flow-admission ADR ([#1000](https://example.com/builders#/task/1000)) and the sandbox-for-everyone ADR ([#1001](https://example.com/builders#/task/1001)); the latter yielded to `0052` before merging, so there is no `0050` duplicate. A sixth was caught pre-land 2026-06-21: the tool-agnostic-design-layer ADR ([#1375](https://example.com/builders#/task/1375)) first collided at `0079` with the value-gated-autonomous-engagement ADR, then at `0080` with the llm-cache-api-transport ADR (both part of the 0075–0080 LLM-cost-reduction series, landed on `main` in parallel); it yielded to `0081` before merging, so there is no `0079`/`0080` design-layer duplicate. A seventh landed 2026-06-23: `0084` was claimed in parallel by the auto-derived-per-task-rank-floor ADR and the remove-default-spend-cap ADR (task 1499); the rank-floor ADR landed on `main` first, so the spend-cap ADR yielded to `0085`, leaving no `0084` duplicate. An eighth landed 2026-06-27: `0095` was claimed in parallel by the borrowed-memory-and-retrieval-concepts ADR (PR #459, landed first) and the cross-agent-context-management ADR ([#1651](https://example.com/builders#/task/1651), PR #460); both keep their filenames, disambiguated as `0095-a` / `0095-b` like `0008`. These two are companions — `0095-a` adopts the memory/retrieval concepts and explicitly defers the grep-vs-semantic question to `0095-b`.)
@@ -179,7 +179,9 @@ or missing ranks fail closed.
179
179
  **[ADR 0157](adr/<redacted>.md) added the whole operational block here** —
180
180
  20 permissions that had been Archon-gated only by ADR 0016's fail-closed default, never by a decision:
181
181
  the governance/monitoring **pages** (`/watch`, `/harbor`, `/gate` — `page.view.governance`) and
182
- `GET /api/gds/sessions/search`; the **dev-box + provisioning fleets** (`GET /boxes[/cost-ledger]`,
182
+ `GET /api/gds/sessions/search` (whose own `/sessions` page had no server gate at all until
183
+ task 1002710 put it behind `page.view.governance` alongside its three group-mates); the
184
+ **dev-box + provisioning fleets** (`GET /boxes[/cost-ledger]`,
183
185
  `POST /boxes/:b/close`, `PATCH .../block`, `GET /provisioning/fleet[/cost-ledger]`,
184
186
  `POST .../force-teardown`); **security adjudication** (`GET /security/reports`,
185
187
  `POST /security/reports/:id/{confirm,dispute,fix,link-fix}`, `/security/docs` CRUD);
@@ -493,16 +493,17 @@
493
493
  // Roll) stays hidden so a fresh arrival isn't drowned before they've
494
494
  // shipped their first works. The Settings link is the exception (task
495
495
  // 1291): a newcomer needs it for first-time setup — CLI re-auth (Settings
496
- // → CLI Access) and choosing their craft both live there. #766 also hides the
497
- // citizen's primer + the "how the system works" maps — a newcomer's path is
498
- // action-only. #770: the Work Board link IS shown to a Xenos — a newcomer
499
- // needs to find the work they can claim, so the path "find work → claim →
500
- // ship three" stays one click away; the rank ladder also stays so they can
501
- // see what they're climbing toward. This is cosmetic UX only — every data
502
- // endpoint stays server-rank-gated regardless (the Work Board's claimable
503
- // feed only returns work accessible to the caller's rank), and a Xenos who
504
- // types /primer or /diagrams directly is bounced to /builders/not-ready by
505
- // the server (#770). Set early in loadAll so the async karma/acclaim feeds
496
+ // → CLI Access) and choosing their craft both live there. The citizen's
497
+ // primer and the "how the system works" maps are ALSO visible to a Xenos as
498
+ // of task 1002710 — #766/#770 had hidden them on an "action-only path"
499
+ // reading, but they are the onboarding documents, so the reader they were
500
+ // written for was the one being turned away. #770: the Work Board link IS
501
+ // shown to a Xenos — a newcomer needs to find the work they can claim, so the
502
+ // path "find work → claim → ship three" stays one click away; the rank ladder
503
+ // also stays so they can see what they're climbing toward. This is cosmetic
504
+ // UX only — every data endpoint stays server-rank-gated regardless (the Work
505
+ // Board's claimable feed only returns work accessible to the caller's rank).
506
+ // Set early in loadAll so the async karma/acclaim feeds
506
507
  // (which un-hide their own scroll on data) respect it; the section-hiding
507
508
  // sweep then runs before renderNav so the chip strip matches.
508
509
  const NEWCOMER_VISIBLE_SECTIONS = new Set([
@@ -136,14 +136,22 @@
136
136
  { group: 'Community', gate: 'member', items: [
137
137
  { id: 'leaderboard', label: 'Leaderboard', icon: 'leaderboard', href: '/#leaderboard', view: 'leaderboard', gate: 'member' },
138
138
  // task 1002577: the cross-project builder directory (/scouting graduated
139
- // into the hall). Archon-gated to MATCH its data — /api/gds/scouting is
140
- // deliberately "NOT shown to builders" (ADR 0141 §4) until spec R14 widens
141
- // it; this gate is cosmetic, requireProjectCuratePage is the wall. The
142
- // module gate hides the tab on instances without the platform-identity hub.
143
- { id: 'people', label: 'People', icon: 'people', href: '/people', gate: 'archon', module: 'platform-identity' },
139
+ // into the hall). Gated to MATCH its data — /api/gds/scouting checks
140
+ // `project.curate`, whose catalog floor is METIC. This read 'archon' until
141
+ // task 1002710: the nav was stricter than the wall, so a Metic could open
142
+ // /people by URL but never saw the link. Cosmetic either way —
143
+ // requireProjectCuratePage is the wall — but a nav that disagrees with the
144
+ // gate teaches the ladder wrong. The module gate hides the tab on
145
+ // instances without the platform-identity hub.
146
+ { id: 'people', label: 'People', icon: 'people', href: '/people', gate: 'governance', module: 'platform-identity' },
144
147
  ] },
145
148
  { group: 'Docs', gate: 'signed', items: [
146
- { id: 'primer', label: 'Primer', icon: 'docs', href: '/primer', gate: 'member' },
149
+ // task 1002710: primer + diagrams were gate:'member' (rank !== xenos) to
150
+ // mirror the old thetes+ page gate. That gate is gone — the reading rooms
151
+ // are for the first-session newcomer above all — so the links follow it
152
+ // down to 'signed'. Leaving them at 'member' would have ungated the pages
153
+ // while still hiding them from the only readers the change was for.
154
+ { id: 'primer', label: 'Primer', icon: 'docs', href: '/primer', gate: 'signed' },
147
155
  { id: 'ranks', label: 'Ranks', icon: 'docs', href: '/ranks', gate: 'signed' },
148
156
  // task 2175: /drachmae had no nav entry at all — reachable only from prose
149
157
  // links since task 2174 moved the bounty rate card onto it. It is reference
@@ -153,7 +161,7 @@
153
161
  // stays right whatever `branding.currency.label` is set to. Page is public
154
162
  // like /ranks — 'signed' is the same cosmetic "show it once you're in".
155
163
  { id: 'economy', label: 'Economy', icon: 'docs', href: '/drachmae', gate: 'signed' },
156
- { id: 'diagrams', label: 'Diagrams', icon: 'docs', href: '/diagrams', gate: 'member' },
164
+ { id: 'diagrams', label: 'Diagrams', icon: 'docs', href: '/diagrams', gate: 'signed' },
157
165
  { id: 'atlas', label: 'Repo Atlas', icon: 'docs', href: '/atlas', gate: 'member' },
158
166
  { id: 'modules', label: 'Modules', icon: 'docs', href: '/modules', gate: 'member' },
159
167
  // The rendered OpenAPI reference lives on the APEX (/docs), not this builders
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.68",
3
+ "version": "1.19.70",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.68",
9
+ "version": "1.19.70",
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.68",
3
+ "version": "1.19.70",
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",
@@ -387,7 +387,11 @@ input[type=text].bad{border-color:var(--err);}
387
387
  <svg class="gh" viewBox="0 0 16 16" aria-hidden="true"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27s1.36.09 2 .27c1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.01 8.01 0 0 0 16 8c0-4.42-3.58-8-8-8z"/></svg>
388
388
  Sign in with GitHub
389
389
  </a>
390
- <p class="finePrint">Free while we find our feet. Your project's repo and server are yours — Cloud Bongos is <a href="https://github.com/lars589/Claude-OTB" style="color:inherit">open source</a> and capture-resistant by design.</p>
390
+ <!-- Trust link → the AGPL license text (task 1002705): the old private-repo URL
391
+ 404ed, and no public code artifact exists until the ADR 0099 mirror flips
392
+ (~2026-11; npm scopes are private too) — the license is the one live,
393
+ instance-neutral artifact behind the claim today. -->
394
+ <p class="finePrint">Free while we find our feet. Your project's repo and server are yours — Cloud Bongos is <a href="https://www.gnu.org/licenses/agpl-3.0.html" style="color:inherit" target="_blank" rel="noopener">open source (AGPL-3.0)</a> and capture-resistant by design.</p>
391
395
  <div class="alert" id="gateAlert" role="alert"></div>
392
396
  </section>
393
397
 
@@ -696,6 +696,13 @@ function mountInternalSurfaces(app) {
696
696
  const WATCH_PAGE_RE = /^\/builders\/watch(?:\.html)?\/?$/;
697
697
  const HARBOR_PAGE_RE = /^\/builders\/harbor(?:\.html)?\/?$/;
698
698
  const GATE_PAGE_RE = /^\/builders\/gate(?:\.html)?\/?$/;
699
+ // /sessions (the session-log ledger) belongs to that same Governance group and
700
+ // its data route (`session.search`) carries the same metic floor since ADR 0157
701
+ // — but it had NO page gate at all: sessions.js only called showSealed() after
702
+ // /api/bongos/me, so the shell was served to anyone who asked for the URL. The
703
+ // data never leaked (the route gate held), but the page was the one item in the
704
+ // group without a wall. Gated here so the group is uniform (task 1002710).
705
+ const SESSIONS_PAGE_RE = /^\/builders\/sessions(?:\.html)?\/?$/;
699
706
  // The /governance roles-and-permissions tab (BV1.R113) sits one floor higher:
700
707
  // the `governance.manage` atom itself (archon), the same key its data routes
701
708
  // check — requireGovernanceManagePage, not the metic-floored gate above.
@@ -704,17 +711,25 @@ function mountInternalSurfaces(app) {
704
711
  // exact permission its data route (GET /scouting) checks — same posture as the
705
712
  // /governance tab above (the page can never drift wider than the API it fronts).
706
713
  const PEOPLE_PAGE_RE = /^\/builders\/people(?:\.html)?\/?$/;
714
+ // /scouting is the LEGACY twin of /people — the same /api/gds/scouting directory,
715
+ // left behind when task 1002577 graduated it into the hall. It has no nav entry
716
+ // and its own header claims "Archon-only", but it was never page-gated: the page
717
+ // rendered for anyone and leaned entirely on the data route's 403. Gated on the
718
+ // same atom as /people so the two can't drift apart (task 1002710). Deleting it
719
+ // in favour of /people is a separate call — it is walled, not removed, here.
720
+ const SCOUTING_PAGE_RE = /^\/builders\/scouting(?:\.html)?\/?$/;
707
721
  app.use((req, res, next) => {
708
722
  if (isModuleSurfaceHost(req)) return next();
709
723
  if (!req.path.startsWith('/builders')) return next();
710
724
  const norm = path.posix.normalize(req.path);
711
- if (WATCH_PAGE_RE.test(norm) || HARBOR_PAGE_RE.test(norm) || GATE_PAGE_RE.test(norm)) {
725
+ if (WATCH_PAGE_RE.test(norm) || HARBOR_PAGE_RE.test(norm) || GATE_PAGE_RE.test(norm)
726
+ || SESSIONS_PAGE_RE.test(norm)) {
712
727
  return gdsAuth.requireGovernancePage(req, res, next);
713
728
  }
714
729
  if (GOVERNANCE_PAGE_RE.test(norm)) {
715
730
  return gdsAuth.requireGovernanceManagePage(req, res, next);
716
731
  }
717
- if (PEOPLE_PAGE_RE.test(norm)) {
732
+ if (PEOPLE_PAGE_RE.test(norm) || SCOUTING_PAGE_RE.test(norm)) {
718
733
  return gdsAuth.requireProjectCuratePage(req, res, next);
719
734
  }
720
735
  next();
@@ -820,24 +835,28 @@ function mountInternalSurfaces(app) {
820
835
  // (style.css, *.js, fonts, cursors), the not-ready notice, and the sign-in flow
821
836
  // itself stay PUBLIC so the landing card + GitHub button render for a stranger.
822
837
  //
823
- // Two tiers of gate (#770):
824
- // * NON-XENOS (Thetes+) — the orientation reading rooms (primer, diagrams).
825
- // A Xenos's path is action-only; an unauth visitor is sent to sign-in, a
826
- // Xenos to the friendly /builders/not-ready notice.
827
- // * ANY SIGNED-IN BUILDER — the Work Board, Roadmap, the Goals page, the
828
- // task-detail page, the Repo Atlas, the Dev Box pair page,
829
- // and Settings (#748, #904, task 1293, task 1757, task 751): a Xenos IS
830
- // allowed, but an unauthenticated visitor is bounced into sign-in rather
831
- // than served the page shell. Settings sits in THIS tier (not the
832
- // reading-room tier) because a newcomer needs it for first-time setup —
833
- // CLI re-auth (the "CLI Access" section) and choosing their craft both
834
- // live there (task 1291). It must still stay behind sign-in: it shows
835
- // personal prefs and mints CLI tokens.
836
- // The link-hiding in builders.js is only cosmetic; these are the real gates.
837
- const NONXENOS_BUILDERS_PAGE_RE =
838
- /^\/builders\/(?:primer(?:\.md|\.html)?|diagrams(?:\.html)?)\/?$/;
839
- const NONXENOS_BUILDERS_PREFIX_RE = /^\/builders\/diagrams\//;
840
- const SIGNEDIN_BUILDERS_PAGE_RE = /^\/builders\/(?:work|pair|settings|atlas|modules|goals|roadmap)(?:\.html)?\/?$/;
838
+ // ONE tier of gate: ANY SIGNED-IN BUILDER — the Work Board, Roadmap, the Goals
839
+ // page, the task-detail page, the Repo Atlas, the Dev Box pair page, Settings
840
+ // (#748, #904, task 1293, task 1757, task 751) and, since task 1002710, the
841
+ // orientation reading rooms (primer, diagrams). A Xenos IS allowed; an
842
+ // unauthenticated visitor is bounced into sign-in rather than served the shell.
843
+ // Settings is in here (not gated higher) because a newcomer needs it for
844
+ // first-time setup — CLI re-auth and choosing their craft both live there
845
+ // (task 1291) — while still staying behind sign-in: it shows personal prefs
846
+ // and mints CLI tokens.
847
+ //
848
+ // The reading rooms USED to sit a tier above this, behind requireNonXenosPage
849
+ // (page.view.builder, thetes+), on #770's reasoning that a Xenos's path is
850
+ // action-only. That got it backwards: the citizen's primer and the onboarding
851
+ // diagrams are written FOR the first-session newcomer, and gating them at
852
+ // thetes redirected exactly that reader to /builders/not-ready. Owner's call
853
+ // (task 1002710) — the RANK floor is gone; sign-in is unchanged.
854
+ // The link-hiding in shell.js is only cosmetic; these are the real gates.
855
+ const SIGNEDIN_BUILDERS_PAGE_RE =
856
+ /^\/builders\/(?:work|pair|settings|atlas|modules|goals|roadmap|primer(?:\.md)?|diagrams)(?:\.html)?\/?$/;
857
+ // The diagram image assets under /builders/diagrams/ ride the same tier as the
858
+ // page that embeds them.
859
+ const SIGNEDIN_BUILDERS_PREFIX_RE = /^\/builders\/diagrams\//;
841
860
  app.use((req, res, next) => {
842
861
  if (isModuleSurfaceHost(req)) return next();
843
862
  // Cheap early-out: only /builders/* paths can ever be gated, so skip the
@@ -850,10 +869,8 @@ function mountInternalSurfaces(app) {
850
869
  // closes that bypass so the gate holds at the application layer, not just
851
870
  // behind the proxy's path normalization.
852
871
  const norm = path.posix.normalize(req.path);
853
- if (NONXENOS_BUILDERS_PAGE_RE.test(norm) || NONXENOS_BUILDERS_PREFIX_RE.test(norm)) {
854
- return gdsAuth.requireNonXenosPage(req, res, next);
855
- }
856
- if (SIGNEDIN_BUILDERS_PAGE_RE.test(norm) || TASK_DETAIL_PAGE_RE.test(norm)) {
872
+ if (SIGNEDIN_BUILDERS_PAGE_RE.test(norm) || SIGNEDIN_BUILDERS_PREFIX_RE.test(norm)
873
+ || TASK_DETAIL_PAGE_RE.test(norm)) {
857
874
  return gdsAuth.requireBuilderPage(req, res, next);
858
875
  }
859
876
  next();
package/src/module-api.js CHANGED
@@ -563,8 +563,12 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
563
563
  // landed since 1.19.66 with no explicit bump. run 31605591253. (task 1002620)
564
564
  // 1.19.68 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
565
565
  // landed since 1.19.67 with no explicit bump. run 31608183268. (task 1002620)
566
+ // 1.19.69 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
567
+ // landed since 1.19.68 with no explicit bump. run 31608503813. (task 1002620)
568
+ // 1.19.70 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
569
+ // landed since 1.19.69 with no explicit bump. run 31612248859. (task 1002620)
566
570
  // ---------------------------------------------------------------------------
567
- const CORE_VERSION = '1.19.68'; // CI auto-patch carrier (ADR 0161); see changelog above
571
+ const CORE_VERSION = '1.19.70'; // CI auto-patch carrier (ADR 0161); see changelog above
568
572
 
569
573
  // A namespaced logger so a module's log lines are attributable + consistent.
570
574
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
@@ -134,8 +134,9 @@ test('sidebar groups read by audience, in order', () => {
134
134
  'the unlabeled group is the project itself (task 1002425 order)');
135
135
  assert.deepEqual(nav[1].items.map((i) => i.id), ['standing', 'profile'], 'You = your own numbers');
136
136
  // People (task 1002577): the /scouting directory graduated into the hall.
137
- // Archon-gated cosmetically to match its data endpoint; requireProjectCuratePage
138
- // is the wall server-side.
137
+ // Gated cosmetically to match its data endpoint — `project.curate`, floor METIC,
138
+ // so the nav entry reads 'governance' since task 1002710 (it said 'archon', which
139
+ // was stricter than the wall). requireProjectCuratePage is the wall server-side.
139
140
  assert.deepEqual(nav[2].items.map((i) => i.id), ['leaderboard', 'people'], 'Community = everyone else');
140
141
  assert.deepEqual(nav[3].items.map((i) => i.id),
141
142
  ['primer', 'ranks', 'economy', 'diagrams', 'atlas', 'modules', 'apidocs'], 'Docs = reference material');
@@ -0,0 +1,166 @@
1
+ // tests/hall_page_gate_map.mjs
2
+ //
3
+ // GUARD (task 1002710): pins WHICH page gate each hall URL routes to.
4
+ //
5
+ // tests/governance_page_gates.mjs proves each gate FUNCTION admits the right
6
+ // audience. Nothing proved the other half — that a given /builders/* URL is
7
+ // wired to the gate it is supposed to be wired to. That gap is not theoretical:
8
+ // /sessions and /scouting each shipped with NO page gate at all, relying on a
9
+ // client-side showSealed() and a data-route 403 respectively, and no test
10
+ // noticed. A page whose shell is served to everyone is not a data leak while
11
+ // its API holds, but it is a wall that exists only in the reviewer's head.
12
+ //
13
+ // It parses serve-internal.js STATICALLY (no import — that pulls the whole app),
14
+ // in the style of tests/served_paths_shipped.mjs: pull the `*_PAGE_RE` literals
15
+ // out of the source, pull the `if (…) return gdsAuth.<gate>(…)` dispatch out of
16
+ // the same source, and run the oracle's URLs through the reconstructed map.
17
+ //
18
+ // Run: node tests/hall_page_gate_map.mjs
19
+
20
+ import { test } from 'node:test';
21
+ import assert from 'node:assert/strict';
22
+ import fs from 'node:fs';
23
+ import path from 'node:path';
24
+ import { fileURLToPath } from 'node:url';
25
+ import { posix as posixPath } from 'node:path';
26
+
27
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
28
+ const SRC = fs.readFileSync(path.join(ROOT, 'src/bongos/serve-internal.js'), 'utf8');
29
+
30
+ // ═══ the ORACLE — hand-written, one row per gated hall URL ═══════════════════
31
+ // `null` means "no page gate": the shell is served to anyone who asks. That is
32
+ // correct for the genuinely public pages and a BUG anywhere else, so every such
33
+ // row is spelled out rather than left to a default.
34
+ //
35
+ // Audiences (the floors live in modules/governance/catalog.js; the gate
36
+ // functions in src/bongos/auth.js):
37
+ // requireBuilderPage page.view.public any signed-in builder (xenos+)
38
+ // requireGovernancePage page.view.governance metic+ (ADR 0157)
39
+ // requireProjectCuratePage project.curate metic+
40
+ // requireGovernanceManagePage governance.manage archon
41
+ const ORACLE = {
42
+ // Any signed-in builder — no rank floor.
43
+ '/builders/work': 'requireBuilderPage',
44
+ '/builders/pair': 'requireBuilderPage',
45
+ '/builders/settings': 'requireBuilderPage',
46
+ '/builders/atlas': 'requireBuilderPage',
47
+ '/builders/modules': 'requireBuilderPage',
48
+ '/builders/goals': 'requireBuilderPage',
49
+ '/builders/roadmap': 'requireBuilderPage',
50
+ // task 1002710: the orientation reading rooms moved DOWN to this tier from
51
+ // requireNonXenosPage (thetes+). They are the onboarding documents, so gating
52
+ // them above xenos turned away the exact reader they were written for. The
53
+ // sign-in requirement is unchanged — only the RANK floor is gone.
54
+ '/builders/primer': 'requireBuilderPage',
55
+ '/builders/primer.md': 'requireBuilderPage',
56
+ '/builders/diagrams': 'requireBuilderPage',
57
+ '/builders/diagrams/02-b.png': 'requireBuilderPage',
58
+
59
+ // Metic+ operator surfaces (the Governance nav group).
60
+ '/builders/watch': 'requireGovernancePage',
61
+ '/builders/harbor': 'requireGovernancePage',
62
+ '/builders/gate': 'requireGovernancePage',
63
+ '/builders/sessions': 'requireGovernancePage',
64
+
65
+ // Metic+ via the `project.curate` atom — the exact permission GET /scouting
66
+ // checks, so neither page can drift wider than the API it fronts.
67
+ '/builders/people': 'requireProjectCuratePage',
68
+ '/builders/scouting': 'requireProjectCuratePage',
69
+
70
+ // Archon.
71
+ '/builders/governance': 'requireGovernanceManagePage',
72
+
73
+ // Deliberately UNGATED — the landing card, the rank ladder, the reward
74
+ // schedule and a builder's public portfolio all render for a stranger.
75
+ '/builders/': null,
76
+ '/builders/ranks': null,
77
+ '/builders/drachmae': null,
78
+ '/builders/profile': null,
79
+ '/builders/not-ready': null,
80
+ };
81
+
82
+ // ─── reconstruct the URL→gate map from the source ───────────────────────────
83
+ // 1. every `const NAME_RE = /…/;` (the page-matching regexes)
84
+ function readRegexes(src) {
85
+ const out = new Map();
86
+ const re = /const\s+([A-Z0-9_]+_RE)\s*=\s*(\/(?:\\.|\[(?:\\.|[^\]])*\]|[^/\\])+\/[gimsuy]*)\s*;/g;
87
+ for (const m of src.matchAll(re)) out.set(m[1], eval(m[2])); // eslint-disable-line no-eval
88
+ return out;
89
+ }
90
+
91
+ // 2. every `if (<condition>) { return gdsAuth.<gate>(req, res, next); }`
92
+ function readDispatch(src) {
93
+ const out = [];
94
+ // `[^{};]` keeps the condition capture INSIDE one if-header. A permissive
95
+ // `[^]*?` looks lazy enough but will happily start at some earlier `if (` and
96
+ // swallow every intervening line, hoovering up unrelated *_RE names and
97
+ // mapping them all to the first gate it reaches.
98
+ const re = /if\s*\(([^{};]*?)\)\s*\{\s*return\s+gdsAuth\.(\w+)\(req,\s*res,\s*next\);/g;
99
+ for (const m of src.matchAll(re)) {
100
+ const names = [...m[1].matchAll(/([A-Z0-9_]+_RE)/g)].map((x) => x[1]);
101
+ if (names.length) out.push({ names, gate: m[2] });
102
+ }
103
+ return out;
104
+ }
105
+
106
+ const REGEXES = readRegexes(SRC);
107
+ const DISPATCH = readDispatch(SRC);
108
+
109
+ // The gate a URL resolves to, mirroring the middleware: first matching branch wins.
110
+ function gateFor(urlPath) {
111
+ const norm = posixPath.normalize(urlPath);
112
+ for (const { names, gate } of DISPATCH) {
113
+ for (const name of names) {
114
+ const rx = REGEXES.get(name);
115
+ if (rx && rx.test(norm)) return gate;
116
+ }
117
+ }
118
+ return null;
119
+ }
120
+
121
+ test('the static parse actually found the gate wiring', () => {
122
+ assert.ok(REGEXES.size >= 8, `expected the *_PAGE_RE literals, found ${REGEXES.size}`);
123
+ assert.ok(DISPATCH.length >= 3, `expected the gdsAuth dispatch branches, found ${DISPATCH.length}`);
124
+ // If this file is ever restructured past the parser, these two fail LOUDLY
125
+ // rather than letting every row below pass vacuously against an empty map.
126
+ assert.ok(DISPATCH.some((d) => d.gate === 'requireBuilderPage'));
127
+ assert.ok(DISPATCH.some((d) => d.gate === 'requireGovernancePage'));
128
+ });
129
+
130
+ test('every hall URL routes to the page gate the oracle names', () => {
131
+ for (const [urlPath, expected] of Object.entries(ORACLE)) {
132
+ assert.equal(gateFor(urlPath), expected,
133
+ `${urlPath} → ${gateFor(urlPath) ?? 'NO GATE'} (expected ${expected ?? 'NO GATE'})`);
134
+ }
135
+ });
136
+
137
+ test('the .html twin of a gated page is gated identically', () => {
138
+ // express.static resolves /builders/watch from watch.html via extensions:['html'],
139
+ // so the explicit .html form must not slip past the gate the bare form carries.
140
+ for (const [urlPath, expected] of Object.entries(ORACLE)) {
141
+ if (expected === null || urlPath.includes('.')) continue;
142
+ assert.equal(gateFor(`${urlPath}.html`), expected, `${urlPath}.html must match ${urlPath}`);
143
+ }
144
+ });
145
+
146
+ test('path-normalization tricks do not slip past a gate', () => {
147
+ // The middleware normalizes before matching precisely so these forms — which
148
+ // express.static WILL resolve to the same file — cannot bypass an anchored RE.
149
+ for (const [urlPath, expected] of Object.entries(ORACLE)) {
150
+ if (expected === null) continue;
151
+ const tail = urlPath.replace(/^\/builders\//, '');
152
+ for (const sneaky of [`/builders//${tail}`, `/builders/./${tail}`]) {
153
+ assert.equal(gateFor(sneaky), expected, `${sneaky} must resolve to ${expected}`);
154
+ }
155
+ }
156
+ });
157
+
158
+ test('no gated page is left matching only a REMOVED gate function', () => {
159
+ // requireNonXenosPage still EXISTS in auth.js (page.view.builder is a live
160
+ // catalog atom an Archon can attach to a custom role) but task 1002710 left it
161
+ // guarding nothing. If someone re-points a URL at it, that is a deliberate act
162
+ // that should update this oracle — not something to happen silently.
163
+ const wired = new Set(DISPATCH.map((d) => d.gate));
164
+ assert.ok(!wired.has('requireNonXenosPage'),
165
+ 'requireNonXenosPage guards no hall URL since task 1002710 — update the ORACLE if that changes');
166
+ });
package/tests/onboard.mjs CHANGED
@@ -110,6 +110,19 @@ test('the /create done panel is plan-driven, manifest-driven, and owner-honest (
110
110
  assert.match(src, /adopt/i, 'the private path (adopt your own private repo) is named');
111
111
  });
112
112
 
113
+ test('the /create gate trust link points at a live artifact, not the dead private-repo URL (task 1002705)', () => {
114
+ // Gap 1 of the project-creation audit: the gate linked a nonexistent private repo
115
+ // (404 even authenticated — ADR 0167 names it). No public code artifact exists yet
116
+ // (the ADR 0099 mirror is dormant until ~2026-11, and both npm scopes 404 anonymously
117
+ // — verified 2026-08-12), so the one live, instance-neutral artifact behind
118
+ // "open source" is the license itself.
119
+ const { readFileSync } = require('node:fs');
120
+ const path = require('node:path');
121
+ const src = readFileSync(path.join(import.meta.dirname, '..', 'public-landing/create.html'), 'utf8');
122
+ assert.doesNotMatch(src, /Claude-OTB/, 'the dead repo link is gone');
123
+ assert.match(src, /https:\/\/www\.gnu\.org\/licenses\/agpl-3\.0/, 'the trust link resolves (the AGPL-3.0 license text)');
124
+ });
125
+
113
126
  test('oauthAppInstructions pins the apex callback + homepage and points at ACCOUNT Developer settings', () => {
114
127
  const oa = ob.oauthAppInstructions('mercury.example.com');
115
128
  assert.equal(oa.homepageUrl, 'https://mercury.example.com');