@bongos/core 1.19.141 → 1.19.143
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 +29 -24
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +2 -0
- package/clients/bongos-client/index.cjs +2 -0
- package/clients/bongos-client/index.d.ts +3 -0
- package/clients/bongos-client/index.mjs +2 -0
- package/docs/api/openapi.json +56 -3
- package/docs/api-reference.md +3 -2
- package/modules/dev-box/app/src/vendor/bongos-client.cjs +2 -0
- package/modules/provisioning/rate-limit.js +15 -5
- package/modules/provisioning/routes/provisioning.js +71 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/public-landing/assets/cosmos.css +35 -8
- package/public-landing/projects.html +130 -18
- package/src/module-api.js +5 -1
- package/tests/cosmos_token_contrast.mjs +147 -11
- package/tests/provisioning_slug_available.mjs +243 -0
- package/tests/public_landing_projects.mjs +118 -0
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.
|
|
6
|
-
"core_contract": "1.19.
|
|
7
|
-
"source_commit": "
|
|
5
|
+
"core_version": "1.19.143",
|
|
6
|
+
"core_contract": "1.19.143",
|
|
7
|
+
"source_commit": "31df926208597154ce684504f0aa15d6b1dde113",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-08-15T15:
|
|
9
|
+
"built_at": "2026-08-15T15:52:12.410Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 303,
|
|
13
13
|
"agent_docs_stubbed": 21,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 1251,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 1575,
|
|
20
|
+
"tree_sha256": "a9a3320aae0cbec4eb5ec01e7b50675d69a50814017d7f4658efa4e52493deb4",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/blocker-review/SKILL.md",
|
|
@@ -347,12 +347,12 @@
|
|
|
347
347
|
{
|
|
348
348
|
"path": "clients/bongos-client/README.md",
|
|
349
349
|
"mode": "0000644",
|
|
350
|
-
"sha256": "
|
|
350
|
+
"sha256": "0942508db8fb6f31b9e095d197f8b4cdd4efdc4e754c46629ea299ff10d895a1"
|
|
351
351
|
},
|
|
352
352
|
{
|
|
353
353
|
"path": "clients/bongos-client/bongos-client.global.js",
|
|
354
354
|
"mode": "0000644",
|
|
355
|
-
"sha256": "
|
|
355
|
+
"sha256": "5a4a95386b5a2ec9bc59334a35b09b5701b2ca5eabc99baf2305c7b82d920342"
|
|
356
356
|
},
|
|
357
357
|
{
|
|
358
358
|
"path": "clients/bongos-client/examples/hello-world.mjs",
|
|
@@ -362,17 +362,17 @@
|
|
|
362
362
|
{
|
|
363
363
|
"path": "clients/bongos-client/index.cjs",
|
|
364
364
|
"mode": "0000644",
|
|
365
|
-
"sha256": "
|
|
365
|
+
"sha256": "2c199a1ad61c8ca8a015b0918368da4c3bf4a8e6196edfd46b93048dda8e9eb5"
|
|
366
366
|
},
|
|
367
367
|
{
|
|
368
368
|
"path": "clients/bongos-client/index.d.ts",
|
|
369
369
|
"mode": "0000644",
|
|
370
|
-
"sha256": "
|
|
370
|
+
"sha256": "e1359174cf948616e95a03232a8fe3f7a56e66aa94d578c786b6e7a258ce0221"
|
|
371
371
|
},
|
|
372
372
|
{
|
|
373
373
|
"path": "clients/bongos-client/index.mjs",
|
|
374
374
|
"mode": "0000644",
|
|
375
|
-
"sha256": "
|
|
375
|
+
"sha256": "e0e5672aafc56bfec2b6d0b3d90a73c984a5f3cf6b745025f5d9a37d8f09d7e6"
|
|
376
376
|
},
|
|
377
377
|
{
|
|
378
378
|
"path": "clients/bongos-client/package.json",
|
|
@@ -1337,12 +1337,12 @@
|
|
|
1337
1337
|
{
|
|
1338
1338
|
"path": "docs/api-reference.md",
|
|
1339
1339
|
"mode": "0000644",
|
|
1340
|
-
"sha256": "
|
|
1340
|
+
"sha256": "e88714b1178d43ff8aea451807a0a6095a3e90d4a592babc4ffb5c1baccac4ba"
|
|
1341
1341
|
},
|
|
1342
1342
|
{
|
|
1343
1343
|
"path": "docs/api/openapi.json",
|
|
1344
1344
|
"mode": "0000644",
|
|
1345
|
-
"sha256": "
|
|
1345
|
+
"sha256": "0a3bf7cc757082f1764b926f72e24a14e8f22e07f0e0a5085ab6ff9d64e74ba8"
|
|
1346
1346
|
},
|
|
1347
1347
|
{
|
|
1348
1348
|
"path": "docs/architecture.md",
|
|
@@ -2652,7 +2652,7 @@
|
|
|
2652
2652
|
{
|
|
2653
2653
|
"path": "modules/dev-box/app/src/vendor/bongos-client.cjs",
|
|
2654
2654
|
"mode": "0000644",
|
|
2655
|
-
"sha256": "
|
|
2655
|
+
"sha256": "2c199a1ad61c8ca8a015b0918368da4c3bf4a8e6196edfd46b93048dda8e9eb5"
|
|
2656
2656
|
},
|
|
2657
2657
|
{
|
|
2658
2658
|
"path": "modules/dev-box/app/test/unit.mjs",
|
|
@@ -3877,12 +3877,12 @@
|
|
|
3877
3877
|
{
|
|
3878
3878
|
"path": "modules/provisioning/rate-limit.js",
|
|
3879
3879
|
"mode": "0000644",
|
|
3880
|
-
"sha256": "
|
|
3880
|
+
"sha256": "27046325beffad5d1edfcc91c331048038900ef7d32f1d38796755c7990ba202"
|
|
3881
3881
|
},
|
|
3882
3882
|
{
|
|
3883
3883
|
"path": "modules/provisioning/routes/provisioning.js",
|
|
3884
3884
|
"mode": "0000644",
|
|
3885
|
-
"sha256": "
|
|
3885
|
+
"sha256": "17f6adb4edd7dee2ab339f5d302b61ecfbcf45db2ba4d0315eeeffbad87a9dce"
|
|
3886
3886
|
},
|
|
3887
3887
|
{
|
|
3888
3888
|
"path": "modules/provisioning/tests/liveness.mjs",
|
|
@@ -4177,12 +4177,12 @@
|
|
|
4177
4177
|
{
|
|
4178
4178
|
"path": "package-lock.json",
|
|
4179
4179
|
"mode": "0000644",
|
|
4180
|
-
"sha256": "
|
|
4180
|
+
"sha256": "5f68e2c2d9e7f46d2884dce06d8bc7a5ed71f7716800c1815d550e43bd896176"
|
|
4181
4181
|
},
|
|
4182
4182
|
{
|
|
4183
4183
|
"path": "package.json",
|
|
4184
4184
|
"mode": "0000644",
|
|
4185
|
-
"sha256": "
|
|
4185
|
+
"sha256": "51affa4342af5f89f0cfda20e74bb4120338ef58c2d0309c706a7dbdb25c4d8f"
|
|
4186
4186
|
},
|
|
4187
4187
|
{
|
|
4188
4188
|
"path": "public-docs/index.html",
|
|
@@ -4422,7 +4422,7 @@
|
|
|
4422
4422
|
{
|
|
4423
4423
|
"path": "public-landing/assets/cosmos.css",
|
|
4424
4424
|
"mode": "0000644",
|
|
4425
|
-
"sha256": "
|
|
4425
|
+
"sha256": "06ed45b09cb35cc8b2e20816416c45a07dd6ca47f418ac9c239ab8c949796f8b"
|
|
4426
4426
|
},
|
|
4427
4427
|
{
|
|
4428
4428
|
"path": "public-landing/buddha.png",
|
|
@@ -4437,7 +4437,7 @@
|
|
|
4437
4437
|
{
|
|
4438
4438
|
"path": "public-landing/projects.html",
|
|
4439
4439
|
"mode": "0000644",
|
|
4440
|
-
"sha256": "
|
|
4440
|
+
"sha256": "aa37e8e5b14f169f593a55eb71a0c21117c818a07d34d23a35c45219c646a5dd"
|
|
4441
4441
|
},
|
|
4442
4442
|
{
|
|
4443
4443
|
"path": "public-landing/world/assets/clouds/cloud-island-large.png",
|
|
@@ -5767,7 +5767,7 @@
|
|
|
5767
5767
|
{
|
|
5768
5768
|
"path": "src/module-api.js",
|
|
5769
5769
|
"mode": "0000644",
|
|
5770
|
-
"sha256": "
|
|
5770
|
+
"sha256": "7ac4130655324a65c52dfd04a7b57c72371904149e8a126a16d341f82d3d1625"
|
|
5771
5771
|
},
|
|
5772
5772
|
{
|
|
5773
5773
|
"path": "src/module-loader/catalog.js",
|
|
@@ -6277,7 +6277,7 @@
|
|
|
6277
6277
|
{
|
|
6278
6278
|
"path": "tests/cosmos_token_contrast.mjs",
|
|
6279
6279
|
"mode": "0000644",
|
|
6280
|
-
"sha256": "
|
|
6280
|
+
"sha256": "6343c10b95c8ed1da86ae32367882a9970f19266bccedb26b3e15b73c5bdd37a"
|
|
6281
6281
|
},
|
|
6282
6282
|
{
|
|
6283
6283
|
"path": "tests/cost.mjs",
|
|
@@ -7339,6 +7339,11 @@
|
|
|
7339
7339
|
"mode": "0000644",
|
|
7340
7340
|
"sha256": "7749df454ac3cca7c7e5ea1acc94ead15d1c9f87b38fe2028a1c93c8adf31bcf"
|
|
7341
7341
|
},
|
|
7342
|
+
{
|
|
7343
|
+
"path": "tests/provisioning_slug_available.mjs",
|
|
7344
|
+
"mode": "0000644",
|
|
7345
|
+
"sha256": "1623d2dff20d7b5843f9fd39dd91c35e4a8ebe10b60cf798e01a7fbc4b0202cf"
|
|
7346
|
+
},
|
|
7342
7347
|
{
|
|
7343
7348
|
"path": "tests/provisioning_teardown_intent.mjs",
|
|
7344
7349
|
"mode": "0000644",
|
|
@@ -7352,7 +7357,7 @@
|
|
|
7352
7357
|
{
|
|
7353
7358
|
"path": "tests/public_landing_projects.mjs",
|
|
7354
7359
|
"mode": "0000644",
|
|
7355
|
-
"sha256": "
|
|
7360
|
+
"sha256": "ea3fc3b311115c765e7734e11cc33750a5ffde9f492d4635460b04cfc2108ad3"
|
|
7356
7361
|
},
|
|
7357
7362
|
{
|
|
7358
7363
|
"path": "tests/public_mirror_export.mjs",
|
|
@@ -5,7 +5,7 @@ A **generated**, zero-dependency typed client for the Bongos API — produced fr
|
|
|
5
5
|
by hand; it regenerates when the spec changes, so it can never drift from the routes.
|
|
6
6
|
|
|
7
7
|
- API version: **v1** (served at `/api/bongos/v1`)
|
|
8
|
-
-
|
|
8
|
+
- 270 operations across 45 resource groups
|
|
9
9
|
|
|
10
10
|
## Install
|
|
11
11
|
|
|
@@ -533,6 +533,8 @@ function createClient(opts = {}) {
|
|
|
533
533
|
postProvisioningInstancesIdTeardown: (args) => request("POST", "/provisioning/instances/{id}/teardown", { hasBody: true }, args),
|
|
534
534
|
// GET /provisioning/onboard-plan — rank: public — GET /provisioning/onboard-plan
|
|
535
535
|
getProvisioningOnboardPlan: (args) => request("GET", "/provisioning/onboard-plan", { hasBody: false }, args),
|
|
536
|
+
// GET /provisioning/slug-available — rank: any-builder — GET /provisioning/slug-available
|
|
537
|
+
getProvisioningSlugAvailable: (args) => request("GET", "/provisioning/slug-available", { hasBody: false }, args),
|
|
536
538
|
// GET /provisioning/tls-check — rank: public — GET /provisioning/tls-check
|
|
537
539
|
getProvisioningTlsCheck: (args) => request("GET", "/provisioning/tls-check", { hasBody: false }, args),
|
|
538
540
|
},
|
|
@@ -532,6 +532,8 @@ function createClient(opts = {}) {
|
|
|
532
532
|
postProvisioningInstancesIdTeardown: (args) => request("POST", "/provisioning/instances/{id}/teardown", { hasBody: true }, args),
|
|
533
533
|
// GET /provisioning/onboard-plan — rank: public — GET /provisioning/onboard-plan
|
|
534
534
|
getProvisioningOnboardPlan: (args) => request("GET", "/provisioning/onboard-plan", { hasBody: false }, args),
|
|
535
|
+
// GET /provisioning/slug-available — rank: any-builder — GET /provisioning/slug-available
|
|
536
|
+
getProvisioningSlugAvailable: (args) => request("GET", "/provisioning/slug-available", { hasBody: false }, args),
|
|
535
537
|
// GET /provisioning/tls-check — rank: public — GET /provisioning/tls-check
|
|
536
538
|
getProvisioningTlsCheck: (args) => request("GET", "/provisioning/tls-check", { hasBody: false }, args),
|
|
537
539
|
},
|
|
@@ -75,6 +75,7 @@ export interface GetProvisioningHealthResponse { module: unknown; ok: boolean; c
|
|
|
75
75
|
export interface GetProvisioningInstancesIdResponse { instance: unknown; open_intent: unknown }
|
|
76
76
|
export interface GetProvisioningInstancesResponse { instances: unknown }
|
|
77
77
|
export interface GetProvisioningOnboardPlanResponse { mode: unknown; steps: unknown }
|
|
78
|
+
export interface GetProvisioningSlugAvailableResponse { slug: unknown; available: unknown; reason: unknown; address: unknown }
|
|
78
79
|
export interface GetPublicBlockersIdResponse { blocker: unknown }
|
|
79
80
|
export interface GetPublicBountyTableResponse { table: unknown; currency: unknown }
|
|
80
81
|
export interface GetPublicGradesResponse { window_days: unknown; rolling: unknown; trend: unknown; attempts: unknown; threshold: unknown; grader_version: unknown }
|
|
@@ -768,6 +769,8 @@ export interface BongosClient {
|
|
|
768
769
|
postProvisioningInstancesIdTeardown(args?: RequestArgs): Promise<PostProvisioningInstancesIdTeardownResponse>;
|
|
769
770
|
/** GET /provisioning/onboard-plan — rank: public */
|
|
770
771
|
getProvisioningOnboardPlan(args?: RequestArgs): Promise<GetProvisioningOnboardPlanResponse>;
|
|
772
|
+
/** GET /provisioning/slug-available — rank: any-builder */
|
|
773
|
+
getProvisioningSlugAvailable(args?: RequestArgs): Promise<GetProvisioningSlugAvailableResponse>;
|
|
771
774
|
/** GET /provisioning/tls-check — rank: public */
|
|
772
775
|
getProvisioningTlsCheck(args?: RequestArgs): Promise<ApiResponse>;
|
|
773
776
|
};
|
|
@@ -529,6 +529,8 @@ export function createClient(opts = {}) {
|
|
|
529
529
|
postProvisioningInstancesIdTeardown: (args) => request("POST", "/provisioning/instances/{id}/teardown", { hasBody: true }, args),
|
|
530
530
|
// GET /provisioning/onboard-plan — rank: public — GET /provisioning/onboard-plan
|
|
531
531
|
getProvisioningOnboardPlan: (args) => request("GET", "/provisioning/onboard-plan", { hasBody: false }, args),
|
|
532
|
+
// GET /provisioning/slug-available — rank: any-builder — GET /provisioning/slug-available
|
|
533
|
+
getProvisioningSlugAvailable: (args) => request("GET", "/provisioning/slug-available", { hasBody: false }, args),
|
|
532
534
|
// GET /provisioning/tls-check — rank: public — GET /provisioning/tls-check
|
|
533
535
|
getProvisioningTlsCheck: (args) => request("GET", "/provisioning/tls-check", { hasBody: false }, args),
|
|
534
536
|
},
|
package/docs/api/openapi.json
CHANGED
|
@@ -8754,6 +8754,44 @@
|
|
|
8754
8754
|
"security": []
|
|
8755
8755
|
}
|
|
8756
8756
|
},
|
|
8757
|
+
"/provisioning/slug-available": {
|
|
8758
|
+
"get": {
|
|
8759
|
+
"operationId": "get_provisioning_slug_available",
|
|
8760
|
+
"tags": [
|
|
8761
|
+
"provisioning"
|
|
8762
|
+
],
|
|
8763
|
+
"summary": "GET /provisioning/slug-available",
|
|
8764
|
+
"description": "GET /provisioning/slug-available?slug=<handle> — is this handle free to create under? rank: any authenticated builder (same gate as the create route it previews; deliberately NOT public — the answer is a yes/no about every project slug on the instance, so it is an enumeration oracle and carries its own per-IP limiter). It ADVISORY-previews the create route and must agree with it exactly, so the normalization and the bar below are lifted from POST /provisioning/instances: trim+lowercase → isValidSlug → getInstanceBySlug (the byte-exact `slug text UNIQUE` in provisioning_001_tables.sql, which is what the 23505 catch in createInstance reflects — so no lower(slug) here, or we would over-report taken). The three refusals mirror the three ways create actually answers: 'taken' — another builder holds it → create 409s slug_taken 'yours' — the CALLER holds it → create is an idempotent re-request (200, created:false), not a new project, so it is not free to create under 'reserved' — the row is free but <slug>.<apex> is a platform-reserved address → create 400s domain_reserved on the DEFAULT (assigned-address) path. A no_address create would still succeed; the wizard only asks about the default path, and reporting it distinctly keeps that recoverable. NARROWS the TOCTOU window, never closes it: another builder can take the slug between this answer and the create. The 409 at POST /provisioning/instances remains the ONLY authority — this exists so a collision is seen while the owner is still typing instead of at the final Create click.\n\n**Rank:** `any-builder` — Any authenticated builder (row-level ownership enforced in-handler).",
|
|
8765
|
+
"x-rank": "any-builder",
|
|
8766
|
+
"x-source": "modules/provisioning/routes/provisioning.js",
|
|
8767
|
+
"responses": {
|
|
8768
|
+
"200": {
|
|
8769
|
+
"description": "Success.",
|
|
8770
|
+
"content": {
|
|
8771
|
+
"application/json": {
|
|
8772
|
+
"schema": {
|
|
8773
|
+
"$ref": "#/components/schemas/GetProvisioningSlugAvailableResponse"
|
|
8774
|
+
}
|
|
8775
|
+
}
|
|
8776
|
+
}
|
|
8777
|
+
},
|
|
8778
|
+
"400": {
|
|
8779
|
+
"$ref": "#/components/responses/BadRequest"
|
|
8780
|
+
},
|
|
8781
|
+
"401": {
|
|
8782
|
+
"$ref": "#/components/responses/Unauthorized"
|
|
8783
|
+
},
|
|
8784
|
+
"403": {
|
|
8785
|
+
"$ref": "#/components/responses/Forbidden"
|
|
8786
|
+
}
|
|
8787
|
+
},
|
|
8788
|
+
"security": [
|
|
8789
|
+
{
|
|
8790
|
+
"builderSession": []
|
|
8791
|
+
}
|
|
8792
|
+
]
|
|
8793
|
+
}
|
|
8794
|
+
},
|
|
8757
8795
|
"/provisioning/tls-check": {
|
|
8758
8796
|
"get": {
|
|
8759
8797
|
"operationId": "get_provisioning_tls_check",
|
|
@@ -13109,6 +13147,21 @@
|
|
|
13109
13147
|
"steps"
|
|
13110
13148
|
]
|
|
13111
13149
|
},
|
|
13150
|
+
"GetProvisioningSlugAvailableResponse": {
|
|
13151
|
+
"type": "object",
|
|
13152
|
+
"properties": {
|
|
13153
|
+
"slug": {},
|
|
13154
|
+
"available": {},
|
|
13155
|
+
"reason": {},
|
|
13156
|
+
"address": {}
|
|
13157
|
+
},
|
|
13158
|
+
"required": [
|
|
13159
|
+
"slug",
|
|
13160
|
+
"available",
|
|
13161
|
+
"reason",
|
|
13162
|
+
"address"
|
|
13163
|
+
]
|
|
13164
|
+
},
|
|
13112
13165
|
"GetPublicBlockersIdResponse": {
|
|
13113
13166
|
"type": "object",
|
|
13114
13167
|
"properties": {
|
|
@@ -16983,9 +17036,9 @@
|
|
|
16983
17036
|
"description": "A required dependency/feature is not configured or is temporarily down."
|
|
16984
17037
|
}
|
|
16985
17038
|
},
|
|
16986
|
-
"x-endpoint-count":
|
|
16987
|
-
"x-schema-count":
|
|
17039
|
+
"x-endpoint-count": 270,
|
|
17040
|
+
"x-schema-count": 303,
|
|
16988
17041
|
"x-undocumented-bodies": 8,
|
|
16989
|
-
"x-response-schemas":
|
|
17042
|
+
"x-response-schemas": 205,
|
|
16990
17043
|
"x-generated-by": "scripts/gds/gen-api-docs.js"
|
|
16991
17044
|
}
|
package/docs/api-reference.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
# Bongos API reference
|
|
4
4
|
|
|
5
|
-
> **Generated from the live route files** — the route file is authoritative.
|
|
5
|
+
> **Generated from the live route files** — the route file is authoritative. 270 endpoints across 44 route files.
|
|
6
6
|
> Machine-readable spec: [`docs/api/openapi.json`](api/openapi.json) (OpenAPI 3.1). Rendered docs site: **`/docs`** (e.g. `cloudbongos.com/docs`).
|
|
7
7
|
|
|
8
8
|
Base path: `/api/bongos`. Ranks (enforced server-side, [ADR 0016](adr/<redacted>.md)): `public` < `any-builder` < `metic+archon` < `archon`.
|
|
@@ -358,7 +358,7 @@ Base path: `/api/bongos`. Ranks (enforced server-side, [ADR 0016](adr/<redacted>
|
|
|
358
358
|
| GET | `/api/bongos/projects/featured` | `public` | — | a curated public showcase; no auth by design. |
|
|
359
359
|
| POST | `/api/bongos/projects/invite` | `metic+archon` | `client_id`, `github_login` | invite a builder into a project (ADR 0141 §5, task #1002115). |
|
|
360
360
|
|
|
361
|
-
## `provisioning` (
|
|
361
|
+
## `provisioning` (15)
|
|
362
362
|
|
|
363
363
|
| Method | Path | Rank | Body | Description |
|
|
364
364
|
|---|---|---|---|---|
|
|
@@ -375,6 +375,7 @@ Base path: `/api/bongos`. Ranks (enforced server-side, [ADR 0016](adr/<redacted>
|
|
|
375
375
|
| POST | `/api/bongos/provisioning/instances/:id/github-app` | `any-builder` | `account` | POST /provisioning/instances/:id/github-app — mint a GitHub-App-Manifest onboarding link for the caller's OWN instance (task 2080, ADR 01… |
|
|
376
376
|
| POST | `/api/bongos/provisioning/instances/:id/teardown` | `any-builder` | — | POST /provisioning/instances/:id/teardown — request teardown of the caller's OWN instance. |
|
|
377
377
|
| GET | `/api/bongos/provisioning/onboard-plan` | `public` | — | GET /provisioning/onboard-plan?mode=greenfield\|adopt&domain=<host> — the CANONICAL onboarding step sequence (task 2085; goal 35 / criteri… |
|
|
378
|
+
| GET | `/api/bongos/provisioning/slug-available` | `any-builder` | — | GET /provisioning/slug-available?slug=<handle> — is this handle free to create under? rank: any authenticated builder (same gate as the c… |
|
|
378
379
|
| GET | `/api/bongos/provisioning/tls-check` | `public` | — | GET /provisioning/tls-check?domain=<host> — the Caddy on-demand-TLS ALLOW-LIST (ADR 0111 §4, task P4 task 1945). |
|
|
379
380
|
|
|
380
381
|
## `public` (18)
|
|
@@ -532,6 +532,8 @@ function createClient(opts = {}) {
|
|
|
532
532
|
postProvisioningInstancesIdTeardown: (args) => request("POST", "/provisioning/instances/{id}/teardown", { hasBody: true }, args),
|
|
533
533
|
// GET /provisioning/onboard-plan — rank: public — GET /provisioning/onboard-plan
|
|
534
534
|
getProvisioningOnboardPlan: (args) => request("GET", "/provisioning/onboard-plan", { hasBody: false }, args),
|
|
535
|
+
// GET /provisioning/slug-available — rank: any-builder — GET /provisioning/slug-available
|
|
536
|
+
getProvisioningSlugAvailable: (args) => request("GET", "/provisioning/slug-available", { hasBody: false }, args),
|
|
535
537
|
// GET /provisioning/tls-check — rank: public — GET /provisioning/tls-check
|
|
536
538
|
getProvisioningTlsCheck: (args) => request("GET", "/provisioning/tls-check", { hasBody: false }, args),
|
|
537
539
|
},
|
|
@@ -12,14 +12,24 @@
|
|
|
12
12
|
//
|
|
13
13
|
// In-memory + per-process (the core limiter's model too). `now` is injectable so the
|
|
14
14
|
// window logic is unit-testable without a real clock.
|
|
15
|
+
//
|
|
16
|
+
// `key` picks the bucket, and defaults to the caller's IP — the only identity a PUBLIC
|
|
17
|
+
// endpoint has. An AUTHENTICATED route should key on the builder instead (task 1002898):
|
|
18
|
+
// per-IP starves co-located builders behind one office/CGNAT address for each other,
|
|
19
|
+
// and it is the weaker key anyway once a session is required, since an attacker rotates
|
|
20
|
+
// IPs far more easily than accounts. Keying on a bounded id also bounds the Map.
|
|
15
21
|
'use strict';
|
|
16
22
|
|
|
17
|
-
function
|
|
18
|
-
|
|
23
|
+
function clientIp(req) {
|
|
24
|
+
return req.ip || (req.socket && req.socket.remoteAddress) || 'unknown';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function makeAskRateLimiter({ windowMs = 60_000, max = 60, now = Date.now, key = clientIp } = {}) {
|
|
28
|
+
const buckets = new Map(); // key -> array of recent hit timestamps within the window
|
|
19
29
|
return function askRateLimit(req, res, next) {
|
|
20
30
|
const t = now();
|
|
21
|
-
const ip =
|
|
22
|
-
// Prune this
|
|
31
|
+
const ip = key(req) || 'unknown';
|
|
32
|
+
// Prune this caller's bucket to the window, then decide.
|
|
23
33
|
const hits = (buckets.get(ip) || []).filter((ts) => ts > t - windowMs);
|
|
24
34
|
if (hits.length >= max) {
|
|
25
35
|
const retryAfter = Math.max(1, Math.ceil((hits[0] + windowMs - t) / 1000));
|
|
@@ -32,4 +42,4 @@ function makeAskRateLimiter({ windowMs = 60_000, max = 60, now = Date.now } = {}
|
|
|
32
42
|
};
|
|
33
43
|
}
|
|
34
44
|
|
|
35
|
-
module.exports = { makeAskRateLimiter };
|
|
45
|
+
module.exports = { makeAskRateLimiter, clientIp };
|
|
@@ -33,7 +33,7 @@ const auth = api;
|
|
|
33
33
|
const { pool, validateOrRespond, parseId } = api;
|
|
34
34
|
const provisioning = require('../provisioning');
|
|
35
35
|
const { onboardPlan, stepsForShape } = require('../onboard-plan');
|
|
36
|
-
const { makeAskRateLimiter } = require('../rate-limit');
|
|
36
|
+
const { makeAskRateLimiter, clientIp } = require('../rate-limit');
|
|
37
37
|
|
|
38
38
|
// How long a GitHub-App-Manifest `state` token is valid — the owner must click the
|
|
39
39
|
// pre-filled link + approve on GitHub within this window (task 2080). Comfortably
|
|
@@ -242,6 +242,18 @@ module.exports = function provisioningRoutes() {
|
|
|
242
242
|
// higher than tls-check's one-shot ask. 120/min/IP gives comfortable headroom while
|
|
243
243
|
// still shedding a flood; in-memory, own bucket (does not share tls-check's budget).
|
|
244
244
|
const statusRateLimit = makeAskRateLimiter({ max: 120 });
|
|
245
|
+
// Limiter for the slug-availability check (task 1002898 N10). Signed-in-only, but a
|
|
246
|
+
// session is cheap and the route answers a yes/no about EVERY project slug on the
|
|
247
|
+
// instance — an enumeration oracle if left unthrottled. A wizard debouncing one handle
|
|
248
|
+
// field fires a handful per minute, so 30 is roomy for a human and tight for a crawler.
|
|
249
|
+
// Keyed on the BUILDER, not the IP (unlike the public limiters above): the route is
|
|
250
|
+
// authenticated, so per-IP would starve co-located builders behind one office address
|
|
251
|
+
// for each other while an attacker just rotates IPs. Own bucket either way — it must
|
|
252
|
+
// not spend, or be starved by, tls-check's budget.
|
|
253
|
+
const slugCheckRateLimit = makeAskRateLimiter({
|
|
254
|
+
max: 30,
|
|
255
|
+
key: (req) => (req.builder ? `b:${req.builder.id}` : clientIp(req)),
|
|
256
|
+
});
|
|
245
257
|
|
|
246
258
|
// Route ORDER is deliberate (the box.js precedent): the Archon-gated routes are
|
|
247
259
|
// declared FIRST, then the any-builder routes. The route-rank auditor
|
|
@@ -358,6 +370,16 @@ module.exports = function provisioningRoutes() {
|
|
|
358
370
|
reason: 'adopt layers onto an existing repo, so target_ref (owner/repo) is required',
|
|
359
371
|
});
|
|
360
372
|
}
|
|
373
|
+
// KNOWN GAP — the credential preflight belongs HERE, and is deliberately absent.
|
|
374
|
+
// This route validates that target_ref is PRESENT but never that the owner holds a
|
|
375
|
+
// usable repo credential for it, so an API caller can queue a standup that dies
|
|
376
|
+
// ~2 minutes later in raw git stderr (the task-1002697 walk did exactly that; the
|
|
377
|
+
// web wizard is only incidentally protected because it 403s earlier). Carved out of
|
|
378
|
+
// task 1002898 as N3 and tracked by task 1002909, which is BLOCKED on task 1002735
|
|
379
|
+
// deciding what credential a private-repo standup actually needs: written against
|
|
380
|
+
// the wrong definition, a preflight would refuse valid creates or keep admitting
|
|
381
|
+
// doomed ones. Greenfield must stay exempt when it lands — the platform creates
|
|
382
|
+
// that repo itself, so there is no pre-existing repo to hold a credential for.
|
|
361
383
|
try {
|
|
362
384
|
// A re-request must not re-litigate the address: createInstance never updates
|
|
363
385
|
// an existing row's domain, so for one the guards below could only misfire —
|
|
@@ -479,6 +501,54 @@ module.exports = function provisioningRoutes() {
|
|
|
479
501
|
}
|
|
480
502
|
});
|
|
481
503
|
|
|
504
|
+
// GET /provisioning/slug-available?slug=<handle> — is this handle free to create
|
|
505
|
+
// under? rank: any authenticated builder (same gate as the create route it previews;
|
|
506
|
+
// deliberately NOT public — the answer is a yes/no about every project slug on the
|
|
507
|
+
// instance, so it is an enumeration oracle and carries its own per-IP limiter).
|
|
508
|
+
//
|
|
509
|
+
// It ADVISORY-previews the create route and must agree with it exactly, so the
|
|
510
|
+
// normalization and the bar below are lifted from POST /provisioning/instances:
|
|
511
|
+
// trim+lowercase → isValidSlug → getInstanceBySlug (the byte-exact `slug text
|
|
512
|
+
// UNIQUE` in provisioning_001_tables.sql, which is what the 23505 catch in
|
|
513
|
+
// createInstance reflects — so no lower(slug) here, or we would over-report taken).
|
|
514
|
+
// The three refusals mirror the three ways create actually answers:
|
|
515
|
+
// 'taken' — another builder holds it → create 409s slug_taken
|
|
516
|
+
// 'yours' — the CALLER holds it → create is an idempotent re-request (200,
|
|
517
|
+
// created:false), not a new project, so it is not free to create under
|
|
518
|
+
// 'reserved' — the row is free but <slug>.<apex> is a platform-reserved address →
|
|
519
|
+
// create 400s domain_reserved on the DEFAULT (assigned-address) path.
|
|
520
|
+
// A no_address create would still succeed; the wizard only asks about
|
|
521
|
+
// the default path, and reporting it distinctly keeps that recoverable.
|
|
522
|
+
//
|
|
523
|
+
// NARROWS the TOCTOU window, never closes it: another builder can take the slug
|
|
524
|
+
// between this answer and the create. The 409 at POST /provisioning/instances
|
|
525
|
+
// remains the ONLY authority — this exists so a collision is seen while the owner
|
|
526
|
+
// is still typing instead of at the final Create click.
|
|
527
|
+
router.get('/provisioning/slug-available', auth.requireBuilder, slugCheckRateLimit, async (req, res) => {
|
|
528
|
+
const slug = String((req.query && req.query.slug) || '').trim().toLowerCase();
|
|
529
|
+
if (!provisioning.isValidSlug(slug)) {
|
|
530
|
+
return res.fail('bad_slug', 400, { reason: 'lowercase, 2–40 chars of [a-z0-9-], no leading/trailing/double dash' });
|
|
531
|
+
}
|
|
532
|
+
try {
|
|
533
|
+
const apex = instanceApex();
|
|
534
|
+
const address = provisioning.defaultDomainForSlug(slug, apex);
|
|
535
|
+
const holder = await provisioning.getInstanceBySlug(pool, slug);
|
|
536
|
+
let reason = null;
|
|
537
|
+
if (holder) {
|
|
538
|
+
reason = String(holder.owner_builder_id) === String(req.builder.id) ? 'yours' : 'taken';
|
|
539
|
+
} else if (address && provisioning.isReservedHostname(address, apex)) {
|
|
540
|
+
reason = 'reserved';
|
|
541
|
+
}
|
|
542
|
+
// Only the verdict travels — never the holder's row. A signed-in stranger
|
|
543
|
+
// learning WHO owns a handle (or anything about their instance) would be a
|
|
544
|
+
// disclosure this route has no reason to make.
|
|
545
|
+
res.json({ slug, available: reason === null, reason, address: address || null });
|
|
546
|
+
} catch (err) {
|
|
547
|
+
console.error('[provisioning] GET /provisioning/slug-available', err);
|
|
548
|
+
res.fail('check_failed', { status: 500, message: 'internal error' });
|
|
549
|
+
}
|
|
550
|
+
});
|
|
551
|
+
|
|
482
552
|
// GET /provisioning/instances — the caller's own instances + each open intent.
|
|
483
553
|
// rank: any authenticated builder (own resource read).
|
|
484
554
|
router.get('/provisioning/instances', auth.requireBuilder, async (req, res) => {
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.143",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.19.
|
|
9
|
+
"version": "1.19.143",
|
|
10
10
|
"license": "AGPL-3.0-or-later",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"express": "^4.21.2",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.143",
|
|
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",
|
|
@@ -98,6 +98,7 @@
|
|
|
98
98
|
--ink:#fdeedd; --ink-soft:#dcb693; --ink-faint:#b89272;
|
|
99
99
|
--rule:#5e3521; --rule-soft:#3f2317;
|
|
100
100
|
--accent-deep:color-mix(in srgb, var(--accent) 45%, #ffffff);
|
|
101
|
+
--link-ink:color-mix(in srgb, var(--accent) 51%, #ffffff);
|
|
101
102
|
--sea-ink:color-mix(in srgb, var(--sea) 65%, #ffffff);
|
|
102
103
|
--ok:#4ade80; --warn:#fbbf24; --err:#f87171;
|
|
103
104
|
--on-ok:#0b2e18;
|
|
@@ -125,6 +126,7 @@
|
|
|
125
126
|
--ink:#fdeedd; --ink-soft:#dcb693; --ink-faint:#b89272;
|
|
126
127
|
--rule:#5e3521; --rule-soft:#3f2317;
|
|
127
128
|
--accent-deep:color-mix(in srgb, var(--accent) 45%, #ffffff);
|
|
129
|
+
--link-ink:color-mix(in srgb, var(--accent) 51%, #ffffff);
|
|
128
130
|
--sea-ink:color-mix(in srgb, var(--sea) 65%, #ffffff);
|
|
129
131
|
--ok:#4ade80; --warn:#fbbf24; --err:#f87171;
|
|
130
132
|
--on-ok:#0b2e18;
|
|
@@ -160,6 +162,15 @@
|
|
|
160
162
|
--sea as-is; dark derives brighter (the --accent-deep pattern) so a pack's
|
|
161
163
|
light-tuned sea clears WCAG 1.4.11's 3:1 on the near-black card too */
|
|
162
164
|
--sea-ink:var(--sea);
|
|
165
|
+
/* TEXT LINKS ONLY (task 1002822). Light is --accent-deep unchanged; dark needs
|
|
166
|
+
its own mix because --accent-deep and --ink-soft land 2 lightness points
|
|
167
|
+
apart there (#e9ad8f vs #dcb693, ΔE76 9.7) — the same colour, and dark links
|
|
168
|
+
carry no underline until hover, so weight was the only cue left. 51% is the
|
|
169
|
+
ceiling, not a preference: --accent-deep can't simply be pushed (it also
|
|
170
|
+
feeds --chrome-accent/--glass-ink/--hero-b/--foot-code), and past 51% a dark
|
|
171
|
+
navy pack accent falls under 4.5:1 on --bg-deep. Chrome keeps --accent-deep;
|
|
172
|
+
only rules that paint a link inside prose take this. */
|
|
173
|
+
--link-ink:var(--accent-deep);
|
|
163
174
|
--neutral-soft:color-mix(in srgb, var(--ink) 8%, var(--bg-card));
|
|
164
175
|
/* ink-derived so the shadow is a WARM brown on the marigold ground instead of
|
|
165
176
|
the cold navy it used to cast. The dark blocks override it with a plain black
|
|
@@ -211,7 +222,7 @@ body{font-family:var(--font-body);color:var(--ink);background:var(--page-bg);min
|
|
|
211
222
|
.card{background:var(--card);border-radius:26px;box-shadow:var(--shadow-card);padding:clamp(22px,3.6vw,40px);}
|
|
212
223
|
h1{font-family:var(--font-display);font-weight:700;font-size:clamp(24px,3.4vw,32px);letter-spacing:-0.5px;line-height:1.15;margin-bottom:8px;}
|
|
213
224
|
.lede{font-size:15.5px;color:var(--ink-soft);font-weight:500;line-height:1.55;margin-bottom:24px;max-width:62ch;}
|
|
214
|
-
.lede a{color:var(--
|
|
225
|
+
.lede a{color:var(--link-ink);font-weight:700;text-decoration:none;}
|
|
215
226
|
.lede a:hover{text-decoration:underline;}
|
|
216
227
|
|
|
217
228
|
/* ── buttons ── */
|
|
@@ -225,7 +236,7 @@ h1{font-family:var(--font-display);font-weight:700;font-size:clamp(24px,3.4vw,32
|
|
|
225
236
|
.btn.danger{background:transparent;color:var(--err);box-shadow:none;border:2px solid color-mix(in srgb,var(--err) 35%,transparent);}
|
|
226
237
|
.btn.danger:hover{background:var(--err-soft);border-color:var(--err);}
|
|
227
238
|
.btn .gh{width:17px;height:17px;fill:currentColor;}
|
|
228
|
-
.linklike{background:none;border:none;cursor:pointer;font-family:var(--font-body);font-weight:700;font-size:13.5px;color:var(--
|
|
239
|
+
.linklike{background:none;border:none;cursor:pointer;font-family:var(--font-body);font-weight:700;font-size:13.5px;color:var(--link-ink);text-decoration:underline;padding:6px 4px;}
|
|
229
240
|
|
|
230
241
|
/* ── chips: the one status vocabulary (direction §4). Recency rides inside
|
|
231
242
|
(.rec) — "live" always says when it was last verified. ── */
|
|
@@ -262,7 +273,16 @@ h1{font-family:var(--font-display);font-weight:700;font-size:clamp(24px,3.4vw,32
|
|
|
262
273
|
/* ── project cards ── */
|
|
263
274
|
.projects{list-style:none;display:flex;flex-direction:column;gap:18px;}
|
|
264
275
|
.proj{border:2px solid var(--line);border-radius:18px;padding:18px 20px;background:var(--bg-card);display:flex;gap:16px;align-items:flex-start;}
|
|
265
|
-
.
|
|
276
|
+
/* torn down = muted TOKENS, not a subtree dim. opacity multiplies AFTER every
|
|
277
|
+
token resolves, so .78 dimmed the WHOLE card — meta to 3.72:1 light / 4.16:1
|
|
278
|
+
dark, address to 4.35:1 light, and the chips and buttons with them (task
|
|
279
|
+
1002823, the .humanSteps>li.slocked pattern from 1002751). Only the name needs
|
|
280
|
+
an override: .projMeta is already --ink-faint and measures once the multiply
|
|
281
|
+
is gone. The chip, the "Torn down" line and the actions keep full strength —
|
|
282
|
+
a dead project's last known state and its Bring-it-back button are what the
|
|
283
|
+
card is there for — and the ghosted .orb stays the graphic cue. */
|
|
284
|
+
.proj.gone{border-color:color-mix(in srgb,var(--line) 55%,var(--bg-card));}
|
|
285
|
+
.proj.gone .projName{color:var(--ink-soft);}
|
|
266
286
|
/* click-to-manage landing glow: the sky's owned orb hands focus to this card */
|
|
267
287
|
.proj.anchored{border-color:var(--accent);box-shadow:0 0 0 3px color-mix(in srgb,var(--accent) 30%,transparent);}
|
|
268
288
|
.projBody{flex:1;min-width:0;}
|
|
@@ -271,7 +291,7 @@ h1{font-family:var(--font-display);font-weight:700;font-size:clamp(24px,3.4vw,32
|
|
|
271
291
|
.projMeta{font-size:13px;font-weight:600;color:var(--ink-faint);margin-top:6px;display:flex;gap:14px;flex-wrap:wrap;}
|
|
272
292
|
.projMeta code{font-family:var(--font-mono);font-size:12.5px;background:var(--bg-deep);border-radius:6px;padding:1px 6px;color:var(--accent-deep);}
|
|
273
293
|
.addr{margin-top:12px;font-size:14.5px;font-weight:600;color:var(--ink-soft);}
|
|
274
|
-
.addr a{color:var(--
|
|
294
|
+
.addr a{color:var(--link-ink);font-weight:700;text-decoration:none;font-family:var(--font-mono);font-size:13.5px;}
|
|
275
295
|
.addr a:hover{text-decoration:underline;}
|
|
276
296
|
.addr .none{color:var(--ink-faint);font-style:italic;}
|
|
277
297
|
.addr code{font-family:var(--font-mono);font-size:13px;background:var(--bg-deep);border-radius:6px;padding:1px 7px;color:var(--accent-deep);}
|
|
@@ -282,7 +302,7 @@ h1{font-family:var(--font-display);font-weight:700;font-size:clamp(24px,3.4vw,32
|
|
|
282
302
|
.projMsg{font-size:13.5px;font-weight:600;margin-top:10px;min-height:18px;color:var(--ink-soft);line-height:1.5;}
|
|
283
303
|
.projMsg.ok{color:var(--ok);}
|
|
284
304
|
.projMsg.err{color:var(--err);}
|
|
285
|
-
.projMsg a{color:var(--
|
|
305
|
+
.projMsg a{color:var(--link-ink);font-weight:700;text-decoration:none;}
|
|
286
306
|
.projMsg a:hover{text-decoration:underline;}
|
|
287
307
|
.projMsg details{margin-top:8px;}
|
|
288
308
|
.projMsg summary{cursor:pointer;font-size:12.5px;font-weight:700;color:var(--ink-faint);}
|
|
@@ -293,7 +313,7 @@ h1{font-family:var(--font-display);font-weight:700;font-size:clamp(24px,3.4vw,32
|
|
|
293
313
|
.finePrint{font-size:12.5px;color:var(--ink-faint);font-weight:600;margin-top:16px;line-height:1.5;}
|
|
294
314
|
/* a link in fine print still has to look like one — without this it fell back to
|
|
295
315
|
the browser's default blue, the one colour in the page that is not ours */
|
|
296
|
-
.finePrint a{color:var(--
|
|
316
|
+
.finePrint a{color:var(--link-ink);font-weight:700;text-decoration:none;}
|
|
297
317
|
.finePrint a:hover{text-decoration:underline;}
|
|
298
318
|
|
|
299
319
|
/* ── progress strips: one visual, per-flow vocabularies (direction §4).
|
|
@@ -315,7 +335,7 @@ h1{font-family:var(--font-display);font-weight:700;font-size:clamp(24px,3.4vw,32
|
|
|
315
335
|
.strip .slbl{font-size:12px;font-weight:700;color:var(--ink-faint);}
|
|
316
336
|
.strip .stages li.now .slbl,.strip .stages li.done .slbl{color:color-mix(in srgb,var(--sea) 40%,var(--ink));}
|
|
317
337
|
.strip .pnote{text-align:center;font-size:13px;font-weight:600;color:var(--ink-soft);min-height:18px;line-height:1.5;}
|
|
318
|
-
.strip .pnote a{color:var(--
|
|
338
|
+
.strip .pnote a{color:var(--link-ink);font-weight:700;text-decoration:none;}
|
|
319
339
|
.strip.isLive{border-color:var(--ok-soft);background:linear-gradient(180deg,color-mix(in srgb,var(--ok) 8%,var(--bg-card)) 0%,var(--bg-card) 100%);}
|
|
320
340
|
.strip.isLive .stages li.done:not(:first-child)::before{background:var(--ok);}
|
|
321
341
|
.strip.isLive .stages li.done .sdot{border-color:var(--ok);background:var(--ok);}
|
|
@@ -651,7 +671,14 @@ p.sec-p{font-size:var(--fs-lede);font-weight:500;color:var(--ink-soft);line-heig
|
|
|
651
671
|
border:2px solid var(--line);border-radius:999px;padding:7px 14px;white-space:nowrap;}
|
|
652
672
|
.filters button:hover{border-color:var(--accent);color:var(--accent-deep);}
|
|
653
673
|
.filters button[aria-pressed="true"]{background:var(--accent);border-color:var(--accent);color:#fff;}
|
|
654
|
-
|
|
674
|
+
/* muted by TOKEN, not opacity — .75 multiplied after the ink resolved and took
|
|
675
|
+
the count to 4.05:1 on a resting pill and 3.23:1 on the pressed one (task
|
|
676
|
+
1002823). The pressed pill is excluded on purpose: its ground is the saturated
|
|
677
|
+
--accent fill, where no ink token clears 4.5:1 (--ink is the best at 3.70
|
|
678
|
+
light / 4.05 dark) — only the button's own #fff does, at 4.61, so the count
|
|
679
|
+
inherits it there. Size, weight and the mono face already say "secondary". */
|
|
680
|
+
.filters .n{font-family:var(--font-mono);font-weight:500;font-size:11.5px;}
|
|
681
|
+
.filters button:not([aria-pressed="true"]) .n{color:var(--ink-faint);}
|
|
655
682
|
.mineEmpty{font-size:14.5px;font-weight:600;color:var(--ink-soft);padding:10px 0 22px;}
|
|
656
683
|
|
|
657
684
|
/* ── the create rail ── */
|