@bongos/core 1.19.142 → 1.19.144
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 +32 -4
- package/public-landing/projects.html +129 -17
- package/src/module-api.js +5 -1
- package/tests/cosmos_token_contrast.mjs +63 -9
- 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.144",
|
|
6
|
+
"core_contract": "1.19.144",
|
|
7
|
+
"source_commit": "36beb6c51bf7da7c8677cd6cf29d3b3739b1fc86",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-08-
|
|
9
|
+
"built_at": "2026-08-15T16:06:10.087Z",
|
|
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": "5c594013e8ee432b685ee793f28efedacd67b463904cda4eb9942dd7dd6b62d3",
|
|
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": "a315183a94657af4ee558bc70eb2860782fa07576b9c55d0a028295f4287e2fc"
|
|
4181
4181
|
},
|
|
4182
4182
|
{
|
|
4183
4183
|
"path": "package.json",
|
|
4184
4184
|
"mode": "0000644",
|
|
4185
|
-
"sha256": "
|
|
4185
|
+
"sha256": "385cb6e8816fd2bfefa4a04d5a8bc4a9dac9e4d540903d9f736883b8d8abbc5c"
|
|
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": "4641a5c0c0222c0948ecdf57db9bb3cd88b5500089bbbbe735597abd4d44d4da"
|
|
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": "deae8db82f897ee77633ffb2b7f5144960d9961ba4cdb9e052346c6cb127b0c9"
|
|
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": "8b8d833fa63b772e134faf867ad771f33293f81d8af3dd695221489217dd7c89"
|
|
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": "dfb0f0b083a118c2bf8f424232f1e36e96f48aa962921649c487c93d5ce4eda2"
|
|
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.144",
|
|
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.144",
|
|
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.144",
|
|
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",
|
|
@@ -171,6 +171,18 @@
|
|
|
171
171
|
navy pack accent falls under 4.5:1 on --bg-deep. Chrome keeps --accent-deep;
|
|
172
172
|
only rules that paint a link inside prose take this. */
|
|
173
173
|
--link-ink:var(--accent-deep);
|
|
174
|
+
/* CONTROL BOUNDARIES ONLY (task 1002824) — the edge that tells you a text
|
|
175
|
+
field IS a field. --line is a CARD hairline and measures 1.16:1 light /
|
|
176
|
+
1.09:1 dark, so a resting input drawn with it was invisible until focus
|
|
177
|
+
(its fill is 1.02:1 on the card too, so the border is the ONLY boundary
|
|
178
|
+
there is); WCAG 1.4.11 asks 3:1. --line itself must not move — it also
|
|
179
|
+
draws .review, .humanSteps>li, .kv, .ghlist, the unselected .tpill and the
|
|
180
|
+
.choice cards, and darkening it globally would restyle every card in both
|
|
181
|
+
modes. Ink-derived on purpose, never accent-derived: a boundary that
|
|
182
|
+
changed hue with a re-theme would read as a state. 55% clears 3:1 with
|
|
183
|
+
headroom in the binding (light) mode — 3.75:1 on --card, 3.82:1 on the
|
|
184
|
+
input's own --bg-card fill; dark lands at 5.41/5.35. */
|
|
185
|
+
--ctl-line:color-mix(in srgb, var(--ink) 55%, var(--bg-card));
|
|
174
186
|
--neutral-soft:color-mix(in srgb, var(--ink) 8%, var(--bg-card));
|
|
175
187
|
/* ink-derived so the shadow is a WARM brown on the marigold ground instead of
|
|
176
188
|
the cold navy it used to cast. The dark blocks override it with a plain black
|
|
@@ -273,7 +285,16 @@ h1{font-family:var(--font-display);font-weight:700;font-size:clamp(24px,3.4vw,32
|
|
|
273
285
|
/* ── project cards ── */
|
|
274
286
|
.projects{list-style:none;display:flex;flex-direction:column;gap:18px;}
|
|
275
287
|
.proj{border:2px solid var(--line);border-radius:18px;padding:18px 20px;background:var(--bg-card);display:flex;gap:16px;align-items:flex-start;}
|
|
276
|
-
.
|
|
288
|
+
/* torn down = muted TOKENS, not a subtree dim. opacity multiplies AFTER every
|
|
289
|
+
token resolves, so .78 dimmed the WHOLE card — meta to 3.72:1 light / 4.16:1
|
|
290
|
+
dark, address to 4.35:1 light, and the chips and buttons with them (task
|
|
291
|
+
1002823, the .humanSteps>li.slocked pattern from 1002751). Only the name needs
|
|
292
|
+
an override: .projMeta is already --ink-faint and measures once the multiply
|
|
293
|
+
is gone. The chip, the "Torn down" line and the actions keep full strength —
|
|
294
|
+
a dead project's last known state and its Bring-it-back button are what the
|
|
295
|
+
card is there for — and the ghosted .orb stays the graphic cue. */
|
|
296
|
+
.proj.gone{border-color:color-mix(in srgb,var(--line) 55%,var(--bg-card));}
|
|
297
|
+
.proj.gone .projName{color:var(--ink-soft);}
|
|
277
298
|
/* click-to-manage landing glow: the sky's owned orb hands focus to this card */
|
|
278
299
|
.proj.anchored{border-color:var(--accent);box-shadow:0 0 0 3px color-mix(in srgb,var(--accent) 30%,transparent);}
|
|
279
300
|
.projBody{flex:1;min-width:0;}
|
|
@@ -287,7 +308,7 @@ h1{font-family:var(--font-display);font-weight:700;font-size:clamp(24px,3.4vw,32
|
|
|
287
308
|
.addr .none{color:var(--ink-faint);font-style:italic;}
|
|
288
309
|
.addr code{font-family:var(--font-mono);font-size:13px;background:var(--bg-deep);border-radius:6px;padding:1px 7px;color:var(--accent-deep);}
|
|
289
310
|
.attach{display:flex;gap:10px;margin-top:10px;flex-wrap:wrap;}
|
|
290
|
-
.attach input[type=text]{flex:1;min-width:220px;font-family:var(--font-body);font-size:15px;font-weight:600;color:var(--ink);background:var(--bg-card);border:2px solid var(--line);border-radius:12px;padding:9px 14px;outline:none;transition:border-color .15s ease, box-shadow .15s ease;}
|
|
311
|
+
.attach input[type=text]{flex:1;min-width:220px;font-family:var(--font-body);font-size:15px;font-weight:600;color:var(--ink);background:var(--bg-card);border:2px solid var(--ctl-line);border-radius:12px;padding:9px 14px;outline:none;transition:border-color .15s ease, box-shadow .15s ease;}
|
|
291
312
|
.attach input[type=text]::placeholder{color:var(--ink-faint);font-weight:500;}
|
|
292
313
|
.attach input[type=text]:focus{border-color:var(--accent);box-shadow:0 0 0 4px color-mix(in srgb,var(--accent) 13%,transparent);}
|
|
293
314
|
.projMsg{font-size:13.5px;font-weight:600;margin-top:10px;min-height:18px;color:var(--ink-soft);line-height:1.5;}
|
|
@@ -651,7 +672,7 @@ p.sec-p{font-size:var(--fs-lede);font-weight:500;color:var(--ink-soft);line-heig
|
|
|
651
672
|
the chip outcomes; it never relabels them — "Live" holds both `live` and
|
|
652
673
|
`live?` because neither is known-down, and each card still says which. ── */
|
|
653
674
|
.mineBar{display:flex;align-items:center;gap:12px;flex-wrap:wrap;margin-bottom:20px;}
|
|
654
|
-
.mineBar .search{background:var(--bg-card);border-color:var(--line);}
|
|
675
|
+
.mineBar .search{background:var(--bg-card);border-color:var(--ctl-line);}
|
|
655
676
|
.mineBar .search input{color:var(--ink);}
|
|
656
677
|
.mineBar .search input::placeholder{color:var(--ink-faint);}
|
|
657
678
|
.mineBar .search svg{stroke:var(--ink-faint);}
|
|
@@ -662,7 +683,14 @@ p.sec-p{font-size:var(--fs-lede);font-weight:500;color:var(--ink-soft);line-heig
|
|
|
662
683
|
border:2px solid var(--line);border-radius:999px;padding:7px 14px;white-space:nowrap;}
|
|
663
684
|
.filters button:hover{border-color:var(--accent);color:var(--accent-deep);}
|
|
664
685
|
.filters button[aria-pressed="true"]{background:var(--accent);border-color:var(--accent);color:#fff;}
|
|
665
|
-
|
|
686
|
+
/* muted by TOKEN, not opacity — .75 multiplied after the ink resolved and took
|
|
687
|
+
the count to 4.05:1 on a resting pill and 3.23:1 on the pressed one (task
|
|
688
|
+
1002823). The pressed pill is excluded on purpose: its ground is the saturated
|
|
689
|
+
--accent fill, where no ink token clears 4.5:1 (--ink is the best at 3.70
|
|
690
|
+
light / 4.05 dark) — only the button's own #fff does, at 4.61, so the count
|
|
691
|
+
inherits it there. Size, weight and the mono face already say "secondary". */
|
|
692
|
+
.filters .n{font-family:var(--font-mono);font-weight:500;font-size:11.5px;}
|
|
693
|
+
.filters button:not([aria-pressed="true"]) .n{color:var(--ink-faint);}
|
|
666
694
|
.mineEmpty{font-size:14.5px;font-weight:600;color:var(--ink-soft);padding:10px 0 22px;}
|
|
667
695
|
|
|
668
696
|
/* ── the create rail ── */
|