@bongos/core 1.25.28 → 1.25.29
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 +30 -20
- package/docs/architecture.md +1 -0
- package/docs/module-api-changelog.md +2 -0
- package/docs/page-readings.json +2 -2
- package/docs/recipes/platform-sandbox.md +54 -0
- package/modules/hall-ui/public/sky-time.js +2 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +9 -0
- package/src/module-api.js +1 -1
- package/src/platform-server.js +8 -0
- package/src/sandbox/gate.js +22 -8
- package/src/sandbox/index.js +8 -5
- package/tests/hall_sky_time.mjs +25 -5
- package/tests/platform_sandbox.mjs +96 -0
package/.bongos-core.json
CHANGED
|
@@ -2,22 +2,22 @@
|
|
|
2
2
|
"artifact": "bongos-core",
|
|
3
3
|
"manifest_schema": 1,
|
|
4
4
|
"generator": "scripts/bongos/package-core.js",
|
|
5
|
-
"core_version": "1.25.
|
|
6
|
-
"core_contract": "1.25.
|
|
7
|
-
"source_commit": "
|
|
5
|
+
"core_version": "1.25.29",
|
|
6
|
+
"core_contract": "1.25.29",
|
|
7
|
+
"source_commit": "62fa1f242e8657746b0c64fbd5adf531a8d9d680",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-11-
|
|
9
|
+
"built_at": "2026-11-14T13:39:09.975Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
|
-
"docs_redacted":
|
|
12
|
+
"docs_redacted": 659,
|
|
13
13
|
"agent_docs_stubbed": 34,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 4092,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 4786,
|
|
20
|
+
"tree_sha256": "cffc1cb4b2282e842a416b44aaa5f0dfd78b4c12a75fbd3d81013c8e10319dae",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/ask-for-help/SKILL.md",
|
|
@@ -2522,7 +2522,7 @@
|
|
|
2522
2522
|
{
|
|
2523
2523
|
"path": "docs/architecture.md",
|
|
2524
2524
|
"mode": "0000644",
|
|
2525
|
-
"sha256": "
|
|
2525
|
+
"sha256": "da3a897ac5c83c96a2e08177374aa40d39d5d69dd361eeb3649922c69575ef57"
|
|
2526
2526
|
},
|
|
2527
2527
|
{
|
|
2528
2528
|
"path": "docs/branding-contract.md",
|
|
@@ -3102,7 +3102,7 @@
|
|
|
3102
3102
|
{
|
|
3103
3103
|
"path": "docs/module-api-changelog.md",
|
|
3104
3104
|
"mode": "0000644",
|
|
3105
|
-
"sha256": "
|
|
3105
|
+
"sha256": "a26428f06b23343e46a71e076981e4fa7c5b72e604552cd14a1829bc084a50e5"
|
|
3106
3106
|
},
|
|
3107
3107
|
{
|
|
3108
3108
|
"path": "docs/modules-contract.md",
|
|
@@ -3187,7 +3187,7 @@
|
|
|
3187
3187
|
{
|
|
3188
3188
|
"path": "docs/page-readings.json",
|
|
3189
3189
|
"mode": "0000644",
|
|
3190
|
-
"sha256": "
|
|
3190
|
+
"sha256": "3c608defd6f95e39d31b5508c639b06981423af325d49ed379cbd85d753e8204"
|
|
3191
3191
|
},
|
|
3192
3192
|
{
|
|
3193
3193
|
"path": "docs/project-context.template.md",
|
|
@@ -3309,6 +3309,11 @@
|
|
|
3309
3309
|
"mode": "0000644",
|
|
3310
3310
|
"sha256": "25349889d924866627bb8c07395be660263457eb83cb984f145c6fac9f388591"
|
|
3311
3311
|
},
|
|
3312
|
+
{
|
|
3313
|
+
"path": "docs/recipes/platform-sandbox.md",
|
|
3314
|
+
"mode": "0000644",
|
|
3315
|
+
"sha256": "919821e09d1466da2f8a00bd6622efa716d23d1bdf5cef6e9f956db755372f02"
|
|
3316
|
+
},
|
|
3312
3317
|
{
|
|
3313
3318
|
"path": "docs/recipes/private-npm-distribution.md",
|
|
3314
3319
|
"mode": "0000644",
|
|
@@ -7472,7 +7477,7 @@
|
|
|
7472
7477
|
{
|
|
7473
7478
|
"path": "modules/hall-ui/public/sky-time.js",
|
|
7474
7479
|
"mode": "0000644",
|
|
7475
|
-
"sha256": "
|
|
7480
|
+
"sha256": "37f4ae3015b9b7bb3d45ac41ee83a75e2ec71cd1c2332b4eebd6c5d537a0cb33"
|
|
7476
7481
|
},
|
|
7477
7482
|
{
|
|
7478
7483
|
"path": "modules/hall-ui/public/sky-tour.js",
|
|
@@ -12522,12 +12527,12 @@
|
|
|
12522
12527
|
{
|
|
12523
12528
|
"path": "package-lock.json",
|
|
12524
12529
|
"mode": "0000644",
|
|
12525
|
-
"sha256": "
|
|
12530
|
+
"sha256": "0f10bd2a7aa8926425d73f8cd0d896d524fe30bac3529437dfc0891795d381d1"
|
|
12526
12531
|
},
|
|
12527
12532
|
{
|
|
12528
12533
|
"path": "package.json",
|
|
12529
12534
|
"mode": "0000644",
|
|
12530
|
-
"sha256": "
|
|
12535
|
+
"sha256": "20f2a47ed4d0e769ac47107a5eba117496014d076d218b5225711d0412f63b17"
|
|
12531
12536
|
},
|
|
12532
12537
|
{
|
|
12533
12538
|
"path": "public-docs/index.html",
|
|
@@ -12547,7 +12552,7 @@
|
|
|
12547
12552
|
{
|
|
12548
12553
|
"path": "release-notes.json",
|
|
12549
12554
|
"mode": "0000644",
|
|
12550
|
-
"sha256": "
|
|
12555
|
+
"sha256": "65da0f0378a212330d8859407c6abf229a99a2500e487d77be389e0671a9edf9"
|
|
12551
12556
|
},
|
|
12552
12557
|
{
|
|
12553
12558
|
"path": "scripts/bongos/CLAUDE.md",
|
|
@@ -16627,7 +16632,7 @@
|
|
|
16627
16632
|
{
|
|
16628
16633
|
"path": "src/module-api.js",
|
|
16629
16634
|
"mode": "0000644",
|
|
16630
|
-
"sha256": "
|
|
16635
|
+
"sha256": "c596b3f0c569ae12a277daeb2be50ad22fd5eeb3309df52112ae35baa3760e30"
|
|
16631
16636
|
},
|
|
16632
16637
|
{
|
|
16633
16638
|
"path": "src/module-loader/catalog.js",
|
|
@@ -16677,7 +16682,7 @@
|
|
|
16677
16682
|
{
|
|
16678
16683
|
"path": "src/platform-server.js",
|
|
16679
16684
|
"mode": "0000644",
|
|
16680
|
-
"sha256": "
|
|
16685
|
+
"sha256": "604478c1cfd916afb2b4094002cb321674b112b1155237250dde9450ebd47a42"
|
|
16681
16686
|
},
|
|
16682
16687
|
{
|
|
16683
16688
|
"path": "src/role-responsibilities.js",
|
|
@@ -16687,12 +16692,12 @@
|
|
|
16687
16692
|
{
|
|
16688
16693
|
"path": "src/sandbox/gate.js",
|
|
16689
16694
|
"mode": "0000644",
|
|
16690
|
-
"sha256": "
|
|
16695
|
+
"sha256": "066f13a1de590bfd6a4acc08a5f59134d21beacbf8332f4fb7de404a04b1dc8c"
|
|
16691
16696
|
},
|
|
16692
16697
|
{
|
|
16693
16698
|
"path": "src/sandbox/index.js",
|
|
16694
16699
|
"mode": "0000644",
|
|
16695
|
-
"sha256": "
|
|
16700
|
+
"sha256": "bc4114997f39a149a1ae162f168ac738ccedd8505daee871d50f0057894408e7"
|
|
16696
16701
|
},
|
|
16697
16702
|
{
|
|
16698
16703
|
"path": "src/sleep.js",
|
|
@@ -19902,7 +19907,7 @@
|
|
|
19902
19907
|
{
|
|
19903
19908
|
"path": "tests/hall_sky_time.mjs",
|
|
19904
19909
|
"mode": "0000644",
|
|
19905
|
-
"sha256": "
|
|
19910
|
+
"sha256": "babbac6ab91fdd9326c0b04aabc784ccde5445c855e8531e9a1decfc6eb583f8"
|
|
19906
19911
|
},
|
|
19907
19912
|
{
|
|
19908
19913
|
"path": "tests/hall_sky_visor.mjs",
|
|
@@ -21649,6 +21654,11 @@
|
|
|
21649
21654
|
"mode": "0000644",
|
|
21650
21655
|
"sha256": "147b219cb30b92df1e48b8cf7809559c6ade8729ae09cd5f4241a642ea3d539b"
|
|
21651
21656
|
},
|
|
21657
|
+
{
|
|
21658
|
+
"path": "tests/platform_sandbox.mjs",
|
|
21659
|
+
"mode": "0000644",
|
|
21660
|
+
"sha256": "ccc162070bdf89dd32b0bc785854057578595d237db4cd3bd0efa1547e4d34e7"
|
|
21661
|
+
},
|
|
21652
21662
|
{
|
|
21653
21663
|
"path": "tests/platform_visibility_gate.mjs",
|
|
21654
21664
|
"mode": "0000644",
|
package/docs/architecture.md
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
- **Server stack:** Node.js 22 LTS · PostgreSQL 16 · Caddy 2.11.2. `example.service` (systemd) runs `node server.js` as user `lars` on `127.0.0.1:3000`; restart-on-failure.
|
|
17
17
|
- **Hardening:** `lars` user with passwordless sudo; SSH key-only (root login disabled, password auth disabled); ufw allows 22/80/443; fail2ban active; unattended-upgrades enabled; timezone `America/New_York`.
|
|
18
18
|
- **Credentials:** every credential the instance needs — what it is for, which env file holds it (`web.env` · `box.env` · `spaces.env`), GitHub Actions secrets and builder-machine logins — is listed in [`docs/credentials.md`](credentials.md), names only. Whether each is SET on a host is one command run there: `node scripts/bongos/credential-presence.js` (task 1002880). Check both before asking the owner for a credential.
|
|
19
|
+
- **Cloud Bongos sandbox droplet (NEW 2026-11-14, [ADR 0406](adr/0406-every-project-gets-a-sandbox-that-follows-main-and-live-moves-only-when-a-person-promotes.md) §7, task 1005292):** DigitalOcean, hostname `cloudbongos-sandbox`, `s-1vcpu-2gb`, **NYC1**, ~$12/mo, tag `cloudbongos-sandbox`. Serves `https://sandbox.cloudbongos.com` (one host; test projects at `*.sandbox.cloudbongos.com`, grey-cloud wildcard), runs with `BONGOS_SANDBOX=1`, its own empty `cloudbongos` DB and no outside credentials, and takes **every** published release itself (`cloudbongos-sandbox-update.timer`, follow `candidates`). The live hub's update rule is `pinned`: it moves only from Hall → Deploy. Full layout: [`recipes/platform-sandbox.md`](recipes/platform-sandbox.md).
|
|
19
20
|
- **TLS:** Origin cert at `/etc/caddy/origin.crt` + `/etc/caddy/origin.key` for the example apexes on the co-hosting box. **cloudbongos.com's apexes moved to the dedicated control-plane droplet** ([ADR 0126](adr/0126-dedicated-cloudbongos-control-plane-droplet.md)) with their own copy of the Cloudflare Origin Cert (SAN `*.cloudbongos.com, cloudbongos.com`) at `/etc/caddy/cloudbongos-origin.{crt,key}` on `REDACTED_IP`. Cloudflare SSL/TLS mode = Full (strict). Renewal is a non-issue for ~15 years. **Provisioned instance hosts use a different, coexisting posture** ([ADR 0111](adr/0111-instance-hosting-provisioning-module.md) §4 / task P4): Caddy **on-demand ACME** (Let's Encrypt) via the global `on_demand_tls { ask … }` block, gated to hostnames the `provisioning` module actually stood up (`/api/bongos/provisioning/tls-check`). Their DNS records are **proxied=false (grey cloud)** so the ACME challenge reaches Caddy — the opposite of the origin-cert apexes; do not enable the proxy on them or renewal breaks silently ([ADR 0003](adr/0003-cloudflare-origin-cert-over-letsencrypt.md)). Per-instance site snippets live in `/etc/caddy/sites/*.caddy` (`import`ed by `infra/Caddyfile`); the provisioning runner verifies the box's resolved Caddy config carries that import and the `on_demand_tls` ask before it reports a domain wired, and refuses with the line to add rather than editing the base file ([ADR 0303](adr/0303-provisioning-verifies-the-base-caddyfile-and-never-edits-it.md)). **Tradeoff (accepted, SEC review of task 1945):** `proxied=false` publishes the box's real origin IP in public DNS for every co-tenant provisioned hostname — defeating, for those names, the Cloudflare-proxy IP-hiding the apexes enjoy (a co-tenant host resolves to the same `REDACTED_IP` that serves the main game). This is inherent to on-demand ACME (the challenge must hit the origin) and acceptable under the budget/trust model; prefer a **dedicated droplet** (its own IP) for any instance expected to draw hostile attention.
|
|
20
21
|
- **Platform observability — REMOVED 2026-09-15 ([ADR 0284](adr/0284-the-control-plane-carries-no-metrics-stack.md), task 1003876):** the control plane runs **no Grafana and no Prometheus**, and `metrics.cloudbongos.com` has no Caddy site. They cost ~300 MB RSS + 2.5 GB of data on the box every provisioned project shares, and scraped none of those projects. Every instance still exports `/metrics` ([`src/bongos/metrics.js`](../src/bongos/metrics.js), loopback or `METRICS_TOKEN`), so a scraper can return from a box that is *not* the control plane. The rest of this bullet is the **historical** ADR 0127 layout, kept so its leftovers (co-hosting-box forwarders, instance `infra/` copies) can be found: this watched the **Bongos API** (`gds_http`/`tasks`/`claims`/`builders`/`auth`), not any one game, so it lived **on the control-plane box** (REDACTED_IP) at **`https://metrics.cloudbongos.com`** and observed the **whole fleet**. **Prometheus** (`127.0.0.1:9090`, loopback-only) scrapes every instance's `/metrics` ([`src/bongos/metrics.js`](../src/bongos/metrics.js)), each tagged with a `project` label (`infra/prometheus/prometheus.yml` (instance-side)): **cloudbongos** locally (`:3002`); **example + emersonian-circles** on the co-hosting box, reached over the **private VPC** via `socat` forwarders on `10.108.0.2:9101/9102` (→ their loopback `:3000`/`:3004`), ufw-locked to the control-plane box's private IP — raw TCP relays, so the exporter still sees a loopback request and needs no token; nothing is public. **Grafana** (`127.0.0.1:3003`, loopback-only) renders the 4 provisioned-as-code dashboards (folder **Cloud Bongos**: system-overview, error-watch, builder-activity, auth-security), each with a **`project`** template variable for per-instance / fleet views. Reverse-proxied on the `*.cloudbongos.com` origin cert — Archon-only, gated by Caddy `basic_auth` (user `archon`) **and** Grafana's own admin login (both secrets owner-held, never in the repo). Grafana config is a systemd `GF_*` env drop-in (`grafana-server.service.d/override.conf`, `chmod 600`). ~256 MB RAM. A future dedicated-droplet instance is added as a `METRICS_TOKEN` Bearer target instead of a VPC forwarder.
|
|
21
22
|
|
|
@@ -3494,5 +3494,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
3494
3494
|
landed since 1.25.26 with no explicit bump. run 43595061258. (task 1002620)
|
|
3495
3495
|
1.25.28 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
3496
3496
|
landed since 1.25.27 with no explicit bump. run 43596801043. (task 1002620)
|
|
3497
|
+
1.25.29 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
3498
|
+
landed since 1.25.28 with no explicit bump. run 43600053549. (task 1002620)
|
|
3497
3499
|
---------------------------------------------------------------------------
|
|
3498
3500
|
```
|
package/docs/page-readings.json
CHANGED
|
@@ -283,7 +283,7 @@
|
|
|
283
283
|
"surface": "builders",
|
|
284
284
|
"title": "The city",
|
|
285
285
|
"states_read": ["city","fullscreen","focus","pins","living","card","card-ask"],
|
|
286
|
-
"files_hash": "
|
|
286
|
+
"files_hash": "064e3dde67829d17",
|
|
287
287
|
"reading_hash": "277096d903879a57",
|
|
288
288
|
"counts": {"lines":26,"placed":9,"shared":6,"unplaced":11},
|
|
289
289
|
"omitted": {"data":7},
|
|
@@ -954,7 +954,7 @@
|
|
|
954
954
|
"surface": "builders",
|
|
955
955
|
"title": "Builders' Hall",
|
|
956
956
|
"states_read": ["home","out","founding","genesis","decide","demo","votes"],
|
|
957
|
-
"files_hash": "
|
|
957
|
+
"files_hash": "4589c1f671807a3c",
|
|
958
958
|
"reading_hash": "c5f4986bd90e2a2a",
|
|
959
959
|
"counts": {"lines":29,"placed":8,"shared":8,"unplaced":13},
|
|
960
960
|
"omitted": {"data":4},
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Cloud Bongos's own sandbox — sandbox.cloudbongos.com
|
|
2
|
+
|
|
3
|
+
> Task [task 1005292](https://cloudbongos.com/builders#/task/1005292), under [ADR 0406](../adr/0406-every-project-gets-a-sandbox-that-follows-main-and-live-moves-only-when-a-person-promotes.md) §7. Stood up 2026-11-14.
|
|
4
|
+
|
|
5
|
+
The same rule every project follows, one level up: **every published core release reaches the sandbox first; the live hub moves only when a person promotes.** For Cloud Bongos, "main" is the published release (ADR 0161), so the sandbox follows releases and the hub holds a pin.
|
|
6
|
+
|
|
7
|
+
## Where it runs (owner's choices, 2026-11-14)
|
|
8
|
+
|
|
9
|
+
| | Sandbox | Live hub |
|
|
10
|
+
|---|---|---|
|
|
11
|
+
| Address | `https://sandbox.cloudbongos.com` (one host: projects page and hall) | `cloudbongos.com`, `builders.`, `status.` |
|
|
12
|
+
| Box | DigitalOcean `cloudbongos-sandbox` (IP in the DigitalOcean console; reached with the control-plane key `cloudbongos_ed25519`), `s-1vcpu-2gb`, **NYC1**, $12/mo, tag `cloudbongos-sandbox` (NYC3 had no `s-1vcpu-2gb` left) | `cloudbongos`, NYC3 |
|
|
13
|
+
| Takes releases | **every one, as published** — `cloudbongos-sandbox-update.timer`, every 15 min, roster entry `follow: candidates`, `channel: minor`, `pinMode: local-commit` | **never by itself** — its row's update rule is `pinned` (set 2026-11-14) |
|
|
14
|
+
| Moves when | a release is published | a person presses the move on **Hall → Deploy** (`/deploy`) |
|
|
15
|
+
| Data | its own empty `cloudbongos` database | the real one |
|
|
16
|
+
|
|
17
|
+
The owner chose a separate server over the hub's box (which every project hall shares and which sits near its memory limit), and chose that patch releases stop going live by themselves once the sandbox exists.
|
|
18
|
+
|
|
19
|
+
## What makes it a sandbox
|
|
20
|
+
|
|
21
|
+
- `BONGOS_SANDBOX=1` in `/etc/cloudbongos/web.env`. The hub server installs `src/sandbox` (`src/platform-server.js`), so every page carries the yellow band and sends, charges and writes to known providers (Discord, GitHub's API, Stripe, mail) are refused and logged as `[bongos-sandbox] refused …` in `journalctl -u cloudbongos-sandbox`. The band appears once a release carrying task 1005292 is installed.
|
|
22
|
+
- **One named exception:** `BONGOS_SANDBOX_ALLOW=github-repo-create`, so a test project's creation can make its (free) GitHub repository. Every other GitHub change stays refused (owner's choice, 2026-11-14; `src/sandbox/gate.js` `ALLOWANCES`).
|
|
23
|
+
- **No outside credentials in its environment:** no push token, no box-source token, no Discord, no Cloudflare or DigitalOcean token. Its `web.env` copies only the hub's look and behaviour settings.
|
|
24
|
+
- Its own GitHub sign-in app ("Cloud Bongos sandbox", callback `https://sandbox.cloudbongos.com/api/gds/auth/web/callback`). Session cookies are host-only, and `cookieDomain` is `sandbox.cloudbongos.com`, so signing in on one site never touches the other. `firstAdmin` is `willHermesLines` (Will O'Brien, who started the goal): his first sign-in seats the sandbox's owner. The creation walk is task 1005314.
|
|
25
|
+
- `config/branding.json` addresses all point at `sandbox.cloudbongos.com` (committed locally in the sandbox checkout; it has no remote, so nothing reaches the hub's instance repo).
|
|
26
|
+
|
|
27
|
+
## Test projects
|
|
28
|
+
|
|
29
|
+
Projects created on the sandbox live at `<slug>.sandbox.cloudbongos.com`. The wildcard `*.sandbox.cloudbongos.com` is one grey-cloud Cloudflare record pointing at the sandbox, so the sandbox needs no Cloudflare token (`BOX_DNS_HOOK` unset, the runner skips the DNS step). Caddy gives each one a certificate on demand, through the same `on_demand_tls` ask as the hub. A test project (`modules/provisioning/test-project.js`) is archon-only and never shows on the public map. Created projects are built by `sandbox-provision-intent-runner.timer` (every 2 minutes; the live hub's twin is `selfhost-provision-intent-runner`), which carries no `box.env`, so it holds no Cloudflare or DigitalOcean token.
|
|
30
|
+
|
|
31
|
+
## Box layout
|
|
32
|
+
|
|
33
|
+
- Checkout `/home/lars/cloudbongos-instance` (a copy of the hub's instance repo with the `origin` remote removed). Service `cloudbongos-sandbox.service`, port 3002, user `lars`.
|
|
34
|
+
- Caddy `/etc/caddy/Caddyfile`: the hub's global block (Cloudflare trusted proxies, `on_demand_tls`), `import /etc/caddy/sites/*.caddy`, and the `sandbox.cloudbongos.com` site on the `*.cloudbongos.com` origin cert (proxied, orange cloud).
|
|
35
|
+
- 2 GB swap, `vm.swappiness=10`, ufw allowing 22/80/443.
|
|
36
|
+
- `/usr/local/sbin/sandbox-set-github` stores the sign-in app's id and secret (one per line on stdin) and restarts the service.
|
|
37
|
+
|
|
38
|
+
## Checks
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
curl -s https://sandbox.cloudbongos.com/version
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
ssh -i ~/.ssh/cloudbongos_ed25519 root@<sandbox droplet IP> 'systemctl list-timers | grep sandbox; journalctl -u cloudbongos-sandbox-update -n 20 --no-pager'
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
The hub's own sweep must keep printing `cloudbongos: pinned — skipped`.
|
|
49
|
+
|
|
50
|
+
## Gotchas found standing it up
|
|
51
|
+
|
|
52
|
+
- The hub's Cloudflare token is locked to the hub's IPv4 address: call the API with `curl -4`, or it answers 9109 "Cannot use the access token from location".
|
|
53
|
+
- `@bongos/core` is public on npm, so the sandbox needs no registry token (`docs/recipes/core-release-pipeline.md` still describes an `NPM_TOKEN` file on the hub, which no longer exists there).
|
|
54
|
+
- The Claude terminal panel shows what is typed even at a hidden (`read -s`) prompt. Never have a person paste a secret into it; read it from the clipboard (`pbpaste`) without reading the keyboard at all.
|
|
@@ -179,7 +179,8 @@
|
|
|
179
179
|
if (!p) return null;
|
|
180
180
|
const el = d.documentElement;
|
|
181
181
|
Object.keys(p).forEach((name) => el.style.setProperty('--sky-now-' + name, String(p[name])));
|
|
182
|
-
|
|
182
|
+
// rounded to a tenth, 23:57 to 23:59 would say 24, which is no hour: wrap after rounding (task 1005299)
|
|
183
|
+
el.setAttribute('data-sky-hour', String(wrap(Math.round(h * 10) / 10)));
|
|
183
184
|
lastHour = h;
|
|
184
185
|
return p;
|
|
185
186
|
}
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.25.
|
|
3
|
+
"version": "1.25.29",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.25.
|
|
9
|
+
"version": "1.25.29",
|
|
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.25.
|
|
3
|
+
"version": "1.25.29",
|
|
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
|
@@ -11940,5 +11940,14 @@
|
|
|
11940
11940
|
"id": "1005305",
|
|
11941
11941
|
"text": "In game mode the builder's Home is now just the city: no scrolling, you move around with the arrow keys, by dragging, or by resting the cursor at a screen edge, and you rewind and replay the city's history from the visor's top"
|
|
11942
11942
|
}
|
|
11943
|
+
],
|
|
11944
|
+
"1.25.29": [
|
|
11945
|
+
{
|
|
11946
|
+
"text": "Cloud Bongos now has a test copy at sandbox.cloudbongos.com that gets every new release first, and the main site only updates when a person presses promote."
|
|
11947
|
+
},
|
|
11948
|
+
{
|
|
11949
|
+
"id": "1005299",
|
|
11950
|
+
"text": "The city's sky clock no longer says \"hour 24\" in the last three minutes before midnight, and its check no longer fails at random late at night."
|
|
11951
|
+
}
|
|
11943
11952
|
]
|
|
11944
11953
|
}
|
package/src/module-api.js
CHANGED
|
@@ -81,7 +81,7 @@ const { sleep } = require('./sleep');
|
|
|
81
81
|
// MAJOR (see allowBoxScope below): passes the request through untouched.
|
|
82
82
|
function deprecatedNoopMiddleware(_req, _res, next) { next(); }
|
|
83
83
|
|
|
84
|
-
const CORE_VERSION = '1.25.
|
|
84
|
+
const CORE_VERSION = '1.25.29'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
85
85
|
|
|
86
86
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
87
87
|
// Usage: const log = api.logger('discord'); log.info('mounted');
|
package/src/platform-server.js
CHANGED
|
@@ -225,6 +225,14 @@ function buildPlatformApp() {
|
|
|
225
225
|
// same reqId. Mounted before any surface so nothing logs uncorrelated.
|
|
226
226
|
app.use(requestIdMiddleware);
|
|
227
227
|
|
|
228
|
+
// THE PLATFORM'S OWN SANDBOX (task 1005292, ADR 0406 §7). Cloud Bongos runs a second
|
|
229
|
+
// copy of itself that takes every release first; that copy runs with BONGOS_SANDBOX=1
|
|
230
|
+
// and nothing else marks it. install() is the same one a project's app uses: every
|
|
231
|
+
// page carries the "Sandbox" band, and sends/charges/writes to known providers
|
|
232
|
+
// (Discord, GitHub's API, Stripe, mail) are refused and logged. On the live hub, where
|
|
233
|
+
// the variable is absent, it returns a pass-through and changes nothing.
|
|
234
|
+
app.use(require('./sandbox').install().middleware);
|
|
235
|
+
|
|
228
236
|
// THE request.divert SEAM (task 1004300, ADR 0349). A module may register a provider that
|
|
229
237
|
// takes a request BEFORE any surface answers and either handles it or passes it on —
|
|
230
238
|
// today the npm-release module's version preview, which hands a browser holding its
|
package/src/sandbox/gate.js
CHANGED
|
@@ -56,18 +56,32 @@ function credentialOf(headers) {
|
|
|
56
56
|
return '';
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
// NAMED EXCEPTIONS a sandbox may switch on with BONGOS_SANDBOX_ALLOW (comma-separated
|
|
60
|
+
// names), each one write and nothing near it (task 1005292). Cloud Bongos's own sandbox
|
|
61
|
+
// switches on `github-repo-create`, so a test project's creation walk can make its (free)
|
|
62
|
+
// repository, owner's choice 2026-11-14; every other GitHub change stays refused. An
|
|
63
|
+
// unknown name allows nothing.
|
|
64
|
+
const ALLOWANCES = {
|
|
65
|
+
'github-repo-create': { host: /^api\.github\.com$/i, method: 'POST', path: /^\/(user|orgs\/[A-Za-z0-9-]+)\/repos$/ },
|
|
66
|
+
};
|
|
67
|
+
function parseAllowances(raw) {
|
|
68
|
+
return String(raw || '').split(',').map((x) => x.trim()).filter((x) => Object.prototype.hasOwnProperty.call(ALLOWANCES, x));
|
|
69
|
+
}
|
|
70
|
+
|
|
59
71
|
/**
|
|
60
|
-
* The gate's whole policy. `{ method, host, path, headers }` → `{ allow: true }`
|
|
61
|
-
* `{ allow: false, provider, kind, reason }`. A host in no provider's row passes: the
|
|
72
|
+
* The gate's whole policy. `{ method, host, path, headers, allowances }` → `{ allow: true }`
|
|
73
|
+
* or `{ allow: false, provider, kind, reason }`. A host in no provider's row passes: the
|
|
62
74
|
* gate covers the providers it knows, and says so.
|
|
63
75
|
*/
|
|
64
|
-
function decide({ method, host, path, headers }) {
|
|
76
|
+
function decide({ method, host, path, headers, allowances = [] }) {
|
|
65
77
|
const m = String(method || 'GET').toUpperCase();
|
|
66
78
|
const h = String(host || '').toLowerCase().replace(/:\d+$/, '');
|
|
67
79
|
const p = PROVIDERS.find((x) => x.host.test(h));
|
|
68
80
|
if (!p) return { allow: true };
|
|
69
81
|
if (READ_METHODS.has(m)) return { allow: true, provider: p.name };
|
|
70
82
|
const pathOnly = String(path || '/').split('?')[0];
|
|
83
|
+
const named = allowances.find((n) => { const a = ALLOWANCES[n]; return a && a.method === m && a.host.test(h) && a.path.test(pathOnly); });
|
|
84
|
+
if (named) return { allow: true, provider: p.name, allowance: named };
|
|
71
85
|
if (m === 'POST' && (p.postReads || []).some((re) => re.test(pathOnly))) return { allow: true, provider: p.name };
|
|
72
86
|
if (p.testKey && p.testKey.test(credentialOf(headers))) return { allow: true, provider: p.name, testMode: true };
|
|
73
87
|
return {
|
|
@@ -97,13 +111,13 @@ class SandboxRefused extends Error {
|
|
|
97
111
|
}
|
|
98
112
|
|
|
99
113
|
/** Wrap a fetch: a refused call answers 403 with a body that says why, and never leaves. */
|
|
100
|
-
function wrapFetch(fetchImpl, log) {
|
|
114
|
+
function wrapFetch(fetchImpl, log, allowances = []) {
|
|
101
115
|
if (!fetchImpl || fetchImpl.__bongosSandbox) return fetchImpl;
|
|
102
116
|
const wrapped = async function sandboxFetch(input, init = {}) {
|
|
103
117
|
const url = new URL(typeof input === 'string' || input instanceof URL ? String(input) : input.url);
|
|
104
118
|
const method = (init.method || (input && input.method) || 'GET');
|
|
105
119
|
const headers = init.headers || (input && input.headers) || {};
|
|
106
|
-
const d = decide({ method, host: url.host, path: url.pathname + url.search, headers: typeof headers.get === 'function' ? headers : lower(headers) });
|
|
120
|
+
const d = decide({ method, host: url.host, path: url.pathname + url.search, headers: typeof headers.get === 'function' ? headers : lower(headers), allowances });
|
|
107
121
|
if (d.allow) return fetchImpl(input, init);
|
|
108
122
|
log.add({ ...d, method: String(method).toUpperCase(), host: url.host, path: url.pathname });
|
|
109
123
|
return new Response(JSON.stringify({ error: { type: 'bongos_sandbox_refused', provider: d.provider, kind: d.kind, message: d.reason } }),
|
|
@@ -140,14 +154,14 @@ function refusedRequest(d, cb) {
|
|
|
140
154
|
}
|
|
141
155
|
|
|
142
156
|
/** Patch a node `http` / `https` module's request + get through the gate. */
|
|
143
|
-
function wrapHttp(mod, log) {
|
|
157
|
+
function wrapHttp(mod, log, allowances = []) {
|
|
144
158
|
if (!mod || mod.request.__bongosSandbox) return;
|
|
145
159
|
const realRequest = mod.request;
|
|
146
160
|
const realGet = mod.get;
|
|
147
161
|
const guarded = (real, isGet) => {
|
|
148
162
|
const fn = function sandboxRequest(...args) {
|
|
149
163
|
const t = targetOf(args);
|
|
150
|
-
const d = decide(t);
|
|
164
|
+
const d = decide({ ...t, allowances });
|
|
151
165
|
if (d.allow) return real.apply(mod, args);
|
|
152
166
|
log.add({ ...d, method: String(t.method).toUpperCase(), host: t.host, path: t.path });
|
|
153
167
|
const req = refusedRequest(d, args[args.length - 1]);
|
|
@@ -161,4 +175,4 @@ function wrapHttp(mod, log) {
|
|
|
161
175
|
mod.get = guarded(realGet, true);
|
|
162
176
|
}
|
|
163
177
|
|
|
164
|
-
module.exports = { decide, makeLog, wrapFetch, wrapHttp };
|
|
178
|
+
module.exports = { decide, makeLog, wrapFetch, wrapHttp, parseAllowances };
|
package/src/sandbox/index.js
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
//
|
|
20
20
|
// Fake data is the app's own to seed: `isSandbox()` lets it seed only here.
|
|
21
21
|
|
|
22
|
-
const { makeLog, wrapFetch, wrapHttp } = require('./gate');
|
|
22
|
+
const { makeLog, wrapFetch, wrapHttp, parseAllowances } = require('./gate');
|
|
23
23
|
|
|
24
24
|
const PAGE = '/__bongos/sandbox';
|
|
25
25
|
let state = null;
|
|
@@ -84,7 +84,9 @@ function pageHtml(log, env = process.env, { owner = false } = {}) {
|
|
|
84
84
|
: `<p>The project's owner sees what this copy refused from their project page.</p>`)
|
|
85
85
|
+ `<h2>What this cannot catch</h2>`
|
|
86
86
|
+ `<p>Reads to a known provider go out; sends, charges and writes to it are refused. It only sees calls this app makes through Node's own web requests, and only to the providers it knows. Code that goes around them, or a provider it does not know, is not stopped. A read-only key is the real protection; this is the second wall.</p>`
|
|
87
|
-
|
|
87
|
+
// Only true on Render's free plan (Render sets RENDER=true); the platform's own sandbox
|
|
88
|
+
// runs on a plain server, where saying it would be false (task 1005292).
|
|
89
|
+
+ (String(env.RENDER || '') === 'true' ? `<p>The free plan also blocks outgoing email ports, so this copy cannot send email directly.</p>` : '')
|
|
88
90
|
+ `</body></html>`;
|
|
89
91
|
}
|
|
90
92
|
|
|
@@ -131,9 +133,10 @@ function install({ env = process.env, fetchHost = globalThis, http = require('no
|
|
|
131
133
|
if (!isSandbox(env)) return { active: false, middleware: passThrough, refusals: [], bandHtml: () => '', isSandbox: false };
|
|
132
134
|
if (!state) {
|
|
133
135
|
const log = makeLog(write ? { write } : {});
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
wrapHttp(
|
|
136
|
+
const allowances = parseAllowances(env.BONGOS_SANDBOX_ALLOW);
|
|
137
|
+
if (typeof fetchHost.fetch === 'function') fetchHost.fetch = wrapFetch(fetchHost.fetch, log, allowances);
|
|
138
|
+
wrapHttp(http, log, allowances);
|
|
139
|
+
wrapHttp(https, log, allowances);
|
|
137
140
|
state = { log, middleware: makeMiddleware(log, env) };
|
|
138
141
|
}
|
|
139
142
|
return { active: true, middleware: state.middleware, refusals: state.log.items, bandHtml, isSandbox: true };
|
package/tests/hall_sky_time.mjs
CHANGED
|
@@ -192,7 +192,13 @@ test('a pinned hour wins: the root attribute, then the query, then the hash; a b
|
|
|
192
192
|
});
|
|
193
193
|
|
|
194
194
|
// The page half, run against a stand-in document whose computed styles are style.css's own tokens.
|
|
195
|
-
|
|
195
|
+
// `now` fixes the viewer's clock (a local "HH:MM"), so no case depends on when the suite runs.
|
|
196
|
+
function clockAt(now) {
|
|
197
|
+
if (now == null) return Date;
|
|
198
|
+
const [hh, mm] = now.split(':').map(Number);
|
|
199
|
+
return class extends Date { constructor(...a) { if (a.length) super(...a); else super(2026, 0, 1, hh, mm, 0); } };
|
|
200
|
+
}
|
|
201
|
+
function page({ search = '', hash = '', attr = null, now = null } = {}) {
|
|
196
202
|
const set = {}; const attrs = attr == null ? {} : { 'data-sky-hour-at': attr };
|
|
197
203
|
const timers = []; let probes = 0; let computed = 0;
|
|
198
204
|
const html = {
|
|
@@ -216,7 +222,7 @@ function page({ search = '', hash = '', attr = null } = {}) {
|
|
|
216
222
|
return { color: `rgb(${v[0]}, ${v[1]}, ${v[2]})` };
|
|
217
223
|
},
|
|
218
224
|
setInterval(fn, ms) { timers.push({ fn, ms }); return timers.length; },
|
|
219
|
-
Date, Number, String, Math, Object, Array, parseInt, decodeURIComponent, isFinite,
|
|
225
|
+
Date: clockAt(now), Number, String, Math, Object, Array, parseInt, decodeURIComponent, isFinite,
|
|
220
226
|
};
|
|
221
227
|
vm.runInNewContext(read('sky-time.js'), ctx, { filename: 'sky-time.js' });
|
|
222
228
|
return { api: window.OTBSkyTime, set, attrs, timers, document, stats: () => ({ probes, computed }) };
|
|
@@ -242,12 +248,26 @@ test('on load the page wears the hour\'s palette as --sky-now-* on the root, fro
|
|
|
242
248
|
test('the override reaches the page: the hash and the root attribute pin the hour too', () => {
|
|
243
249
|
assert.equal(page({ hash: '#hour=18' }).set['--sky-now-cloud'], KEYS.dusk.cloud.toLowerCase());
|
|
244
250
|
assert.equal(page({ attr: '6', search: '?hour=13' }).set['--sky-now-cloud'], KEYS.dawn.cloud.toLowerCase());
|
|
245
|
-
// with nothing pinned the viewer's clock is the source
|
|
246
|
-
|
|
247
|
-
|
|
251
|
+
// with nothing pinned the viewer's clock is the source (fixed here, so the case never
|
|
252
|
+
// depends on when the suite runs)
|
|
253
|
+
const live = page({ now: '13:30' });
|
|
254
|
+
assert.equal(live.attrs['data-sky-hour'], '13.5');
|
|
248
255
|
assert.equal(live.api.hourNow({ document: live.document, location: { search: '?hour=4', hash: '' } }), 4);
|
|
249
256
|
});
|
|
250
257
|
|
|
258
|
+
// Task 1005299: 23:57 to 23:59 rounded to one decimal is 24.0, which is not an hour; the
|
|
259
|
+
// last minutes of the day say the next day's first hour, and wear its palette.
|
|
260
|
+
test('the hour the page says never reaches 24: the last minutes of the day wrap to 0', () => {
|
|
261
|
+
for (const now of ['23:57', '23:58', '23:59']) {
|
|
262
|
+
const p = page({ now });
|
|
263
|
+
assert.equal(p.attrs['data-sky-hour'], '0', `${now} says hour 0, not 24`);
|
|
264
|
+
assert.equal(p.set['--sky-now-mid'], KEYS.night.mid.toLowerCase(), `${now} wears night`);
|
|
265
|
+
}
|
|
266
|
+
assert.equal(page({ now: '23:56' }).attrs['data-sky-hour'], '23.9');
|
|
267
|
+
assert.equal(page({ now: '00:00' }).attrs['data-sky-hour'], '0');
|
|
268
|
+
assert.equal(page({ search: '?hour=23.98' }).attrs['data-sky-hour'], '0', 'a pinned hour wraps the same way');
|
|
269
|
+
});
|
|
270
|
+
|
|
251
271
|
test('no motion: the palette is recomputed at most once a minute and nothing animates between two', () => {
|
|
252
272
|
const p = page({ search: '?hour=6' });
|
|
253
273
|
assert.equal(p.timers.length, 1, 'one timer');
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// tests/platform_sandbox.mjs — Cloud Bongos's own sandbox (task 1005292; ADR 0406 §7).
|
|
2
|
+
//
|
|
3
|
+
// The platform runs a second copy of itself that takes every release before the live hub.
|
|
4
|
+
// That copy is the same server started with BONGOS_SANDBOX=1. Pinned here:
|
|
5
|
+
// 1. the hub server installs the sandbox middleware, before any surface answers, so every
|
|
6
|
+
// page of the copy carries the band — and on the live hub (no variable) it is a no-op;
|
|
7
|
+
// 2. the sandbox page only claims Render's blocked mail ports where Render is the host,
|
|
8
|
+
// because the platform's sandbox runs on a plain server where that sentence is false.
|
|
9
|
+
//
|
|
10
|
+
// Run: node tests/platform_sandbox.mjs
|
|
11
|
+
|
|
12
|
+
import { strict as assert } from 'node:assert';
|
|
13
|
+
import { createRequire } from 'node:module';
|
|
14
|
+
import fs from 'node:fs';
|
|
15
|
+
import path from 'node:path';
|
|
16
|
+
import { fileURLToPath } from 'node:url';
|
|
17
|
+
|
|
18
|
+
const require = createRequire(import.meta.url);
|
|
19
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
20
|
+
const { install, pageHtml } = require('../src/sandbox/index.js');
|
|
21
|
+
|
|
22
|
+
let passed = 0, failed = 0;
|
|
23
|
+
async function t(name, fn) {
|
|
24
|
+
try { await fn(); passed++; console.log(` PASS ${name}`); }
|
|
25
|
+
catch (e) { failed++; console.log(` FAIL ${name}\n ${e.message}`); }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const src = fs.readFileSync(path.join(ROOT, 'src/platform-server.js'), 'utf8')
|
|
29
|
+
.replace(/^\s*\/\/.*$/gm, ''); // comments may cite the rule; only code counts
|
|
30
|
+
|
|
31
|
+
await t('the hub server mounts the sandbox middleware', () => {
|
|
32
|
+
assert.match(src, /app\.use\(require\('\.\/sandbox'\)\.install\(\)\.middleware\)/);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
await t('it is mounted before any surface or module answers', () => {
|
|
36
|
+
const at = src.indexOf("require('./sandbox')");
|
|
37
|
+
const divert = src.indexOf("resolveOptional('request.divert')");
|
|
38
|
+
const surfaces = src.indexOf('mountInternalSurfaces(');
|
|
39
|
+
assert.ok(at > 0 && divert > 0 && at < divert, 'sandbox must come before the request.divert seam');
|
|
40
|
+
// the import of mountInternalSurfaces sits at the top; the CALL is what matters
|
|
41
|
+
const call = src.indexOf('mountInternalSurfaces(', surfaces + 1);
|
|
42
|
+
assert.ok(call < 0 || at < call, 'sandbox must come before the surfaces mount');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
await t('on the live hub (no BONGOS_SANDBOX) it changes nothing', () => {
|
|
46
|
+
const r = install({ env: {} });
|
|
47
|
+
assert.equal(r.active, false);
|
|
48
|
+
let called = false;
|
|
49
|
+
r.middleware({}, {}, () => { called = true; });
|
|
50
|
+
assert.equal(called, true);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const log = { items: [] };
|
|
54
|
+
await t('on a plain server the page makes no claim about blocked mail ports', () => {
|
|
55
|
+
assert.doesNotMatch(pageHtml(log, { BONGOS_SANDBOX: '1' }), /email ports/);
|
|
56
|
+
});
|
|
57
|
+
await t('on Render it still says the free plan blocks them', () => {
|
|
58
|
+
assert.match(pageHtml(log, { BONGOS_SANDBOX: '1', RENDER: 'true' }), /blocks outgoing email ports/);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// 3. The one named exception: creating a repository (owner's choice, 2026-11-14).
|
|
62
|
+
const { decide, parseAllowances } = require('../src/sandbox/gate.js');
|
|
63
|
+
const ALLOW = parseAllowances('github-repo-create');
|
|
64
|
+
await t('without the exception, creating a GitHub repository is refused', () => {
|
|
65
|
+
assert.equal(decide({ method: 'POST', host: 'api.github.com', path: '/user/repos' }).allow, false);
|
|
66
|
+
});
|
|
67
|
+
await t('with it, creating a repository for a person or an organisation passes', () => {
|
|
68
|
+
assert.equal(decide({ method: 'POST', host: 'api.github.com', path: '/user/repos', allowances: ALLOW }).allow, true);
|
|
69
|
+
assert.equal(decide({ method: 'POST', host: 'api.github.com', path: '/orgs/acme-co/repos', allowances: ALLOW }).allow, true);
|
|
70
|
+
});
|
|
71
|
+
await t('every other GitHub change stays refused with it on', () => {
|
|
72
|
+
for (const [m, p] of [['POST', '/repos/o/r/pulls'], ['PUT', '/repos/o/r/pulls/1/merge'], ['PATCH', '/repos/o/r'], ['DELETE', '/repos/o/r'], ['POST', '/repos/o/r/keys'], ['PUT', '/repos/o/r/collaborators/x'], ['POST', '/user/repos/extra']]) {
|
|
73
|
+
assert.equal(decide({ method: m, host: 'api.github.com', path: p, allowances: ALLOW }).allow, false, `${m} ${p}`);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
await t('an unknown exception name allows nothing', () => {
|
|
77
|
+
assert.deepEqual(parseAllowances('everything, github-*'), []);
|
|
78
|
+
});
|
|
79
|
+
await t('install() reads the exception from BONGOS_SANDBOX_ALLOW', async () => {
|
|
80
|
+
const seen = [];
|
|
81
|
+
const host = { fetch: async (u) => { seen.push(String(u)); return new Response('{}', { status: 201 }); } };
|
|
82
|
+
const fakeMod = () => ({ request() {}, get() {} });
|
|
83
|
+
const idx = require('../src/sandbox/index.js');
|
|
84
|
+
// a fresh module instance, since install() keeps one state per process
|
|
85
|
+
delete require.cache[require.resolve('../src/sandbox/index.js')];
|
|
86
|
+
const fresh = require('../src/sandbox/index.js');
|
|
87
|
+
fresh.install({ env: { BONGOS_SANDBOX: '1', BONGOS_SANDBOX_ALLOW: 'github-repo-create' }, fetchHost: host, http: fakeMod(), https: fakeMod(), write() {} });
|
|
88
|
+
void idx;
|
|
89
|
+
return host.fetch('https://api.github.com/user/repos', { method: 'POST' }).then((r) => {
|
|
90
|
+
assert.equal(r.status, 201);
|
|
91
|
+
assert.equal(seen.length, 1);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
console.log(`\n${passed} passed, ${failed} failed`);
|
|
96
|
+
if (failed) process.exit(1);
|