@bongos/core 1.19.206 → 1.19.207
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 +31 -21
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +4 -0
- package/clients/bongos-client/index.cjs +4 -0
- package/clients/bongos-client/index.d.ts +7 -0
- package/clients/bongos-client/index.mjs +4 -0
- package/docs/api/openapi.json +156 -3
- package/docs/api-reference.md +4 -2
- package/modules/dev-box/app/src/vendor/bongos-client.cjs +4 -0
- package/modules/provisioning/migrations/provisioning_009_project_settings.sql +19 -0
- package/modules/provisioning/provisioning.js +41 -2
- package/modules/provisioning/routes/provisioning.js +63 -0
- package/modules/provisioning/tests/provisioning.mjs +38 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/src/module-api.js +3 -1
- package/tests/provisioning_settings.mjs +200 -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.207",
|
|
6
|
+
"core_contract": "1.19.207",
|
|
7
|
+
"source_commit": "f67179175e23c7d9144c3ede28fd365783eed34f",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-08-17T20:
|
|
9
|
+
"built_at": "2026-08-17T20:29:20.492Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 310,
|
|
13
13
|
"agent_docs_stubbed": 21,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 1298,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 1629,
|
|
20
|
+
"tree_sha256": "fb6abbc6818ae6e399e4154a08816d3aeeaf4d9cd65ab89ff1bb3ce3f33428b5",
|
|
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": "2755203dec4a1450c5950c39a767bc093d1d1227f952193001cdfa94205e995f"
|
|
351
351
|
},
|
|
352
352
|
{
|
|
353
353
|
"path": "clients/bongos-client/bongos-client.global.js",
|
|
354
354
|
"mode": "0000644",
|
|
355
|
-
"sha256": "
|
|
355
|
+
"sha256": "7b4e5ecf6c6d12a3c9e603a0f5b4bd28ba8cd991399ed8941181b6aeae53772d"
|
|
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": "a92355ef21073634500f6806f1e495c955a0b67e889cf07264ed8d1169b119be"
|
|
366
366
|
},
|
|
367
367
|
{
|
|
368
368
|
"path": "clients/bongos-client/index.d.ts",
|
|
369
369
|
"mode": "0000644",
|
|
370
|
-
"sha256": "
|
|
370
|
+
"sha256": "8fe97a3daefdceb9bf40a15a19dffb81fb6387c0acbe20041e58e63b24e80dd1"
|
|
371
371
|
},
|
|
372
372
|
{
|
|
373
373
|
"path": "clients/bongos-client/index.mjs",
|
|
374
374
|
"mode": "0000644",
|
|
375
|
-
"sha256": "
|
|
375
|
+
"sha256": "adb2a36a9598c543adbf7c8011f36585f7c394bac7f4961718359ee7a29cb1dd"
|
|
376
376
|
},
|
|
377
377
|
{
|
|
378
378
|
"path": "clients/bongos-client/package.json",
|
|
@@ -1367,12 +1367,12 @@
|
|
|
1367
1367
|
{
|
|
1368
1368
|
"path": "docs/api-reference.md",
|
|
1369
1369
|
"mode": "0000644",
|
|
1370
|
-
"sha256": "
|
|
1370
|
+
"sha256": "52554cf7c8e5355c59594c872f30975581eb6c1b9a5bcb63efa1a1a19f2d3877"
|
|
1371
1371
|
},
|
|
1372
1372
|
{
|
|
1373
1373
|
"path": "docs/api/openapi.json",
|
|
1374
1374
|
"mode": "0000644",
|
|
1375
|
-
"sha256": "
|
|
1375
|
+
"sha256": "825af2c499836301a34cff79192e41556cc863fe9a5378d9e5134c862769c2fa"
|
|
1376
1376
|
},
|
|
1377
1377
|
{
|
|
1378
1378
|
"path": "docs/architecture.md",
|
|
@@ -2682,7 +2682,7 @@
|
|
|
2682
2682
|
{
|
|
2683
2683
|
"path": "modules/dev-box/app/src/vendor/bongos-client.cjs",
|
|
2684
2684
|
"mode": "0000644",
|
|
2685
|
-
"sha256": "
|
|
2685
|
+
"sha256": "a92355ef21073634500f6806f1e495c955a0b67e889cf07264ed8d1169b119be"
|
|
2686
2686
|
},
|
|
2687
2687
|
{
|
|
2688
2688
|
"path": "modules/dev-box/app/test/unit.mjs",
|
|
@@ -3949,6 +3949,11 @@
|
|
|
3949
3949
|
"mode": "0000644",
|
|
3950
3950
|
"sha256": "8dc04a18ad77563a2bd73029bf13cab03fb5dadd1b62850fddbde475257e6a76"
|
|
3951
3951
|
},
|
|
3952
|
+
{
|
|
3953
|
+
"path": "modules/provisioning/migrations/provisioning_009_project_settings.sql",
|
|
3954
|
+
"mode": "0000644",
|
|
3955
|
+
"sha256": "de17cfb74f8684044abfb03e56698f91fa5aa813608f3a19d2ddf81d113e897e"
|
|
3956
|
+
},
|
|
3952
3957
|
{
|
|
3953
3958
|
"path": "modules/provisioning/module.json",
|
|
3954
3959
|
"mode": "0000644",
|
|
@@ -3967,7 +3972,7 @@
|
|
|
3967
3972
|
{
|
|
3968
3973
|
"path": "modules/provisioning/provisioning.js",
|
|
3969
3974
|
"mode": "0000644",
|
|
3970
|
-
"sha256": "
|
|
3975
|
+
"sha256": "b9d4ee116eb4b213b084408868f727a4587876faa5fae78f4d2f64cb2fe12231"
|
|
3971
3976
|
},
|
|
3972
3977
|
{
|
|
3973
3978
|
"path": "modules/provisioning/rate-limit.js",
|
|
@@ -3977,7 +3982,7 @@
|
|
|
3977
3982
|
{
|
|
3978
3983
|
"path": "modules/provisioning/routes/provisioning.js",
|
|
3979
3984
|
"mode": "0000644",
|
|
3980
|
-
"sha256": "
|
|
3985
|
+
"sha256": "78c89017e17dfba57ade2330fce293e6422aab316e9311f8215ff3eb9fdc3fe5"
|
|
3981
3986
|
},
|
|
3982
3987
|
{
|
|
3983
3988
|
"path": "modules/provisioning/tests/liveness.mjs",
|
|
@@ -3987,7 +3992,7 @@
|
|
|
3987
3992
|
{
|
|
3988
3993
|
"path": "modules/provisioning/tests/provisioning.mjs",
|
|
3989
3994
|
"mode": "0000644",
|
|
3990
|
-
"sha256": "
|
|
3995
|
+
"sha256": "3db404b6ad50e01de0b1d73131cefc01ab8fe939eee18157ba6aa8f28a05fb7f"
|
|
3991
3996
|
},
|
|
3992
3997
|
{
|
|
3993
3998
|
"path": "modules/provisioning/ui/.gitkeep",
|
|
@@ -4277,12 +4282,12 @@
|
|
|
4277
4282
|
{
|
|
4278
4283
|
"path": "package-lock.json",
|
|
4279
4284
|
"mode": "0000644",
|
|
4280
|
-
"sha256": "
|
|
4285
|
+
"sha256": "fc64c06226801735546c101af64db40d30492a4ce855303e538bac1111a18930"
|
|
4281
4286
|
},
|
|
4282
4287
|
{
|
|
4283
4288
|
"path": "package.json",
|
|
4284
4289
|
"mode": "0000644",
|
|
4285
|
-
"sha256": "
|
|
4290
|
+
"sha256": "d3457496b5c76bab949eb1693518d88745db75eb7bd3afb4299d68e545cdb0bd"
|
|
4286
4291
|
},
|
|
4287
4292
|
{
|
|
4288
4293
|
"path": "public-docs/index.html",
|
|
@@ -5892,7 +5897,7 @@
|
|
|
5892
5897
|
{
|
|
5893
5898
|
"path": "src/module-api.js",
|
|
5894
5899
|
"mode": "0000644",
|
|
5895
|
-
"sha256": "
|
|
5900
|
+
"sha256": "167ead50611054c2a0d46ebb14510c993e7c05e9c86428cad2d15d78aae0bad4"
|
|
5896
5901
|
},
|
|
5897
5902
|
{
|
|
5898
5903
|
"path": "src/module-loader/catalog.js",
|
|
@@ -7539,6 +7544,11 @@
|
|
|
7539
7544
|
"mode": "0000644",
|
|
7540
7545
|
"sha256": "7749df454ac3cca7c7e5ea1acc94ead15d1c9f87b38fe2028a1c93c8adf31bcf"
|
|
7541
7546
|
},
|
|
7547
|
+
{
|
|
7548
|
+
"path": "tests/provisioning_settings.mjs",
|
|
7549
|
+
"mode": "0000644",
|
|
7550
|
+
"sha256": "bb800d2e15045efbac4fadbc62996054d33e163eb86e6a5e09087d409cc7fbfa"
|
|
7551
|
+
},
|
|
7542
7552
|
{
|
|
7543
7553
|
"path": "tests/provisioning_slug_available.mjs",
|
|
7544
7554
|
"mode": "0000644",
|
|
@@ -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
|
+
- 278 operations across 46 resource groups
|
|
9
9
|
|
|
10
10
|
## Install
|
|
11
11
|
|
|
@@ -543,6 +543,10 @@ function createClient(opts = {}) {
|
|
|
543
543
|
postProvisioningInstancesIdForceTeardown: (args) => request("POST", "/provisioning/instances/{id}/force-teardown", { hasBody: true }, args),
|
|
544
544
|
// POST /provisioning/instances/{id}/github-app — rank: any-builder — POST /provisioning/instances/:id/github-app
|
|
545
545
|
postProvisioningInstancesIdGithubApp: (args) => request("POST", "/provisioning/instances/{id}/github-app", { hasBody: true }, args),
|
|
546
|
+
// GET /provisioning/instances/{id}/settings — rank: any-builder — GET /provisioning/instances/:id/settings
|
|
547
|
+
getProvisioningInstancesIdSettings: (args) => request("GET", "/provisioning/instances/{id}/settings", { hasBody: false }, args),
|
|
548
|
+
// PATCH /provisioning/instances/{id}/settings — rank: any-builder — PATCH /provisioning/instances/:id/settings
|
|
549
|
+
patchProvisioningInstancesIdSettings: (args) => request("PATCH", "/provisioning/instances/{id}/settings", { hasBody: true }, args),
|
|
546
550
|
// POST /provisioning/instances/{id}/teardown — rank: any-builder — POST /provisioning/instances/:id/teardown
|
|
547
551
|
postProvisioningInstancesIdTeardown: (args) => request("POST", "/provisioning/instances/{id}/teardown", { hasBody: true }, args),
|
|
548
552
|
// GET /provisioning/onboard-plan — rank: public — GET /provisioning/onboard-plan
|
|
@@ -542,6 +542,10 @@ function createClient(opts = {}) {
|
|
|
542
542
|
postProvisioningInstancesIdForceTeardown: (args) => request("POST", "/provisioning/instances/{id}/force-teardown", { hasBody: true }, args),
|
|
543
543
|
// POST /provisioning/instances/{id}/github-app — rank: any-builder — POST /provisioning/instances/:id/github-app
|
|
544
544
|
postProvisioningInstancesIdGithubApp: (args) => request("POST", "/provisioning/instances/{id}/github-app", { hasBody: true }, args),
|
|
545
|
+
// GET /provisioning/instances/{id}/settings — rank: any-builder — GET /provisioning/instances/:id/settings
|
|
546
|
+
getProvisioningInstancesIdSettings: (args) => request("GET", "/provisioning/instances/{id}/settings", { hasBody: false }, args),
|
|
547
|
+
// PATCH /provisioning/instances/{id}/settings — rank: any-builder — PATCH /provisioning/instances/:id/settings
|
|
548
|
+
patchProvisioningInstancesIdSettings: (args) => request("PATCH", "/provisioning/instances/{id}/settings", { hasBody: true }, args),
|
|
545
549
|
// POST /provisioning/instances/{id}/teardown — rank: any-builder — POST /provisioning/instances/:id/teardown
|
|
546
550
|
postProvisioningInstancesIdTeardown: (args) => request("POST", "/provisioning/instances/{id}/teardown", { hasBody: true }, args),
|
|
547
551
|
// GET /provisioning/onboard-plan — rank: public — GET /provisioning/onboard-plan
|
|
@@ -75,6 +75,7 @@ export interface GetProvisioningFleetResponse { instances: unknown }
|
|
|
75
75
|
export interface GetProvisioningGithubAppStatusResponse { ok: boolean; status: unknown; ready: unknown; terminal: unknown; domain: unknown; instance_status: unknown; app_slug: unknown }
|
|
76
76
|
export interface GetProvisioningHealthResponse { module: unknown; ok: boolean; coreVersion: unknown }
|
|
77
77
|
export interface GetProvisioningInstancesIdResponse { instance: unknown; open_intent: unknown }
|
|
78
|
+
export interface GetProvisioningInstancesIdSettingsResponse { ok: boolean; settings: unknown; options: unknown }
|
|
78
79
|
export interface GetProvisioningInstancesResponse { instances: unknown }
|
|
79
80
|
export interface GetProvisioningOnboardPlanResponse { mode: unknown; steps: unknown }
|
|
80
81
|
export interface GetProvisioningSlugAvailableResponse { slug: unknown; available: unknown; reason: unknown; address: unknown }
|
|
@@ -140,6 +141,8 @@ export interface PatchMeSkillPrefsResponse { prefs: unknown; effective: unknown;
|
|
|
140
141
|
export interface PatchMeSoundPrefsResponse { prefs: unknown; effective: unknown; effective_map: unknown; volume: unknown; defaults: unknown; known_sounds: unknown }
|
|
141
142
|
export interface PatchProjectsIdFeaturedRequest { featured: boolean }
|
|
142
143
|
export interface PatchProjectsIdFeaturedResponse { project: unknown }
|
|
144
|
+
export interface PatchProvisioningInstancesIdSettingsRequest { platform_visibility?: string; joinability?: string }
|
|
145
|
+
export interface PatchProvisioningInstancesIdSettingsResponse { ok: boolean; settings: unknown }
|
|
143
146
|
export interface PatchTasksIdRequest { kind?: string; discipline?: string; requires_rank?: string; module_key?: string; goal_id?: number; parent_task_id?: StringifiableId; parallel_safe?: unknown; newcomer_friendly?: boolean; xenos_claimable?: boolean; security_sensitive?: boolean; automation_tag?: string; needs_migration?: boolean; description?: string; status?: unknown }
|
|
144
147
|
export interface PostAccessRequestsRequest { github_login: string; display_name?: string; note?: string }
|
|
145
148
|
export interface PostAccessRequestsResponse { ok: boolean; request?: unknown; already_pending?: boolean }
|
|
@@ -787,6 +790,10 @@ export interface BongosClient {
|
|
|
787
790
|
postProvisioningInstancesIdForceTeardown(args?: RequestArgs): Promise<PostProvisioningInstancesIdForceTeardownResponse>;
|
|
788
791
|
/** POST /provisioning/instances/{id}/github-app — rank: any-builder */
|
|
789
792
|
postProvisioningInstancesIdGithubApp(args?: RequestArgs & { body?: PostProvisioningInstancesIdGithubAppRequest }): Promise<PostProvisioningInstancesIdGithubAppResponse>;
|
|
793
|
+
/** GET /provisioning/instances/{id}/settings — rank: any-builder */
|
|
794
|
+
getProvisioningInstancesIdSettings(args?: RequestArgs): Promise<GetProvisioningInstancesIdSettingsResponse>;
|
|
795
|
+
/** PATCH /provisioning/instances/{id}/settings — rank: any-builder */
|
|
796
|
+
patchProvisioningInstancesIdSettings(args?: RequestArgs & { body?: PatchProvisioningInstancesIdSettingsRequest }): Promise<PatchProvisioningInstancesIdSettingsResponse>;
|
|
790
797
|
/** POST /provisioning/instances/{id}/teardown — rank: any-builder */
|
|
791
798
|
postProvisioningInstancesIdTeardown(args?: RequestArgs): Promise<PostProvisioningInstancesIdTeardownResponse>;
|
|
792
799
|
/** GET /provisioning/onboard-plan — rank: public */
|
|
@@ -539,6 +539,10 @@ export function createClient(opts = {}) {
|
|
|
539
539
|
postProvisioningInstancesIdForceTeardown: (args) => request("POST", "/provisioning/instances/{id}/force-teardown", { hasBody: true }, args),
|
|
540
540
|
// POST /provisioning/instances/{id}/github-app — rank: any-builder — POST /provisioning/instances/:id/github-app
|
|
541
541
|
postProvisioningInstancesIdGithubApp: (args) => request("POST", "/provisioning/instances/{id}/github-app", { hasBody: true }, args),
|
|
542
|
+
// GET /provisioning/instances/{id}/settings — rank: any-builder — GET /provisioning/instances/:id/settings
|
|
543
|
+
getProvisioningInstancesIdSettings: (args) => request("GET", "/provisioning/instances/{id}/settings", { hasBody: false }, args),
|
|
544
|
+
// PATCH /provisioning/instances/{id}/settings — rank: any-builder — PATCH /provisioning/instances/:id/settings
|
|
545
|
+
patchProvisioningInstancesIdSettings: (args) => request("PATCH", "/provisioning/instances/{id}/settings", { hasBody: true }, args),
|
|
542
546
|
// POST /provisioning/instances/{id}/teardown — rank: any-builder — POST /provisioning/instances/:id/teardown
|
|
543
547
|
postProvisioningInstancesIdTeardown: (args) => request("POST", "/provisioning/instances/{id}/teardown", { hasBody: true }, args),
|
|
544
548
|
// GET /provisioning/onboard-plan — rank: public — GET /provisioning/onboard-plan
|
package/docs/api/openapi.json
CHANGED
|
@@ -8951,6 +8951,119 @@
|
|
|
8951
8951
|
]
|
|
8952
8952
|
}
|
|
8953
8953
|
},
|
|
8954
|
+
"/provisioning/instances/{id}/settings": {
|
|
8955
|
+
"get": {
|
|
8956
|
+
"operationId": "get_provisioning_instances_id_settings",
|
|
8957
|
+
"tags": [
|
|
8958
|
+
"provisioning"
|
|
8959
|
+
],
|
|
8960
|
+
"summary": "GET /provisioning/instances/:id/settings",
|
|
8961
|
+
"description": "GET /provisioning/instances/:id/settings — read a project's effective settings + the allowed values (so a form can render its choices). rank: any authenticated builder (own resource; archon may read any).\n\n**Rank:** `any-builder` — Any authenticated builder (row-level ownership enforced in-handler).",
|
|
8962
|
+
"x-rank": "any-builder",
|
|
8963
|
+
"x-source": "modules/provisioning/routes/provisioning.js",
|
|
8964
|
+
"parameters": [
|
|
8965
|
+
{
|
|
8966
|
+
"name": "id",
|
|
8967
|
+
"in": "path",
|
|
8968
|
+
"required": true,
|
|
8969
|
+
"schema": {
|
|
8970
|
+
"type": "string"
|
|
8971
|
+
},
|
|
8972
|
+
"description": "Path parameter `id`."
|
|
8973
|
+
}
|
|
8974
|
+
],
|
|
8975
|
+
"responses": {
|
|
8976
|
+
"200": {
|
|
8977
|
+
"description": "Success.",
|
|
8978
|
+
"content": {
|
|
8979
|
+
"application/json": {
|
|
8980
|
+
"schema": {
|
|
8981
|
+
"$ref": "#/components/schemas/GetProvisioningInstancesIdSettingsResponse"
|
|
8982
|
+
}
|
|
8983
|
+
}
|
|
8984
|
+
}
|
|
8985
|
+
},
|
|
8986
|
+
"400": {
|
|
8987
|
+
"$ref": "#/components/responses/BadRequest"
|
|
8988
|
+
},
|
|
8989
|
+
"401": {
|
|
8990
|
+
"$ref": "#/components/responses/Unauthorized"
|
|
8991
|
+
},
|
|
8992
|
+
"403": {
|
|
8993
|
+
"$ref": "#/components/responses/Forbidden"
|
|
8994
|
+
},
|
|
8995
|
+
"404": {
|
|
8996
|
+
"$ref": "#/components/responses/NotFound"
|
|
8997
|
+
}
|
|
8998
|
+
},
|
|
8999
|
+
"security": [
|
|
9000
|
+
{
|
|
9001
|
+
"builderSession": []
|
|
9002
|
+
}
|
|
9003
|
+
]
|
|
9004
|
+
},
|
|
9005
|
+
"patch": {
|
|
9006
|
+
"operationId": "patch_provisioning_instances_id_settings",
|
|
9007
|
+
"tags": [
|
|
9008
|
+
"provisioning"
|
|
9009
|
+
],
|
|
9010
|
+
"summary": "PATCH /provisioning/instances/:id/settings",
|
|
9011
|
+
"description": "PATCH /provisioning/instances/:id/settings — change a project's settings. Body: any subset of the known keys; strict validation refuses unknown keys and off-vocabulary values. rank: any authenticated builder (own resource; archon may change any — the override is audited via the event's actor).\n\n**Rank:** `any-builder` — Any authenticated builder (row-level ownership enforced in-handler).",
|
|
9012
|
+
"x-rank": "any-builder",
|
|
9013
|
+
"x-source": "modules/provisioning/routes/provisioning.js",
|
|
9014
|
+
"parameters": [
|
|
9015
|
+
{
|
|
9016
|
+
"name": "id",
|
|
9017
|
+
"in": "path",
|
|
9018
|
+
"required": true,
|
|
9019
|
+
"schema": {
|
|
9020
|
+
"type": "string"
|
|
9021
|
+
},
|
|
9022
|
+
"description": "Path parameter `id`."
|
|
9023
|
+
}
|
|
9024
|
+
],
|
|
9025
|
+
"requestBody": {
|
|
9026
|
+
"required": false,
|
|
9027
|
+
"content": {
|
|
9028
|
+
"application/json": {
|
|
9029
|
+
"schema": {
|
|
9030
|
+
"$ref": "#/components/schemas/PatchProvisioningInstancesIdSettingsRequest"
|
|
9031
|
+
}
|
|
9032
|
+
}
|
|
9033
|
+
},
|
|
9034
|
+
"x-validated": true
|
|
9035
|
+
},
|
|
9036
|
+
"responses": {
|
|
9037
|
+
"200": {
|
|
9038
|
+
"description": "Success.",
|
|
9039
|
+
"content": {
|
|
9040
|
+
"application/json": {
|
|
9041
|
+
"schema": {
|
|
9042
|
+
"$ref": "#/components/schemas/PatchProvisioningInstancesIdSettingsResponse"
|
|
9043
|
+
}
|
|
9044
|
+
}
|
|
9045
|
+
}
|
|
9046
|
+
},
|
|
9047
|
+
"400": {
|
|
9048
|
+
"$ref": "#/components/responses/ValidationFailed"
|
|
9049
|
+
},
|
|
9050
|
+
"401": {
|
|
9051
|
+
"$ref": "#/components/responses/Unauthorized"
|
|
9052
|
+
},
|
|
9053
|
+
"403": {
|
|
9054
|
+
"$ref": "#/components/responses/Forbidden"
|
|
9055
|
+
},
|
|
9056
|
+
"404": {
|
|
9057
|
+
"$ref": "#/components/responses/NotFound"
|
|
9058
|
+
}
|
|
9059
|
+
},
|
|
9060
|
+
"security": [
|
|
9061
|
+
{
|
|
9062
|
+
"builderSession": []
|
|
9063
|
+
}
|
|
9064
|
+
]
|
|
9065
|
+
}
|
|
9066
|
+
},
|
|
8954
9067
|
"/provisioning/instances/{id}/teardown": {
|
|
8955
9068
|
"post": {
|
|
8956
9069
|
"operationId": "post_provisioning_instances_id_teardown",
|
|
@@ -13441,6 +13554,21 @@
|
|
|
13441
13554
|
"open_intent"
|
|
13442
13555
|
]
|
|
13443
13556
|
},
|
|
13557
|
+
"GetProvisioningInstancesIdSettingsResponse": {
|
|
13558
|
+
"type": "object",
|
|
13559
|
+
"properties": {
|
|
13560
|
+
"ok": {
|
|
13561
|
+
"type": "boolean"
|
|
13562
|
+
},
|
|
13563
|
+
"settings": {},
|
|
13564
|
+
"options": {}
|
|
13565
|
+
},
|
|
13566
|
+
"required": [
|
|
13567
|
+
"ok",
|
|
13568
|
+
"settings",
|
|
13569
|
+
"options"
|
|
13570
|
+
]
|
|
13571
|
+
},
|
|
13444
13572
|
"GetProvisioningInstancesResponse": {
|
|
13445
13573
|
"type": "object",
|
|
13446
13574
|
"properties": {
|
|
@@ -14253,6 +14381,31 @@
|
|
|
14253
14381
|
"project"
|
|
14254
14382
|
]
|
|
14255
14383
|
},
|
|
14384
|
+
"PatchProvisioningInstancesIdSettingsRequest": {
|
|
14385
|
+
"type": "object",
|
|
14386
|
+
"properties": {
|
|
14387
|
+
"platform_visibility": {
|
|
14388
|
+
"type": "string"
|
|
14389
|
+
},
|
|
14390
|
+
"joinability": {
|
|
14391
|
+
"type": "string"
|
|
14392
|
+
}
|
|
14393
|
+
},
|
|
14394
|
+
"additionalProperties": false
|
|
14395
|
+
},
|
|
14396
|
+
"PatchProvisioningInstancesIdSettingsResponse": {
|
|
14397
|
+
"type": "object",
|
|
14398
|
+
"properties": {
|
|
14399
|
+
"ok": {
|
|
14400
|
+
"type": "boolean"
|
|
14401
|
+
},
|
|
14402
|
+
"settings": {}
|
|
14403
|
+
},
|
|
14404
|
+
"required": [
|
|
14405
|
+
"ok",
|
|
14406
|
+
"settings"
|
|
14407
|
+
]
|
|
14408
|
+
},
|
|
14256
14409
|
"PatchTasksIdRequest": {
|
|
14257
14410
|
"type": "object",
|
|
14258
14411
|
"properties": {
|
|
@@ -17434,9 +17587,9 @@
|
|
|
17434
17587
|
"description": "A required dependency/feature is not configured or is temporarily down."
|
|
17435
17588
|
}
|
|
17436
17589
|
},
|
|
17437
|
-
"x-endpoint-count":
|
|
17438
|
-
"x-schema-count":
|
|
17590
|
+
"x-endpoint-count": 278,
|
|
17591
|
+
"x-schema-count": 314,
|
|
17439
17592
|
"x-undocumented-bodies": 8,
|
|
17440
|
-
"x-response-schemas":
|
|
17593
|
+
"x-response-schemas": 212,
|
|
17441
17594
|
"x-generated-by": "scripts/gds/gen-api-docs.js"
|
|
17442
17595
|
}
|
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. 278 endpoints across 46 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`.
|
|
@@ -369,7 +369,7 @@ Base path: `/api/bongos`. Ranks (enforced server-side, [ADR 0016](adr/<redacted>
|
|
|
369
369
|
| GET | `/api/bongos/projects/featured` | `public` | — | a curated public showcase; no auth by design. |
|
|
370
370
|
| POST | `/api/bongos/projects/invite` | `metic+archon` | `client_id`, `github_login` | invite a builder into a project (ADR 0141 §5, task #1002115). |
|
|
371
371
|
|
|
372
|
-
## `provisioning` (
|
|
372
|
+
## `provisioning` (17)
|
|
373
373
|
|
|
374
374
|
| Method | Path | Rank | Body | Description |
|
|
375
375
|
|---|---|---|---|---|
|
|
@@ -384,6 +384,8 @@ Base path: `/api/bongos`. Ranks (enforced server-side, [ADR 0016](adr/<redacted>
|
|
|
384
384
|
| POST | `/api/bongos/provisioning/instances/:id/domain` | `any-builder` | `domain` | POST /provisioning/instances/:id/domain — give the caller's OWN instance its address (task 1002692, gap 5: no path existed to add a domai… |
|
|
385
385
|
| POST | `/api/bongos/provisioning/instances/:id/force-teardown` | `metic+archon` | — | POST /provisioning/instances/:id/force-teardown — Archon tears down ANY instance. |
|
|
386
386
|
| 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… |
|
|
387
|
+
| GET | `/api/bongos/provisioning/instances/:id/settings` | `any-builder` | — | GET /provisioning/instances/:id/settings — read a project's effective settings + the allowed values (so a form can render its choices). |
|
|
388
|
+
| PATCH | `/api/bongos/provisioning/instances/:id/settings` | `any-builder` | `platform_visibility`, `joinability` | PATCH /provisioning/instances/:id/settings — change a project's settings. |
|
|
387
389
|
| POST | `/api/bongos/provisioning/instances/:id/teardown` | `any-builder` | — | POST /provisioning/instances/:id/teardown — request teardown of the caller's OWN instance. |
|
|
388
390
|
| 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… |
|
|
389
391
|
| 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… |
|
|
@@ -542,6 +542,10 @@ function createClient(opts = {}) {
|
|
|
542
542
|
postProvisioningInstancesIdForceTeardown: (args) => request("POST", "/provisioning/instances/{id}/force-teardown", { hasBody: true }, args),
|
|
543
543
|
// POST /provisioning/instances/{id}/github-app — rank: any-builder — POST /provisioning/instances/:id/github-app
|
|
544
544
|
postProvisioningInstancesIdGithubApp: (args) => request("POST", "/provisioning/instances/{id}/github-app", { hasBody: true }, args),
|
|
545
|
+
// GET /provisioning/instances/{id}/settings — rank: any-builder — GET /provisioning/instances/:id/settings
|
|
546
|
+
getProvisioningInstancesIdSettings: (args) => request("GET", "/provisioning/instances/{id}/settings", { hasBody: false }, args),
|
|
547
|
+
// PATCH /provisioning/instances/{id}/settings — rank: any-builder — PATCH /provisioning/instances/:id/settings
|
|
548
|
+
patchProvisioningInstancesIdSettings: (args) => request("PATCH", "/provisioning/instances/{id}/settings", { hasBody: true }, args),
|
|
545
549
|
// POST /provisioning/instances/{id}/teardown — rank: any-builder — POST /provisioning/instances/:id/teardown
|
|
546
550
|
postProvisioningInstancesIdTeardown: (args) => request("POST", "/provisioning/instances/{id}/teardown", { hasBody: true }, args),
|
|
547
551
|
// GET /provisioning/onboard-plan — rank: public — GET /provisioning/onboard-plan
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
-- provisioning_009_project_settings.sql — per-project settings storage (task
|
|
2
|
+
-- 1003040, goal 1000058: the project-management page wave's foundation).
|
|
3
|
+
--
|
|
4
|
+
-- WHY jsonb, not columns: the manage-page wave adds settings one task at a time
|
|
5
|
+
-- (platform visibility, joinability, repo visibility, …) and a new key must not
|
|
6
|
+
-- cost a migration each time. The column is a dumb store; the known keys + their
|
|
7
|
+
-- vocabularies are enforced at the API layer (provisioning.SETTINGS_VOCAB /
|
|
8
|
+
-- the settings routes' validator), and unknown stored values fall back to the
|
|
9
|
+
-- default at read time (effectiveSettings) rather than leaking out.
|
|
10
|
+
--
|
|
11
|
+
-- DEFAULTS = today's behavior: '{}'::jsonb READS as
|
|
12
|
+
-- { platform_visibility: 'public', joinability: 'apply' } — the default lives in
|
|
13
|
+
-- code, applied at read time, so ADD COLUMN backfills every existing row with an
|
|
14
|
+
-- empty object and no instance changes behavior.
|
|
15
|
+
ALTER TABLE provisioning_instances
|
|
16
|
+
ADD COLUMN IF NOT EXISTS settings jsonb NOT NULL DEFAULT '{}'::jsonb;
|
|
17
|
+
|
|
18
|
+
INSERT INTO schema_migrations (version) VALUES ('provisioning_009_project_settings')
|
|
19
|
+
ON CONFLICT (version) DO NOTHING;
|
|
@@ -59,6 +59,17 @@ const PROVISION_ENQUEUE_FROM = new Set(['requested', 'provisioning', 'error', 't
|
|
|
59
59
|
// itself. Mid-flight or torn-down states refuse truthfully.
|
|
60
60
|
const DOMAIN_ATTACH_FROM = new Set(['requested', 'error', 'active']);
|
|
61
61
|
|
|
62
|
+
// Per-project settings (task 1003040, goal 1000058): the known keys + their
|
|
63
|
+
// vocabularies, enforced at the API layer — the jsonb `settings` column
|
|
64
|
+
// (migration provisioning_009) is a dumb store with room for future keys.
|
|
65
|
+
// DEFAULT_SETTINGS is TODAY's behavior (public surfaces, apply-to-join),
|
|
66
|
+
// applied at read time so pre-existing rows need no data backfill.
|
|
67
|
+
const SETTINGS_VOCAB = Object.freeze({
|
|
68
|
+
platform_visibility: ['public', 'gated'],
|
|
69
|
+
joinability: ['open', 'apply', 'invite_only'],
|
|
70
|
+
});
|
|
71
|
+
const DEFAULT_SETTINGS = Object.freeze({ platform_visibility: 'public', joinability: 'apply' });
|
|
72
|
+
|
|
62
73
|
// Monthly cost estimate (USD) for a dedicated droplet tier — VISIBILITY only
|
|
63
74
|
// (ADR 0111 §6), never a bill. A small local table so this module needs no
|
|
64
75
|
// sibling-module import; P2 (shared control-plane client extraction) is where a
|
|
@@ -210,6 +221,19 @@ function publicInstance(row) {
|
|
|
210
221
|
};
|
|
211
222
|
}
|
|
212
223
|
|
|
224
|
+
// The instance's EFFECTIVE settings: the defaults overlaid with the stored,
|
|
225
|
+
// on-vocabulary keys. An off-vocabulary or unknown stored value falls back to
|
|
226
|
+
// its default rather than leaking into the read surface. PURE.
|
|
227
|
+
function effectiveSettings(row) {
|
|
228
|
+
const stored = row && row.settings && typeof row.settings === 'object' && !Array.isArray(row.settings)
|
|
229
|
+
? row.settings : {};
|
|
230
|
+
const out = { ...DEFAULT_SETTINGS };
|
|
231
|
+
for (const [key, vocab] of Object.entries(SETTINGS_VOCAB)) {
|
|
232
|
+
if (vocab.includes(stored[key])) out[key] = stored[key];
|
|
233
|
+
}
|
|
234
|
+
return out;
|
|
235
|
+
}
|
|
236
|
+
|
|
213
237
|
// ---------------------------------------------------------------------------
|
|
214
238
|
// GitHub App Manifest flow — PURE builders (task 2080, ADR 0133).
|
|
215
239
|
// GitHub has no create-an-OAuth-App API, but the App *Manifest* flow lets the owner
|
|
@@ -345,6 +369,20 @@ async function getInstanceByDomain(db, domain) {
|
|
|
345
369
|
return rows[0] || null;
|
|
346
370
|
}
|
|
347
371
|
|
|
372
|
+
// Merge a VALIDATED settings patch into the instance's settings column (task
|
|
373
|
+
// 1003040). Narrow like updateInstanceDomain: settings + updated_at only — a
|
|
374
|
+
// settings change is a deliberate owner action, so surfacing it in the
|
|
375
|
+
// updated_at-ordered card list is correct (unlike the liveness overlay).
|
|
376
|
+
async function updateInstanceSettings(db, id, patch) {
|
|
377
|
+
const { rows } = await db.query(
|
|
378
|
+
`UPDATE provisioning_instances
|
|
379
|
+
SET settings = settings || $2::jsonb, updated_at = now()
|
|
380
|
+
WHERE id = $1 RETURNING *`,
|
|
381
|
+
[id, JSON.stringify(patch)]
|
|
382
|
+
);
|
|
383
|
+
return rows[0] || null;
|
|
384
|
+
}
|
|
385
|
+
|
|
348
386
|
// Record an instance's domain — the ONLY column this touches (task 1002692). Status
|
|
349
387
|
// changes stay setInstanceStatus's job; keeping this narrow means the domain-attach
|
|
350
388
|
// route can never drift an instance's lifecycle as a side effect.
|
|
@@ -735,9 +773,10 @@ module.exports = {
|
|
|
735
773
|
// constants
|
|
736
774
|
INSTANCE_STATES, HOSTING_SHAPES, ONBOARD_MODES, PROJECT_TYPES, INTENT_ACTIONS, TEARDOWN_FROM,
|
|
737
775
|
PROVISION_ENQUEUE_FROM, DOMAIN_ATTACH_FROM, TIER_MONTHLY_USD, CERT_ELIGIBLE_STATES,
|
|
776
|
+
SETTINGS_VOCAB, DEFAULT_SETTINGS,
|
|
738
777
|
// pure
|
|
739
778
|
isValidSlug, costEstimateUsd, publicInstance, normalizeHostname,
|
|
740
|
-
defaultDomainForSlug, isReservedHostname,
|
|
779
|
+
defaultDomainForSlug, isReservedHostname, effectiveSettings,
|
|
741
780
|
// pure — GitHub App Manifest flow (task 2080)
|
|
742
781
|
githubAppName, buildGithubAppManifest, manifestPostUrl,
|
|
743
782
|
// data access
|
|
@@ -745,7 +784,7 @@ module.exports = {
|
|
|
745
784
|
getInstanceById, getInstanceBySlug, getInstanceByDomain, listInstancesForOwner, listAllInstances,
|
|
746
785
|
// data access — verification overlay (task 1002696)
|
|
747
786
|
listActiveInstancesForLiveness, recordLivenessProbe, recordDnsNote, clearErrorNote,
|
|
748
|
-
createInstance, setInstanceStatus, updateInstanceDomain,
|
|
787
|
+
createInstance, setInstanceStatus, updateInstanceDomain, updateInstanceSettings,
|
|
749
788
|
enqueueIntent, enqueueTeardown, getOpenIntent, claimNextIntent, resolveIntent,
|
|
750
789
|
recordEvent, instanceStateForSlug, costLedger,
|
|
751
790
|
// data access — GitHub App Manifest attempts (task 2080)
|
|
@@ -591,6 +591,69 @@ module.exports = function provisioningRoutes() {
|
|
|
591
591
|
}
|
|
592
592
|
});
|
|
593
593
|
|
|
594
|
+
// ── Per-project settings (task 1003040, goal 1000058) ─────────────────────
|
|
595
|
+
// The foundation the manage-page wave hangs off: effective settings are the
|
|
596
|
+
// DEFAULTS (today's behavior — public surfaces, apply-to-join) overlaid with
|
|
597
|
+
// the instance's stored keys. Own-scoped with an Archon override: the owner
|
|
598
|
+
// manages their own project; an Archon may act on any (fleet support). 404
|
|
599
|
+
// for everyone else — no existence leak (the GET-by-id precedent).
|
|
600
|
+
function settingsAccessDenied(inst, builder) {
|
|
601
|
+
if (!inst) return true;
|
|
602
|
+
if (builder.rank === 'archon') return false;
|
|
603
|
+
return String(inst.owner_builder_id) !== String(builder.id);
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// GET /provisioning/instances/:id/settings — read a project's effective
|
|
607
|
+
// settings + the allowed values (so a form can render its choices).
|
|
608
|
+
// rank: any authenticated builder (own resource; archon may read any).
|
|
609
|
+
router.get('/provisioning/instances/:id/settings', auth.requireBuilder, async (req, res) => {
|
|
610
|
+
const id = parseId(req, res); // (req, res) — parseId sends 400 bad_id + returns null on failure
|
|
611
|
+
if (!id) return;
|
|
612
|
+
try {
|
|
613
|
+
const inst = await provisioning.getInstanceById(pool, id);
|
|
614
|
+
if (settingsAccessDenied(inst, req.builder)) return res.fail('instance_not_found', 404);
|
|
615
|
+
res.json({ ok: true, settings: provisioning.effectiveSettings(inst), options: provisioning.SETTINGS_VOCAB });
|
|
616
|
+
} catch (err) {
|
|
617
|
+
console.error('[provisioning] GET /provisioning/instances/:id/settings', err);
|
|
618
|
+
res.fail('settings_read_failed', { status: 500, message: 'internal error' });
|
|
619
|
+
}
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
// PATCH /provisioning/instances/:id/settings — change a project's settings.
|
|
623
|
+
// Body: any subset of the known keys; strict validation refuses unknown keys
|
|
624
|
+
// and off-vocabulary values. rank: any authenticated builder (own resource;
|
|
625
|
+
// archon may change any — the override is audited via the event's actor).
|
|
626
|
+
router.patch('/provisioning/instances/:id/settings', auth.requireBuilder, async (req, res) => {
|
|
627
|
+
const id = parseId(req, res); // (req, res) — parseId sends 400 bad_id + returns null on failure
|
|
628
|
+
if (!id) return;
|
|
629
|
+
if (validateOrRespond(req, res, {
|
|
630
|
+
platform_visibility: { type: 'string', enum: provisioning.SETTINGS_VOCAB.platform_visibility },
|
|
631
|
+
joinability: { type: 'string', enum: provisioning.SETTINGS_VOCAB.joinability },
|
|
632
|
+
})) return;
|
|
633
|
+
const patch = {};
|
|
634
|
+
for (const key of Object.keys(provisioning.SETTINGS_VOCAB)) {
|
|
635
|
+
if ((req.body || {})[key] !== undefined) patch[key] = req.body[key];
|
|
636
|
+
}
|
|
637
|
+
if (!Object.keys(patch).length) {
|
|
638
|
+
return res.fail('bad_settings', { status: 400,
|
|
639
|
+
message: `The body carries no settings to change — send any of: ${Object.keys(provisioning.SETTINGS_VOCAB).join(', ')}.` });
|
|
640
|
+
}
|
|
641
|
+
try {
|
|
642
|
+
const inst = await provisioning.getInstanceById(pool, id);
|
|
643
|
+
if (settingsAccessDenied(inst, req.builder)) return res.fail('instance_not_found', 404);
|
|
644
|
+
const updated = await provisioning.updateInstanceSettings(pool, id, patch);
|
|
645
|
+
await provisioning.recordEvent(pool, {
|
|
646
|
+
instanceId: id, ownerBuilderId: inst.owner_builder_id, event: 'settings',
|
|
647
|
+
detail: `settings changed: ${Object.entries(patch).map(([k, v]) => `${k}=${v}`).join(', ')}`,
|
|
648
|
+
actor: String(inst.owner_builder_id) === String(req.builder.id) ? 'api:self' : 'api:archon',
|
|
649
|
+
}).catch(() => {});
|
|
650
|
+
res.json({ ok: true, settings: provisioning.effectiveSettings(updated) });
|
|
651
|
+
} catch (err) {
|
|
652
|
+
console.error('[provisioning] PATCH /provisioning/instances/:id/settings', err);
|
|
653
|
+
res.fail('settings_update_failed', { status: 500, message: 'internal error' });
|
|
654
|
+
}
|
|
655
|
+
});
|
|
656
|
+
|
|
594
657
|
// POST /provisioning/instances/:id/teardown — request teardown of the caller's
|
|
595
658
|
// OWN instance. rank: any authenticated builder (own resource). Enqueues a
|
|
596
659
|
// 'teardown' intent; the runner executes it (web tier holds no tokens). 404 when
|
|
@@ -260,5 +260,43 @@ await ta('recordDnsNote / clearErrorNote are NARROW single-column updates — no
|
|
|
260
260
|
assert.doesNotMatch(sql, /status\s*=|updated_at/, 'releasing the latch must not touch the lifecycle');
|
|
261
261
|
});
|
|
262
262
|
|
|
263
|
+
console.log('\nPer-project settings (task 1003040):');
|
|
264
|
+
|
|
265
|
+
t('effectiveSettings: defaults for a bare/empty row = today\'s behavior; stored keys overlay; junk falls back', () => {
|
|
266
|
+
assert.deepEqual(P.effectiveSettings(null), { platform_visibility: 'public', joinability: 'apply' });
|
|
267
|
+
assert.deepEqual(P.effectiveSettings({}), { platform_visibility: 'public', joinability: 'apply' });
|
|
268
|
+
assert.deepEqual(P.effectiveSettings({ settings: {} }), { platform_visibility: 'public', joinability: 'apply' });
|
|
269
|
+
assert.deepEqual(
|
|
270
|
+
P.effectiveSettings({ settings: { platform_visibility: 'gated' } }),
|
|
271
|
+
{ platform_visibility: 'gated', joinability: 'apply' },
|
|
272
|
+
'a stored key overlays only itself'
|
|
273
|
+
);
|
|
274
|
+
assert.deepEqual(
|
|
275
|
+
P.effectiveSettings({ settings: { platform_visibility: 'bogus', joinability: 'invite_only', future_key: 'x' } }),
|
|
276
|
+
{ platform_visibility: 'public', joinability: 'invite_only' },
|
|
277
|
+
'off-vocabulary values fall back to the default; unknown keys never leak out'
|
|
278
|
+
);
|
|
279
|
+
assert.deepEqual(P.effectiveSettings({ settings: ['not', 'an', 'object'] }),
|
|
280
|
+
{ platform_visibility: 'public', joinability: 'apply' }, 'a non-object settings value reads as defaults');
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
t('SETTINGS_VOCAB / DEFAULT_SETTINGS: every default is on its own vocabulary (lockstep guard)', () => {
|
|
284
|
+
for (const [key, dflt] of Object.entries(P.DEFAULT_SETTINGS)) {
|
|
285
|
+
assert.ok(P.SETTINGS_VOCAB[key], `default key '${key}' has a vocabulary`);
|
|
286
|
+
assert.ok(P.SETTINGS_VOCAB[key].includes(dflt), `default '${dflt}' is a legal '${key}' value`);
|
|
287
|
+
}
|
|
288
|
+
assert.deepEqual(Object.keys(P.SETTINGS_VOCAB).sort(), Object.keys(P.DEFAULT_SETTINGS).sort(),
|
|
289
|
+
'every known key has a default (an undefaulted key would read as undefined)');
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
await ta('updateInstanceSettings: jsonb MERGE (||) + updated_at, parameterized, returns the row', async () => {
|
|
293
|
+
let sql = '', params = null;
|
|
294
|
+
const db = { query: async (q, p) => { sql = q; params = p; return { rows: [{ id: 18, settings: { joinability: 'open' } }] }; } };
|
|
295
|
+
const row = await P.updateInstanceSettings(db, 18, { joinability: 'open' });
|
|
296
|
+
assert.equal(row.settings.joinability, 'open');
|
|
297
|
+
assert.match(sql, /SET settings = settings \|\| \$2::jsonb, updated_at = now\(\)/, 'merges — never clobbers other keys');
|
|
298
|
+
assert.deepEqual(params, [18, JSON.stringify({ joinability: 'open' })]);
|
|
299
|
+
});
|
|
300
|
+
|
|
263
301
|
console.log(`\nprovisioning module: ${passed} passed, ${failed} failed`);
|
|
264
302
|
process.exit(failed ? 1 : 0);
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.207",
|
|
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.207",
|
|
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.207",
|
|
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/src/module-api.js
CHANGED
|
@@ -839,8 +839,10 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
839
839
|
// landed since 1.19.204 with no explicit bump. run 32061149280. (task 1002620)
|
|
840
840
|
// 1.19.206 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
841
841
|
// landed since 1.19.205 with no explicit bump. run 32064728346. (task 1002620)
|
|
842
|
+
// 1.19.207 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
843
|
+
// landed since 1.19.206 with no explicit bump. run 32065995444. (task 1002620)
|
|
842
844
|
// ---------------------------------------------------------------------------
|
|
843
|
-
const CORE_VERSION = '1.19.
|
|
845
|
+
const CORE_VERSION = '1.19.207'; // CI auto-patch carrier (ADR 0161); see changelog above
|
|
844
846
|
|
|
845
847
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
846
848
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
// tests/provisioning_settings.mjs
|
|
2
|
+
//
|
|
3
|
+
// Per-project settings foundation (task 1003040, goal 1000058) — the surface the
|
|
4
|
+
// manage-page wave hangs off:
|
|
5
|
+
// • GET /provisioning/instances/:id/settings — effective settings + options
|
|
6
|
+
// • PATCH /provisioning/instances/:id/settings — validated, own-scoped change
|
|
7
|
+
//
|
|
8
|
+
// The load-bearing assertions:
|
|
9
|
+
// • the OWN-SCOPE WALL: another builder's GET/PATCH answers 404 and the write
|
|
10
|
+
// path is NEVER reached (no existence leak, no cross-owner mutation)
|
|
11
|
+
// • the Archon override works and is audited as api:archon
|
|
12
|
+
// • strict validation: off-vocabulary values and unknown keys are 400s
|
|
13
|
+
// • defaults = today's behavior (public / apply) for rows that predate the column
|
|
14
|
+
//
|
|
15
|
+
// DB-free: data-access fns are stubbed on the cached module object (the
|
|
16
|
+
// provisioning_domain_attach.mjs pattern). No network beyond localhost.
|
|
17
|
+
//
|
|
18
|
+
// Run: node tests/provisioning_settings.mjs
|
|
19
|
+
|
|
20
|
+
import { strict as assert } from 'node:assert';
|
|
21
|
+
import { createRequire } from 'node:module';
|
|
22
|
+
import http from 'node:http';
|
|
23
|
+
|
|
24
|
+
const require = createRequire(import.meta.url);
|
|
25
|
+
const provisioning = require('../modules/provisioning/provisioning.js');
|
|
26
|
+
|
|
27
|
+
let passed = 0, failed = 0;
|
|
28
|
+
async function ta(name, fn) {
|
|
29
|
+
try { await fn(); passed++; console.log(` PASS ${name}`); }
|
|
30
|
+
catch (e) { failed++; console.log(` FAIL ${name}\n ${e.message}`); }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ── stubbed data access on the cached module object ──────────────────────────
|
|
34
|
+
let instance = null; // the row getInstanceById answers with
|
|
35
|
+
let updateCalls = []; // recorded updateInstanceSettings calls
|
|
36
|
+
let events = []; // recorded provisioning_events
|
|
37
|
+
provisioning.getInstanceById = async (_db, id) => (instance ? { ...instance, id } : null);
|
|
38
|
+
provisioning.updateInstanceSettings = async (_db, id, patch) => {
|
|
39
|
+
updateCalls.push({ id, patch });
|
|
40
|
+
instance = { ...instance, settings: { ...(instance.settings || {}), ...patch } };
|
|
41
|
+
return { ...instance, id };
|
|
42
|
+
};
|
|
43
|
+
provisioning.recordEvent = async (_db, e) => { events.push(e); };
|
|
44
|
+
provisioning.getOpenIntent = async () => null;
|
|
45
|
+
|
|
46
|
+
const api = require('../src/module-api.js');
|
|
47
|
+
let builder = { id: 7, github_login: 'owner', rank: 'thetes' };
|
|
48
|
+
api.requireBuilder = (req, _res, next) => { req.builder = builder; next(); };
|
|
49
|
+
api.requirePermission = () => (_req, _res, next) => next();
|
|
50
|
+
api.branding = () => ({
|
|
51
|
+
domains: { publicOrigin: 'https://cb.test' },
|
|
52
|
+
identity: { productName: 'Cloud Bongos' },
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const express = require('express');
|
|
56
|
+
const provisioningRoutes = require('../modules/provisioning/routes/provisioning.js');
|
|
57
|
+
const app = express();
|
|
58
|
+
app.use(express.json());
|
|
59
|
+
app.use((_req, res, next) => {
|
|
60
|
+
res.fail = (code, opts, details) => {
|
|
61
|
+
const status = typeof opts === 'number' ? opts : (opts && opts.status) || 400;
|
|
62
|
+
res.status(status).json({ error: { code, ...(typeof opts === 'object' ? opts : {}), ...(details || {}) } });
|
|
63
|
+
};
|
|
64
|
+
next();
|
|
65
|
+
});
|
|
66
|
+
app.use('/api/bongos', provisioningRoutes());
|
|
67
|
+
|
|
68
|
+
const server = app.listen(0);
|
|
69
|
+
await new Promise((r) => server.once('listening', r));
|
|
70
|
+
const base = `http://127.0.0.1:${server.address().port}/api/bongos`;
|
|
71
|
+
|
|
72
|
+
function call(method, path, body) {
|
|
73
|
+
return new Promise((resolve, reject) => {
|
|
74
|
+
const payload = body === undefined ? null : JSON.stringify(body);
|
|
75
|
+
const req = http.request(`${base}${path}`, {
|
|
76
|
+
method,
|
|
77
|
+
headers: payload
|
|
78
|
+
? { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) }
|
|
79
|
+
: {},
|
|
80
|
+
}, (res) => {
|
|
81
|
+
let d = '';
|
|
82
|
+
res.on('data', (c) => { d += c; });
|
|
83
|
+
res.on('end', () => { let j = null; try { j = JSON.parse(d); } catch { /* non-JSON */ } resolve({ status: res.statusCode, body: j, raw: d }); });
|
|
84
|
+
});
|
|
85
|
+
req.on('error', reject);
|
|
86
|
+
req.end(payload || undefined);
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
const get = (p) => call('GET', p);
|
|
90
|
+
const patch = (p, b) => call('PATCH', p, b);
|
|
91
|
+
|
|
92
|
+
function reset({ inst, as } = {}) {
|
|
93
|
+
instance = inst !== undefined ? inst
|
|
94
|
+
: { owner_builder_id: 7, slug: 'spike', status: 'active', domain: 'spike.cloudbongos.com', settings: {} };
|
|
95
|
+
builder = as || { id: 7, github_login: 'owner', rank: 'thetes' };
|
|
96
|
+
updateCalls = [];
|
|
97
|
+
events = [];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
console.log('GET /provisioning/instances/:id/settings:');
|
|
101
|
+
|
|
102
|
+
await ta('owner reads DEFAULTS (public / apply) on a row with empty settings — today\'s behavior', async () => {
|
|
103
|
+
reset();
|
|
104
|
+
const r = await get('/provisioning/instances/18/settings');
|
|
105
|
+
assert.equal(r.status, 200, r.raw);
|
|
106
|
+
assert.deepEqual(r.body.settings, { platform_visibility: 'public', joinability: 'apply' });
|
|
107
|
+
assert.deepEqual(r.body.options, { platform_visibility: ['public', 'gated'], joinability: ['open', 'apply', 'invite_only'] });
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
await ta('stored keys overlay the defaults', async () => {
|
|
111
|
+
reset({ inst: { owner_builder_id: 7, slug: 'spike', status: 'active', settings: { joinability: 'invite_only' } } });
|
|
112
|
+
const r = await get('/provisioning/instances/18/settings');
|
|
113
|
+
assert.equal(r.status, 200, r.raw);
|
|
114
|
+
assert.deepEqual(r.body.settings, { platform_visibility: 'public', joinability: 'invite_only' });
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
await ta('ANOTHER builder\'s GET → 404 (no existence leak)', async () => {
|
|
118
|
+
reset({ as: { id: 8, github_login: 'stranger', rank: 'thetes' } });
|
|
119
|
+
const r = await get('/provisioning/instances/18/settings');
|
|
120
|
+
assert.equal(r.status, 404, r.raw);
|
|
121
|
+
assert.equal(r.body.error.code, 'instance_not_found');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
await ta('an Archon may read ANY instance (the override)', async () => {
|
|
125
|
+
reset({ as: { id: 999, github_login: 'archon', rank: 'archon' } });
|
|
126
|
+
const r = await get('/provisioning/instances/18/settings');
|
|
127
|
+
assert.equal(r.status, 200, r.raw);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
await ta('unknown instance id → 404 for everyone', async () => {
|
|
131
|
+
reset({ inst: null });
|
|
132
|
+
const r = await get('/provisioning/instances/999/settings');
|
|
133
|
+
assert.equal(r.status, 404, r.raw);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
console.log('\nPATCH /provisioning/instances/:id/settings:');
|
|
137
|
+
|
|
138
|
+
await ta('owner changes one key — merged, returned effective, audited api:self', async () => {
|
|
139
|
+
reset();
|
|
140
|
+
const r = await patch('/provisioning/instances/18/settings', { joinability: 'open' });
|
|
141
|
+
assert.equal(r.status, 200, r.raw);
|
|
142
|
+
assert.deepEqual(r.body.settings, { platform_visibility: 'public', joinability: 'open' });
|
|
143
|
+
assert.deepEqual(updateCalls, [{ id: 18, patch: { joinability: 'open' } }], 'exactly the sent key, nothing else');
|
|
144
|
+
assert.equal(events.length, 1);
|
|
145
|
+
assert.equal(events[0].event, 'settings');
|
|
146
|
+
assert.equal(events[0].actor, 'api:self');
|
|
147
|
+
assert.match(events[0].detail, /joinability=open/);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
await ta('owner changes both keys in one call', async () => {
|
|
151
|
+
reset();
|
|
152
|
+
const r = await patch('/provisioning/instances/18/settings', { platform_visibility: 'gated', joinability: 'invite_only' });
|
|
153
|
+
assert.equal(r.status, 200, r.raw);
|
|
154
|
+
assert.deepEqual(r.body.settings, { platform_visibility: 'gated', joinability: 'invite_only' });
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
await ta('the OWN-SCOPE WALL: another builder\'s PATCH → 404 and the write path is never reached', async () => {
|
|
158
|
+
reset({ as: { id: 8, github_login: 'stranger', rank: 'thetes' } });
|
|
159
|
+
const r = await patch('/provisioning/instances/18/settings', { joinability: 'open' });
|
|
160
|
+
assert.equal(r.status, 404, r.raw);
|
|
161
|
+
assert.equal(r.body.error.code, 'instance_not_found');
|
|
162
|
+
assert.equal(updateCalls.length, 0, 'no cross-owner mutation');
|
|
163
|
+
assert.equal(events.length, 0, 'no event for a refused write');
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
await ta('an Archon may change ANY instance — audited api:archon', async () => {
|
|
167
|
+
reset({ as: { id: 999, github_login: 'archon', rank: 'archon' } });
|
|
168
|
+
const r = await patch('/provisioning/instances/18/settings', { platform_visibility: 'gated' });
|
|
169
|
+
assert.equal(r.status, 200, r.raw);
|
|
170
|
+
assert.equal(events[0].actor, 'api:archon', 'the override is distinguishable in the audit ledger');
|
|
171
|
+
assert.equal(events[0].ownerBuilderId, 7, 'the event still belongs to the instance owner');
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
await ta('an off-vocabulary value → 400 validation_failed (nothing written)', async () => {
|
|
175
|
+
reset();
|
|
176
|
+
const r = await patch('/provisioning/instances/18/settings', { joinability: 'closed' });
|
|
177
|
+
assert.equal(r.status, 400, r.raw);
|
|
178
|
+
assert.equal(r.body.error.code, 'validation_failed');
|
|
179
|
+
assert.equal(updateCalls.length, 0);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
await ta('an unknown key → 400 (strict: a typo fails loudly, never silently dropped)', async () => {
|
|
183
|
+
reset();
|
|
184
|
+
const r = await patch('/provisioning/instances/18/settings', { joinabilty: 'open' });
|
|
185
|
+
assert.equal(r.status, 400, r.raw);
|
|
186
|
+
assert.equal(r.body.error.code, 'validation_failed');
|
|
187
|
+
assert.equal(updateCalls.length, 0);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
await ta('an empty body → 400 bad_settings with a plain sentence naming the keys', async () => {
|
|
191
|
+
reset();
|
|
192
|
+
const r = await patch('/provisioning/instances/18/settings', {});
|
|
193
|
+
assert.equal(r.status, 400, r.raw);
|
|
194
|
+
assert.equal(r.body.error.code, 'bad_settings');
|
|
195
|
+
assert.match(r.body.error.message, /platform_visibility, joinability/);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
server.close();
|
|
199
|
+
console.log(`\nprovisioning_settings: ${passed} passed, ${failed} failed`);
|
|
200
|
+
process.exit(failed ? 1 : 0);
|