@fleetless/contracts 2.0.0 → 3.0.0

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/CHANGELOG.md CHANGED
@@ -5,6 +5,19 @@ follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the
5
5
  project uses [semantic versioning](https://semver.org/spec/v2.0.0.html) over
6
6
  the wire shapes.
7
7
 
8
+ ## [3.0.0] — 2026-09-22
9
+
10
+ The protocol window carries over unchanged: `LATEST_BRIDGE_VERSION` is still `4.0.0`, and protocol 2 still sunsets 2026-12-21. Everything below is the REST surface.
11
+
12
+ ### Added
13
+
14
+ - **Auth-config as three slices, not one document.** `putAppAuthRegistrationRequest` (`self_registration`, `allowed_domains`, `allowed_origins`), `putAppAuthUrlsRequest` (`invite_url`, `verify_url`, `reset_url`) and `putAppAuthMcpRequest` (`mcp_enabled`, `mcp_login_url`) are three `.strict()` replaces behind three new routes — `PUT /api/apps/:id/auth-config/registration`, `/urls` and `/mcp` — each merged server-side against the stored row, so a write to one slice can no longer clear a field it never showed. `GET /api/apps/:id/auth-config` is unchanged and still answers the whole document.
15
+ - **A deletion preview and a delete.** `appDeletionSummary` — six independent counts (`user_count`, `role_count`, `server_key_count`, `invitation_count`, `oidc_provider_count`, `mail_template_count`), deliberately not summed — is what `GET /api/apps/:id/deletion-preview` (`200`) answers and what the `app.deleted` audit event carries, computed by the same function so the confirmation dialog and the eventual receipt cannot quietly disagree. `DELETE /api/apps/:id` (Owner tier, `204`) runs the cascade: an app's users, roles, server keys, invitations, OIDC configuration and mail templates all go; its robots do not, since they belong to the org, not the app. **No `force` parameter** — unlike the robot deletion pair this is modelled on, an app has no open-session state to force past, and inventing one would be a guess wearing a guard's clothes.
16
+
17
+ ### Removed
18
+
19
+ - **`putAppAuthConfigRequest` and `PUT /api/apps/:id/auth-config`.** Replaced by the three slice requests and routes above — `PUT /api/apps/:id/auth-config/registration`, `PUT /api/apps/:id/auth-config/urls` and `PUT /api/apps/:id/auth-config/mcp`. This is the break that makes this release a major: a caller still sending the old whole-document body finds no route left to send it to.
20
+
8
21
  ## [2.0.0] — 2026-09-22
9
22
 
10
23
  ### Added
@@ -924,6 +924,93 @@
924
924
  }
925
925
  }
926
926
  }
927
+ },
928
+ "delete": {
929
+ "operationId": "delete_api_apps_id",
930
+ "summary": "Deletes an app and everything it produced.",
931
+ "tags": [
932
+ "apps"
933
+ ],
934
+ "security": [
935
+ {
936
+ "developerSession": []
937
+ }
938
+ ],
939
+ "parameters": [
940
+ {
941
+ "name": "id",
942
+ "in": "path",
943
+ "required": true,
944
+ "description": "The app's uuid, as returned by `POST /api/apps` or listed by `GET /api/apps`.",
945
+ "schema": {
946
+ "type": "string"
947
+ }
948
+ }
949
+ ],
950
+ "responses": {
951
+ "204": {
952
+ "description": "Success."
953
+ },
954
+ "default": {
955
+ "description": "An error envelope. Codes this route is known to answer: `unauthorized`, `token_expired`, `token_revoked`, `tier_required`, `invalid_uuid`, `not_found`.",
956
+ "content": {
957
+ "application/json": {
958
+ "schema": {
959
+ "$ref": "#/components/schemas/api-error"
960
+ }
961
+ }
962
+ }
963
+ }
964
+ },
965
+ "description": "Owner tier, and the gate runs **after** the org-scoped lookup: a developer-tier admin therefore sees the same `404` a stranger would for an app outside their org, rather than a tier refusal that confirms the id exists. A full cascade — its users, roles, server keys, invitations, OIDC provider configuration and mail templates all go, recorded once as `app.deleted` carrying an `appDeletionSummary`. Its robots are untouched: they belong to the org, not to the app. \n\n**No `force` parameter** — see `GET /api/apps/:id/deletion-preview`."
966
+ }
967
+ },
968
+ "/api/apps/{id}/deletion-preview": {
969
+ "get": {
970
+ "operationId": "get_api_apps_id_deletion_preview",
971
+ "summary": "Reports what deleting the app would destroy, without destroying it.",
972
+ "tags": [
973
+ "apps"
974
+ ],
975
+ "security": [
976
+ {
977
+ "developerSession": []
978
+ }
979
+ ],
980
+ "parameters": [
981
+ {
982
+ "name": "id",
983
+ "in": "path",
984
+ "required": true,
985
+ "description": "The app's uuid, as returned by `POST /api/apps` or listed by `GET /api/apps`.",
986
+ "schema": {
987
+ "type": "string"
988
+ }
989
+ }
990
+ ],
991
+ "responses": {
992
+ "200": {
993
+ "description": "Success.",
994
+ "content": {
995
+ "application/json": {
996
+ "schema": {
997
+ "$ref": "#/components/schemas/app-deletion-summary"
998
+ }
999
+ }
1000
+ }
1001
+ },
1002
+ "default": {
1003
+ "description": "An error envelope. Codes this route is known to answer: `unauthorized`, `token_expired`, `token_revoked`, `invalid_uuid`, `not_found`.",
1004
+ "content": {
1005
+ "application/json": {
1006
+ "schema": {
1007
+ "$ref": "#/components/schemas/api-error"
1008
+ }
1009
+ }
1010
+ }
1011
+ }
1012
+ },
1013
+ "description": "The same shape the delete's own audit event carries, computed by the same function on purpose: the confirmation dialog and the eventual receipt agree by construction, and any difference between them is real drift rather than two estimates that quietly disagree. \n\n**No `force` parameter, unlike the robot pair this is modelled on.** A robot's open live session is a single nameable state whose interruption is its own hazard, which is why that route makes the caller pass `force` explicitly. An app has no equivalent state to force past, and inventing one would be a guess wearing a guard's clothes — this preview is the guard."
927
1014
  }
928
1015
  },
929
1016
  "/api/apps/{id}/roles": {
@@ -2376,11 +2463,13 @@
2376
2463
  }
2377
2464
  }
2378
2465
  },
2379
- "description": "One row per app, created with the app and never absent — an app that has configured nothing reads back the defaults rather than a `404`. `oidc_callback_url` is in the answer and not in the request: it is minted by the cloud from its own public base URL, is the same for every app and every provider, and is the value a developer registers at their identity provider."
2380
- },
2466
+ "description": "One row per app, created with the app and never absent — an app that has configured nothing reads back the defaults rather than a `404`. `oidc_callback_url` is in the answer and not in the request: it is minted by the cloud from its own public base URL, is the same for every app and every provider, and is the value a developer registers at their identity provider. It stays read-only on every slice write below for a second reason: a writable callback URL would let a caller point the return leg of an OIDC sign-in, which carries an authorization code, at a host they own. `updated_at` is read-only for a duller one: the server stamps it on every write, and a client-supplied value would be a lie about when the row last changed."
2467
+ }
2468
+ },
2469
+ "/api/apps/{id}/auth-config/registration": {
2381
2470
  "put": {
2382
- "operationId": "put_api_apps_id_auth_config",
2383
- "summary": "Replaces the app's auth settings in one write.",
2471
+ "operationId": "put_api_apps_id_auth_config_registration",
2472
+ "summary": "Replaces who may self-register, and from where.",
2384
2473
  "tags": [
2385
2474
  "apps"
2386
2475
  ],
@@ -2422,13 +2511,129 @@
2422
2511
  }
2423
2512
  }
2424
2513
  },
2425
- "description": "**A replace, not a merge, and `.strict()`**: every field arrives or the write is refused, so a client built against an older shape cannot silently clear a setting it does not know about. `oidc_callback_url` and `updated_at` are refused in the body — a writable callback URL would let a caller point the return leg of an OIDC sign-in, which carries an authorization code, at a host they own. \n\n`400 validation_error` is where the three field rules land: a URL template must be https (or `http` on `localhost`) and carry its placeholder exactly once, an origin must be a bare scheme-host-port with no path, and a domain must be lowercase. Each refuses at configuration time because each would otherwise fail silently later — a second placeholder leaves one occurrence literal in a mailed link, an origin with a path can never equal a browser's `Origin` header, and a capitalised domain can never match a lowercased address.",
2514
+ "description": "**A replace, not a merge, and `.strict()`**: `self_registration`, `allowed_domains` and `allowed_origins` all arrive or the write is refused, so a client built against an older shape cannot silently clear a setting it does not know about. `oidc_callback_url` and `updated_at` are the server's, refused in this body as in every slice's — see `GET`'s notes for why. \n\n`400 validation_error` is where the two field rules land: an entry in `allowed_domains` must be lowercase, since a capitalised one can never match a lowercased address, and an entry in `allowed_origins` must be a bare scheme-host-port with no path, since a browser sends nothing longer in its `Origin` header. Each refuses at configuration time rather than failing silently later. \n\nThe merge is server-side against the stored row, so this write never disturbs the urls or mcp slice.",
2426
2515
  "requestBody": {
2427
2516
  "required": true,
2428
2517
  "content": {
2429
2518
  "application/json": {
2430
2519
  "schema": {
2431
- "$ref": "#/components/schemas/put-app-auth-config-request"
2520
+ "$ref": "#/components/schemas/put-app-auth-registration-request"
2521
+ }
2522
+ }
2523
+ }
2524
+ }
2525
+ }
2526
+ },
2527
+ "/api/apps/{id}/auth-config/urls": {
2528
+ "put": {
2529
+ "operationId": "put_api_apps_id_auth_config_urls",
2530
+ "summary": "Replaces the three pages Fleetless's mails point at.",
2531
+ "tags": [
2532
+ "apps"
2533
+ ],
2534
+ "security": [
2535
+ {
2536
+ "developerSession": []
2537
+ }
2538
+ ],
2539
+ "parameters": [
2540
+ {
2541
+ "name": "id",
2542
+ "in": "path",
2543
+ "required": true,
2544
+ "description": "The app's uuid, as returned by `POST /api/apps` or listed by `GET /api/apps`.",
2545
+ "schema": {
2546
+ "type": "string"
2547
+ }
2548
+ }
2549
+ ],
2550
+ "responses": {
2551
+ "200": {
2552
+ "description": "Success.",
2553
+ "content": {
2554
+ "application/json": {
2555
+ "schema": {
2556
+ "$ref": "#/components/schemas/app-auth-config"
2557
+ }
2558
+ }
2559
+ }
2560
+ },
2561
+ "default": {
2562
+ "description": "An error envelope. Codes this route is known to answer: `unauthorized`, `token_expired`, `token_revoked`, `invalid_uuid`, `validation_error`, `not_found`.",
2563
+ "content": {
2564
+ "application/json": {
2565
+ "schema": {
2566
+ "$ref": "#/components/schemas/api-error"
2567
+ }
2568
+ }
2569
+ }
2570
+ }
2571
+ },
2572
+ "description": "**A replace, not a merge, and `.strict()`**: `invite_url`, `verify_url` and `reset_url` all arrive or the write is refused, so a client built against an older shape cannot silently clear a setting it does not know about. `oidc_callback_url` and `updated_at` are the server's, refused in this body as in every slice's — see `GET`'s notes for why. \n\n`400 validation_error` is where the field rule lands: a URL template must be https (or `http` on `localhost`) and carry its placeholder exactly once — a second occurrence leaves one literal in a mailed link, refused here rather than failing silently once the mail is sent. \n\nThe merge is server-side against the stored row, so this write never disturbs the registration or mcp slice.",
2573
+ "requestBody": {
2574
+ "required": true,
2575
+ "content": {
2576
+ "application/json": {
2577
+ "schema": {
2578
+ "$ref": "#/components/schemas/put-app-auth-urls-request"
2579
+ }
2580
+ }
2581
+ }
2582
+ }
2583
+ }
2584
+ },
2585
+ "/api/apps/{id}/auth-config/mcp": {
2586
+ "put": {
2587
+ "operationId": "put_api_apps_id_auth_config_mcp",
2588
+ "summary": "Replaces the MCP switch and its login URL together.",
2589
+ "tags": [
2590
+ "apps"
2591
+ ],
2592
+ "security": [
2593
+ {
2594
+ "developerSession": []
2595
+ }
2596
+ ],
2597
+ "parameters": [
2598
+ {
2599
+ "name": "id",
2600
+ "in": "path",
2601
+ "required": true,
2602
+ "description": "The app's uuid, as returned by `POST /api/apps` or listed by `GET /api/apps`.",
2603
+ "schema": {
2604
+ "type": "string"
2605
+ }
2606
+ }
2607
+ ],
2608
+ "responses": {
2609
+ "200": {
2610
+ "description": "Success.",
2611
+ "content": {
2612
+ "application/json": {
2613
+ "schema": {
2614
+ "$ref": "#/components/schemas/app-auth-config"
2615
+ }
2616
+ }
2617
+ }
2618
+ },
2619
+ "default": {
2620
+ "description": "An error envelope. Codes this route is known to answer: `unauthorized`, `token_expired`, `token_revoked`, `invalid_uuid`, `validation_error`, `not_found`.",
2621
+ "content": {
2622
+ "application/json": {
2623
+ "schema": {
2624
+ "$ref": "#/components/schemas/api-error"
2625
+ }
2626
+ }
2627
+ }
2628
+ }
2629
+ },
2630
+ "description": "**A replace, not a merge, and `.strict()`**: `mcp_enabled` and `mcp_login_url` both arrive or the write is refused, so a client built against an older shape cannot silently clear a setting it does not know about. `oidc_callback_url` and `updated_at` are the server's, refused in this body as in every slice's — see `GET`'s notes for why. \n\n`mcp_login_url` answers to the same rule as the `urls` slice's three templates — https (or `http` on `localhost`), its placeholder exactly once — refused as `400 validation_error` rather than left to fail mid-OAuth, in a client's browser where no console screen is watching. \n\nThe merge is server-side against the stored row, so this write never disturbs the registration or urls slice.",
2631
+ "requestBody": {
2632
+ "required": true,
2633
+ "content": {
2634
+ "application/json": {
2635
+ "schema": {
2636
+ "$ref": "#/components/schemas/put-app-auth-mcp-request"
2432
2637
  }
2433
2638
  }
2434
2639
  }
@@ -8674,6 +8879,56 @@
8674
8879
  ],
8675
8880
  "additionalProperties": false
8676
8881
  },
8882
+ "app-deletion-summary": {
8883
+ "type": "object",
8884
+ "properties": {
8885
+ "user_count": {
8886
+ "type": "integer",
8887
+ "minimum": 0,
8888
+ "maximum": 9007199254740991,
8889
+ "description": "App users deleted with the app. They are the developer's own customers, not Fleetless users, and exist in no other app."
8890
+ },
8891
+ "role_count": {
8892
+ "type": "integer",
8893
+ "minimum": 0,
8894
+ "maximum": 9007199254740991,
8895
+ "description": "Roles deleted with the app, each with its per-robot slug grants."
8896
+ },
8897
+ "server_key_count": {
8898
+ "type": "integer",
8899
+ "minimum": 0,
8900
+ "maximum": 9007199254740991,
8901
+ "description": "Server keys deleted with the app. A client still holding one is refused at its next request."
8902
+ },
8903
+ "invitation_count": {
8904
+ "type": "integer",
8905
+ "minimum": 0,
8906
+ "maximum": 9007199254740991,
8907
+ "description": "Outstanding invitations — unspent and unexpired — that will never be accepted."
8908
+ },
8909
+ "oidc_provider_count": {
8910
+ "type": "integer",
8911
+ "minimum": 0,
8912
+ "maximum": 9007199254740991,
8913
+ "description": "Identity providers configured for this app. The providers themselves are somebody else's; only this app's configuration of them goes."
8914
+ },
8915
+ "mail_template_count": {
8916
+ "type": "integer",
8917
+ "minimum": 0,
8918
+ "maximum": 9007199254740991,
8919
+ "description": "Custom mail templates, of at most three. A kind using the Fleetless default text is not counted — there is no row to lose."
8920
+ }
8921
+ },
8922
+ "required": [
8923
+ "user_count",
8924
+ "role_count",
8925
+ "server_key_count",
8926
+ "invitation_count",
8927
+ "oidc_provider_count",
8928
+ "mail_template_count"
8929
+ ],
8930
+ "additionalProperties": false
8931
+ },
8677
8932
  "app-invitation": {
8678
8933
  "type": "object",
8679
8934
  "properties": {
@@ -16318,7 +16573,33 @@
16318
16573
  "message"
16319
16574
  ]
16320
16575
  },
16321
- "put-app-auth-config-request": {
16576
+ "put-app-auth-mcp-request": {
16577
+ "type": "object",
16578
+ "properties": {
16579
+ "mcp_enabled": {
16580
+ "type": "boolean",
16581
+ "description": "Whether this app serves an MCP endpoint at `/mcp/<identifier>`. Off refuses the whole OAuth surface for the app, not merely the tool calls, and is re-read on every request rather than cached off a token."
16582
+ },
16583
+ "mcp_login_url": {
16584
+ "anyOf": [
16585
+ {
16586
+ "type": "string",
16587
+ "maxLength": 500
16588
+ },
16589
+ {
16590
+ "type": "null"
16591
+ }
16592
+ ],
16593
+ "description": "The page an MCP authorization redirects to, with `{interaction}` where the interaction id goes. Not a token: the id names a pending request the server already holds, and the app authenticates the user itself before approving it."
16594
+ }
16595
+ },
16596
+ "required": [
16597
+ "mcp_enabled",
16598
+ "mcp_login_url"
16599
+ ],
16600
+ "additionalProperties": false
16601
+ },
16602
+ "put-app-auth-registration-request": {
16322
16603
  "type": "object",
16323
16604
  "properties": {
16324
16605
  "self_registration": {
@@ -16344,11 +16625,18 @@
16344
16625
  "maxLength": 200
16345
16626
  },
16346
16627
  "description": "The origins the client auth API answers CORS for, and the only origins an OIDC `redirect_uri` may name. Bare origins: scheme, host and port, with no path — a browser sends nothing longer, so an entry carrying one could never match."
16347
- },
16348
- "mcp_enabled": {
16349
- "type": "boolean",
16350
- "description": "Whether this app serves an MCP endpoint at `/mcp/<identifier>`. Off refuses the whole OAuth surface for the app, not merely the tool calls, and is re-read on every request rather than cached off a token."
16351
- },
16628
+ }
16629
+ },
16630
+ "required": [
16631
+ "self_registration",
16632
+ "allowed_domains",
16633
+ "allowed_origins"
16634
+ ],
16635
+ "additionalProperties": false
16636
+ },
16637
+ "put-app-auth-urls-request": {
16638
+ "type": "object",
16639
+ "properties": {
16352
16640
  "invite_url": {
16353
16641
  "anyOf": [
16354
16642
  {
@@ -16384,29 +16672,12 @@
16384
16672
  }
16385
16673
  ],
16386
16674
  "description": "The page that takes a new password, with `{token}` where the token goes."
16387
- },
16388
- "mcp_login_url": {
16389
- "anyOf": [
16390
- {
16391
- "type": "string",
16392
- "maxLength": 500
16393
- },
16394
- {
16395
- "type": "null"
16396
- }
16397
- ],
16398
- "description": "The page an MCP authorization redirects to, with `{interaction}` where the interaction id goes. Not a token: the id names a pending request the server already holds, and the app authenticates the user itself before approving it."
16399
16675
  }
16400
16676
  },
16401
16677
  "required": [
16402
- "self_registration",
16403
- "allowed_domains",
16404
- "allowed_origins",
16405
- "mcp_enabled",
16406
16678
  "invite_url",
16407
16679
  "verify_url",
16408
- "reset_url",
16409
- "mcp_login_url"
16680
+ "reset_url"
16410
16681
  ],
16411
16682
  "additionalProperties": false
16412
16683
  },
@@ -487,6 +487,65 @@
487
487
  "transport": "http",
488
488
  "notes": "A `default_role_id` naming a role of another app is refused: it is the one cross-app authorization check this shape can carry. Changing the robot set closes every live subscription the app's users hold, since a grant may no longer name a reachable robot."
489
489
  },
490
+ {
491
+ "method": "GET",
492
+ "path": "/api/apps/:id/deletion-preview",
493
+ "section": "apps",
494
+ "summary": "Reports what deleting the app would destroy, without destroying it.",
495
+ "audience": "developer",
496
+ "auth": "developer",
497
+ "rateLimited": false,
498
+ "ownerTier": false,
499
+ "status": 200,
500
+ "params": [
501
+ {
502
+ "name": "id",
503
+ "description": "The app's uuid, as returned by `POST /api/apps` or listed by `GET /api/apps`."
504
+ }
505
+ ],
506
+ "query": null,
507
+ "request": null,
508
+ "response": "app-deletion-summary",
509
+ "errors": [
510
+ "unauthorized",
511
+ "token_expired",
512
+ "token_revoked",
513
+ "invalid_uuid",
514
+ "not_found"
515
+ ],
516
+ "transport": "http",
517
+ "notes": "The same shape the delete's own audit event carries, computed by the same function on purpose: the confirmation dialog and the eventual receipt agree by construction, and any difference between them is real drift rather than two estimates that quietly disagree. \n\n**No `force` parameter, unlike the robot pair this is modelled on.** A robot's open live session is a single nameable state whose interruption is its own hazard, which is why that route makes the caller pass `force` explicitly. An app has no equivalent state to force past, and inventing one would be a guess wearing a guard's clothes — this preview is the guard."
518
+ },
519
+ {
520
+ "method": "DELETE",
521
+ "path": "/api/apps/:id",
522
+ "section": "apps",
523
+ "summary": "Deletes an app and everything it produced.",
524
+ "audience": "developer",
525
+ "auth": "developer",
526
+ "rateLimited": false,
527
+ "ownerTier": true,
528
+ "status": 204,
529
+ "params": [
530
+ {
531
+ "name": "id",
532
+ "description": "The app's uuid, as returned by `POST /api/apps` or listed by `GET /api/apps`."
533
+ }
534
+ ],
535
+ "query": null,
536
+ "request": null,
537
+ "response": null,
538
+ "errors": [
539
+ "unauthorized",
540
+ "token_expired",
541
+ "token_revoked",
542
+ "tier_required",
543
+ "invalid_uuid",
544
+ "not_found"
545
+ ],
546
+ "transport": "http",
547
+ "notes": "Owner tier, and the gate runs **after** the org-scoped lookup: a developer-tier admin therefore sees the same `404` a stranger would for an app outside their org, rather than a tier refusal that confirms the id exists. A full cascade — its users, roles, server keys, invitations, OIDC provider configuration and mail templates all go, recorded once as `app.deleted` carrying an `appDeletionSummary`. Its robots are untouched: they belong to the org, not to the app. \n\n**No `force` parameter** — see `GET /api/apps/:id/deletion-preview`."
548
+ },
490
549
  {
491
550
  "method": "POST",
492
551
  "path": "/api/apps/:id/roles",
@@ -1360,13 +1419,73 @@
1360
1419
  "not_found"
1361
1420
  ],
1362
1421
  "transport": "http",
1363
- "notes": "One row per app, created with the app and never absent — an app that has configured nothing reads back the defaults rather than a `404`. `oidc_callback_url` is in the answer and not in the request: it is minted by the cloud from its own public base URL, is the same for every app and every provider, and is the value a developer registers at their identity provider."
1422
+ "notes": "One row per app, created with the app and never absent — an app that has configured nothing reads back the defaults rather than a `404`. `oidc_callback_url` is in the answer and not in the request: it is minted by the cloud from its own public base URL, is the same for every app and every provider, and is the value a developer registers at their identity provider. It stays read-only on every slice write below for a second reason: a writable callback URL would let a caller point the return leg of an OIDC sign-in, which carries an authorization code, at a host they own. `updated_at` is read-only for a duller one: the server stamps it on every write, and a client-supplied value would be a lie about when the row last changed."
1364
1423
  },
1365
1424
  {
1366
1425
  "method": "PUT",
1367
- "path": "/api/apps/:id/auth-config",
1426
+ "path": "/api/apps/:id/auth-config/registration",
1427
+ "section": "apps",
1428
+ "summary": "Replaces who may self-register, and from where.",
1429
+ "audience": "developer",
1430
+ "auth": "developer",
1431
+ "rateLimited": false,
1432
+ "ownerTier": false,
1433
+ "status": 200,
1434
+ "params": [
1435
+ {
1436
+ "name": "id",
1437
+ "description": "The app's uuid, as returned by `POST /api/apps` or listed by `GET /api/apps`."
1438
+ }
1439
+ ],
1440
+ "query": null,
1441
+ "request": "put-app-auth-registration-request",
1442
+ "response": "app-auth-config",
1443
+ "errors": [
1444
+ "unauthorized",
1445
+ "token_expired",
1446
+ "token_revoked",
1447
+ "invalid_uuid",
1448
+ "validation_error",
1449
+ "not_found"
1450
+ ],
1451
+ "transport": "http",
1452
+ "notes": "**A replace, not a merge, and `.strict()`**: `self_registration`, `allowed_domains` and `allowed_origins` all arrive or the write is refused, so a client built against an older shape cannot silently clear a setting it does not know about. `oidc_callback_url` and `updated_at` are the server's, refused in this body as in every slice's — see `GET`'s notes for why. \n\n`400 validation_error` is where the two field rules land: an entry in `allowed_domains` must be lowercase, since a capitalised one can never match a lowercased address, and an entry in `allowed_origins` must be a bare scheme-host-port with no path, since a browser sends nothing longer in its `Origin` header. Each refuses at configuration time rather than failing silently later. \n\nThe merge is server-side against the stored row, so this write never disturbs the urls or mcp slice."
1453
+ },
1454
+ {
1455
+ "method": "PUT",
1456
+ "path": "/api/apps/:id/auth-config/urls",
1457
+ "section": "apps",
1458
+ "summary": "Replaces the three pages Fleetless's mails point at.",
1459
+ "audience": "developer",
1460
+ "auth": "developer",
1461
+ "rateLimited": false,
1462
+ "ownerTier": false,
1463
+ "status": 200,
1464
+ "params": [
1465
+ {
1466
+ "name": "id",
1467
+ "description": "The app's uuid, as returned by `POST /api/apps` or listed by `GET /api/apps`."
1468
+ }
1469
+ ],
1470
+ "query": null,
1471
+ "request": "put-app-auth-urls-request",
1472
+ "response": "app-auth-config",
1473
+ "errors": [
1474
+ "unauthorized",
1475
+ "token_expired",
1476
+ "token_revoked",
1477
+ "invalid_uuid",
1478
+ "validation_error",
1479
+ "not_found"
1480
+ ],
1481
+ "transport": "http",
1482
+ "notes": "**A replace, not a merge, and `.strict()`**: `invite_url`, `verify_url` and `reset_url` all arrive or the write is refused, so a client built against an older shape cannot silently clear a setting it does not know about. `oidc_callback_url` and `updated_at` are the server's, refused in this body as in every slice's — see `GET`'s notes for why. \n\n`400 validation_error` is where the field rule lands: a URL template must be https (or `http` on `localhost`) and carry its placeholder exactly once — a second occurrence leaves one literal in a mailed link, refused here rather than failing silently once the mail is sent. \n\nThe merge is server-side against the stored row, so this write never disturbs the registration or mcp slice."
1483
+ },
1484
+ {
1485
+ "method": "PUT",
1486
+ "path": "/api/apps/:id/auth-config/mcp",
1368
1487
  "section": "apps",
1369
- "summary": "Replaces the app's auth settings in one write.",
1488
+ "summary": "Replaces the MCP switch and its login URL together.",
1370
1489
  "audience": "developer",
1371
1490
  "auth": "developer",
1372
1491
  "rateLimited": false,
@@ -1379,7 +1498,7 @@
1379
1498
  }
1380
1499
  ],
1381
1500
  "query": null,
1382
- "request": "put-app-auth-config-request",
1501
+ "request": "put-app-auth-mcp-request",
1383
1502
  "response": "app-auth-config",
1384
1503
  "errors": [
1385
1504
  "unauthorized",
@@ -1390,7 +1509,7 @@
1390
1509
  "not_found"
1391
1510
  ],
1392
1511
  "transport": "http",
1393
- "notes": "**A replace, not a merge, and `.strict()`**: every field arrives or the write is refused, so a client built against an older shape cannot silently clear a setting it does not know about. `oidc_callback_url` and `updated_at` are refused in the body — a writable callback URL would let a caller point the return leg of an OIDC sign-in, which carries an authorization code, at a host they own. \n\n`400 validation_error` is where the three field rules land: a URL template must be https (or `http` on `localhost`) and carry its placeholder exactly once, an origin must be a bare scheme-host-port with no path, and a domain must be lowercase. Each refuses at configuration time because each would otherwise fail silently later — a second placeholder leaves one occurrence literal in a mailed link, an origin with a path can never equal a browser's `Origin` header, and a capitalised domain can never match a lowercased address."
1512
+ "notes": "**A replace, not a merge, and `.strict()`**: `mcp_enabled` and `mcp_login_url` both arrive or the write is refused, so a client built against an older shape cannot silently clear a setting it does not know about. `oidc_callback_url` and `updated_at` are the server's, refused in this body as in every slice's — see `GET`'s notes for why. \n\n`mcp_login_url` answers to the same rule as the `urls` slice's three templates — https (or `http` on `localhost`), its placeholder exactly once — refused as `400 validation_error` rather than left to fail mid-OAuth, in a client's browser where no console screen is watching. \n\nThe merge is server-side against the stored row, so this write never disturbs the registration or urls slice."
1394
1513
  },
1395
1514
  {
1396
1515
  "method": "GET",
@@ -0,0 +1,51 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "type": "object",
4
+ "properties": {
5
+ "user_count": {
6
+ "type": "integer",
7
+ "minimum": 0,
8
+ "maximum": 9007199254740991,
9
+ "description": "App users deleted with the app. They are the developer's own customers, not Fleetless users, and exist in no other app."
10
+ },
11
+ "role_count": {
12
+ "type": "integer",
13
+ "minimum": 0,
14
+ "maximum": 9007199254740991,
15
+ "description": "Roles deleted with the app, each with its per-robot slug grants."
16
+ },
17
+ "server_key_count": {
18
+ "type": "integer",
19
+ "minimum": 0,
20
+ "maximum": 9007199254740991,
21
+ "description": "Server keys deleted with the app. A client still holding one is refused at its next request."
22
+ },
23
+ "invitation_count": {
24
+ "type": "integer",
25
+ "minimum": 0,
26
+ "maximum": 9007199254740991,
27
+ "description": "Outstanding invitations — unspent and unexpired — that will never be accepted."
28
+ },
29
+ "oidc_provider_count": {
30
+ "type": "integer",
31
+ "minimum": 0,
32
+ "maximum": 9007199254740991,
33
+ "description": "Identity providers configured for this app. The providers themselves are somebody else's; only this app's configuration of them goes."
34
+ },
35
+ "mail_template_count": {
36
+ "type": "integer",
37
+ "minimum": 0,
38
+ "maximum": 9007199254740991,
39
+ "description": "Custom mail templates, of at most three. A kind using the Fleetless default text is not counted — there is no row to lose."
40
+ }
41
+ },
42
+ "required": [
43
+ "user_count",
44
+ "role_count",
45
+ "server_key_count",
46
+ "invitation_count",
47
+ "oidc_provider_count",
48
+ "mail_template_count"
49
+ ],
50
+ "additionalProperties": false
51
+ }
@@ -0,0 +1,27 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "type": "object",
4
+ "properties": {
5
+ "mcp_enabled": {
6
+ "type": "boolean",
7
+ "description": "Whether this app serves an MCP endpoint at `/mcp/<identifier>`. Off refuses the whole OAuth surface for the app, not merely the tool calls, and is re-read on every request rather than cached off a token."
8
+ },
9
+ "mcp_login_url": {
10
+ "anyOf": [
11
+ {
12
+ "type": "string",
13
+ "maxLength": 500
14
+ },
15
+ {
16
+ "type": "null"
17
+ }
18
+ ],
19
+ "description": "The page an MCP authorization redirects to, with `{interaction}` where the interaction id goes. Not a token: the id names a pending request the server already holds, and the app authenticates the user itself before approving it."
20
+ }
21
+ },
22
+ "required": [
23
+ "mcp_enabled",
24
+ "mcp_login_url"
25
+ ],
26
+ "additionalProperties": false
27
+ }
@@ -0,0 +1,36 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "type": "object",
4
+ "properties": {
5
+ "self_registration": {
6
+ "type": "boolean",
7
+ "description": "Whether a stranger may create an account in this app. Off refuses `POST /api/client/register` with `403 registration_closed`, and refuses an unknown identity at an OIDC callback with the same reasoning — one switch for one decision, whichever door the person arrives at."
8
+ },
9
+ "allowed_domains": {
10
+ "maxItems": 50,
11
+ "type": "array",
12
+ "items": {
13
+ "type": "string",
14
+ "minLength": 1,
15
+ "maxLength": 253,
16
+ "pattern": "^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\\.)+[a-z]{2,63}$"
17
+ },
18
+ "description": "The email domains self-registration accepts, lowercase. An empty list means no domain restriction, not \"nobody\" — the switch above is what closes the door. **An invitation always bypasses this**, by password and through a provider alike."
19
+ },
20
+ "allowed_origins": {
21
+ "maxItems": 20,
22
+ "type": "array",
23
+ "items": {
24
+ "type": "string",
25
+ "maxLength": 200
26
+ },
27
+ "description": "The origins the client auth API answers CORS for, and the only origins an OIDC `redirect_uri` may name. Bare origins: scheme, host and port, with no path — a browser sends nothing longer, so an entry carrying one could never match."
28
+ }
29
+ },
30
+ "required": [
31
+ "self_registration",
32
+ "allowed_domains",
33
+ "allowed_origins"
34
+ ],
35
+ "additionalProperties": false
36
+ }
@@ -0,0 +1,48 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "type": "object",
4
+ "properties": {
5
+ "invite_url": {
6
+ "anyOf": [
7
+ {
8
+ "type": "string",
9
+ "maxLength": 500
10
+ },
11
+ {
12
+ "type": "null"
13
+ }
14
+ ],
15
+ "description": "The page in the developer's app that accepts an invitation, with `{token}` where the token goes. `null` when unconfigured, and then an invitation still issues but `send_mail` is refused with `409 target_state_conflict` — there would be nowhere for the link to point."
16
+ },
17
+ "verify_url": {
18
+ "anyOf": [
19
+ {
20
+ "type": "string",
21
+ "maxLength": 500
22
+ },
23
+ {
24
+ "type": "null"
25
+ }
26
+ ],
27
+ "description": "The page that confirms a new address, with `{token}` where the token goes. Self-registration needs it: without a page to send people to, a registration would leave an account nobody can activate."
28
+ },
29
+ "reset_url": {
30
+ "anyOf": [
31
+ {
32
+ "type": "string",
33
+ "maxLength": 500
34
+ },
35
+ {
36
+ "type": "null"
37
+ }
38
+ ],
39
+ "description": "The page that takes a new password, with `{token}` where the token goes."
40
+ }
41
+ },
42
+ "required": [
43
+ "invite_url",
44
+ "verify_url",
45
+ "reset_url"
46
+ ],
47
+ "additionalProperties": false
48
+ }
@@ -400,23 +400,41 @@ export declare const appAuthConfig: z.ZodObject<{
400
400
  }, z.core.$strip>;
401
401
  export type AppAuthConfig = z.infer<typeof appAuthConfig>;
402
402
  /**
403
- * `PUT /api/apps/:id/auth-config` — a replace, not a merge, and `.strict()`.
403
+ * `PUT /api/apps/:id/auth-config/registration` — who may get in, and from
404
+ * where.
404
405
  *
405
- * `oidc_callback_url` and `updated_at` are omitted because both are the
406
- * server's: see the callback URL's own note for why a writable one would be a
407
- * redirect-target hole rather than a convenience.
406
+ * Three slices rather than one document, and each still a **replace** with
407
+ * every field of its slice required: three screens carving up one
408
+ * all-required request is how a field nobody's screen shows becomes a field
409
+ * somebody's save clears. The slice states its own ownership, so a new field
410
+ * lands in one schema and one screen.
411
+ *
412
+ * The merge is the server's, against the stored row — never the caller's,
413
+ * whose copy may be older than the row it would overwrite.
408
414
  */
409
- export declare const putAppAuthConfigRequest: z.ZodObject<{
415
+ export declare const putAppAuthRegistrationRequest: z.ZodObject<{
410
416
  self_registration: z.ZodBoolean;
411
417
  allowed_domains: z.ZodArray<z.ZodString>;
412
418
  allowed_origins: z.ZodArray<z.ZodString>;
413
- mcp_enabled: z.ZodBoolean;
419
+ }, z.core.$strict>;
420
+ export type PutAppAuthRegistrationRequest = z.infer<typeof putAppAuthRegistrationRequest>;
421
+ /** `PUT /api/apps/:id/auth-config/urls` — the three pages Fleetless's mails point at. */
422
+ export declare const putAppAuthUrlsRequest: z.ZodObject<{
414
423
  invite_url: z.ZodNullable<z.ZodString>;
415
424
  verify_url: z.ZodNullable<z.ZodString>;
416
425
  reset_url: z.ZodNullable<z.ZodString>;
426
+ }, z.core.$strict>;
427
+ export type PutAppAuthUrlsRequest = z.infer<typeof putAppAuthUrlsRequest>;
428
+ /**
429
+ * `PUT /api/apps/:id/auth-config/mcp` — the switch and the login URL, which
430
+ * belong together: on without a URL refuses every sign-in, in the MCP
431
+ * client's browser mid-OAuth, where no console screen ever sees it.
432
+ */
433
+ export declare const putAppAuthMcpRequest: z.ZodObject<{
434
+ mcp_enabled: z.ZodBoolean;
417
435
  mcp_login_url: z.ZodNullable<z.ZodString>;
418
436
  }, z.core.$strict>;
419
- export type PutAppAuthConfigRequest = z.infer<typeof putAppAuthConfigRequest>;
437
+ export type PutAppAuthMcpRequest = z.infer<typeof putAppAuthMcpRequest>;
420
438
  /**
421
439
  * The three mails a developer may replace with their own template.
422
440
  * Mails to *Fleetless* users — a team invitation, a console password reset —
package/dist/app-users.js CHANGED
@@ -481,14 +481,32 @@ export const appAuthConfig = z.object({
481
481
  updated_at: z.iso.datetime().meta({ description: 'When the configuration was last written, as an ISO 8601 timestamp.' }),
482
482
  });
483
483
  /**
484
- * `PUT /api/apps/:id/auth-config` — a replace, not a merge, and `.strict()`.
484
+ * `PUT /api/apps/:id/auth-config/registration` — who may get in, and from
485
+ * where.
485
486
  *
486
- * `oidc_callback_url` and `updated_at` are omitted because both are the
487
- * server's: see the callback URL's own note for why a writable one would be a
488
- * redirect-target hole rather than a convenience.
487
+ * Three slices rather than one document, and each still a **replace** with
488
+ * every field of its slice required: three screens carving up one
489
+ * all-required request is how a field nobody's screen shows becomes a field
490
+ * somebody's save clears. The slice states its own ownership, so a new field
491
+ * lands in one schema and one screen.
492
+ *
493
+ * The merge is the server's, against the stored row — never the caller's,
494
+ * whose copy may be older than the row it would overwrite.
495
+ */
496
+ export const putAppAuthRegistrationRequest = appAuthConfig
497
+ .pick({ self_registration: true, allowed_domains: true, allowed_origins: true })
498
+ .strict();
499
+ /** `PUT /api/apps/:id/auth-config/urls` — the three pages Fleetless's mails point at. */
500
+ export const putAppAuthUrlsRequest = appAuthConfig
501
+ .pick({ invite_url: true, verify_url: true, reset_url: true })
502
+ .strict();
503
+ /**
504
+ * `PUT /api/apps/:id/auth-config/mcp` — the switch and the login URL, which
505
+ * belong together: on without a URL refuses every sign-in, in the MCP
506
+ * client's browser mid-OAuth, where no console screen ever sees it.
489
507
  */
490
- export const putAppAuthConfigRequest = appAuthConfig
491
- .omit({ oidc_callback_url: true, updated_at: true })
508
+ export const putAppAuthMcpRequest = appAuthConfig
509
+ .pick({ mcp_enabled: true, mcp_login_url: true })
492
510
  .strict();
493
511
  /**
494
512
  * The three mails a developer may replace with their own template.
package/dist/apps.d.ts CHANGED
@@ -42,6 +42,28 @@ export declare const appListResponse: z.ZodObject<{
42
42
  }, z.core.$strip>>;
43
43
  }, z.core.$strip>;
44
44
  export type AppListResponse = z.infer<typeof appListResponse>;
45
+ /**
46
+ * What deleting an app would destroy — read before the irreversible click,
47
+ * and carried again by the `app.deleted` audit event.
48
+ *
49
+ * **Six numbers, never a sum**, for the reason `robotDeletionSummary` states
50
+ * at length: the console reads this aloud as one sentence, and a total would
51
+ * describe six unrelated magnitudes with one figure on the one screen whose
52
+ * entire justification is naming what cannot be undone.
53
+ *
54
+ * Robots are not here because they do not go: they belong to the
55
+ * organization, not to the app. The audit trail is not here either — a record
56
+ * of what happened outlives the thing it happened to.
57
+ */
58
+ export declare const appDeletionSummary: z.ZodObject<{
59
+ user_count: z.ZodNumber;
60
+ role_count: z.ZodNumber;
61
+ server_key_count: z.ZodNumber;
62
+ invitation_count: z.ZodNumber;
63
+ oidc_provider_count: z.ZodNumber;
64
+ mail_template_count: z.ZodNumber;
65
+ }, z.core.$strip>;
66
+ export type AppDeletionSummary = z.infer<typeof appDeletionSummary>;
45
67
  /**
46
68
  * **`robot_ids` is accepted here, and `.strict()` catches everything else.**
47
69
  * A create shape carrying `name` and `identifier` only would let zod strip an
package/dist/apps.js CHANGED
@@ -78,6 +78,39 @@ export const appListResponse = z.object({
78
78
  description: 'Every app of the caller\'s organisation, oldest first by `created_at`. The org scope is the whole filter — there is no id to narrow by and nothing to refuse.',
79
79
  }),
80
80
  });
81
+ /**
82
+ * What deleting an app would destroy — read before the irreversible click,
83
+ * and carried again by the `app.deleted` audit event.
84
+ *
85
+ * **Six numbers, never a sum**, for the reason `robotDeletionSummary` states
86
+ * at length: the console reads this aloud as one sentence, and a total would
87
+ * describe six unrelated magnitudes with one figure on the one screen whose
88
+ * entire justification is naming what cannot be undone.
89
+ *
90
+ * Robots are not here because they do not go: they belong to the
91
+ * organization, not to the app. The audit trail is not here either — a record
92
+ * of what happened outlives the thing it happened to.
93
+ */
94
+ export const appDeletionSummary = z.object({
95
+ user_count: z.number().int().nonnegative().meta({
96
+ description: 'App users deleted with the app. They are the developer\'s own customers, not Fleetless users, and exist in no other app.',
97
+ }),
98
+ role_count: z.number().int().nonnegative().meta({
99
+ description: 'Roles deleted with the app, each with its per-robot slug grants.',
100
+ }),
101
+ server_key_count: z.number().int().nonnegative().meta({
102
+ description: 'Server keys deleted with the app. A client still holding one is refused at its next request.',
103
+ }),
104
+ invitation_count: z.number().int().nonnegative().meta({
105
+ description: 'Outstanding invitations — unspent and unexpired — that will never be accepted.',
106
+ }),
107
+ oidc_provider_count: z.number().int().nonnegative().meta({
108
+ description: 'Identity providers configured for this app. The providers themselves are somebody else\'s; only this app\'s configuration of them goes.',
109
+ }),
110
+ mail_template_count: z.number().int().nonnegative().meta({
111
+ description: 'Custom mail templates, of at most three. A kind using the Fleetless default text is not counted — there is no row to lose.',
112
+ }),
113
+ });
81
114
  /**
82
115
  * **`robot_ids` is accepted here, and `.strict()` catches everything else.**
83
116
  * A create shape carrying `name` and `identifier` only would let zod strip an
package/dist/index.d.ts CHANGED
@@ -31,14 +31,14 @@ export { clientAuth, authOk, authError, clientInvoke, clientCancel, clientPublis
31
31
  export type { ClientAuth, AuthOk, AuthError, ClientInvoke, ClientCancel, ClientPublish, CommandResult, ErrorFrame, ClientSubscribe, ClientUnsubscribe, SubscribeError, DatapointEvent, ResourceHealthEvent, ResourceHealthCleared, LiveSessionEndReason, LiveSessionEvent, OrgEventKind, OrgEventSeverity, OrgEvent, OrgEventSubscribe, OrgEventUnsubscribe, OrgEventReplay, OrgEventDropped, } from './realtime.js';
32
32
  export { password, org, patchOrgResponse, sessionTokens, refreshRequest, signUpRequest, signUpResponse, waitlistRequest, developerLoginRequest, USER_DISPLAY_NAME_MAX, orgAdminTier, fleetlessUser, fleetlessUserListResponse, createTeamInviteRequest, teamInvite, pendingTeamInvite, pendingTeamInviteListResponse, acceptTeamInviteRequest, patchFleetlessUserRequest, tierChangeRequest, mailStatus, tierRequiredDetails, passwordChangeRequest, passwordResetRequest, passwordResetConfirm, idpIssuer, authMeResponse, patchOrgRequest, patchAuthMeRequest, } from './identity.js';
33
33
  export type { Org, PatchOrgResponse, SessionTokens, RefreshRequest, SignUpRequest, SignUpResponse, WaitlistRequest, DeveloperLoginRequest, OrgAdminTier, FleetlessUser, FleetlessUserListResponse, CreateTeamInviteRequest, TeamInvite, PendingTeamInvite, PendingTeamInviteListResponse, AcceptTeamInviteRequest, PatchFleetlessUserRequest, TierChangeRequest, MailStatus, TierRequiredDetails, PasswordChangeRequest, PasswordResetRequest, PasswordResetConfirm, IdpIssuer, AuthMeResponse, PatchOrgRequest, PatchAuthMeRequest, } from './identity.js';
34
- export { appIdentifier, app, appListResponse, createAppRequest, updateAppRequest, serverKeyToken, serverKey, serverKeyListResponse, createServerKeyResponse, role, roleListResponse, rolePermissions, } from './apps.js';
35
- export type { App, AppListResponse, CreateAppRequest, UpdateAppRequest, ServerKey, ServerKeyListResponse, CreateServerKeyResponse, Role, RoleListResponse, RolePermissions, } from './apps.js';
34
+ export { appIdentifier, app, appListResponse, appDeletionSummary, createAppRequest, updateAppRequest, serverKeyToken, serverKey, serverKeyListResponse, createServerKeyResponse, role, roleListResponse, rolePermissions, } from './apps.js';
35
+ export type { App, AppListResponse, AppDeletionSummary, CreateAppRequest, UpdateAppRequest, ServerKey, ServerKeyListResponse, CreateServerKeyResponse, Role, RoleListResponse, RolePermissions, } from './apps.js';
36
36
  export { clientLoginRequest, clientRefreshRequest, clientLogoutRequest, clientRegisterRequest, clientVerifyEmailRequest, clientResendVerificationRequest, clientPasswordResetRequest, clientPasswordResetConfirmRequest, clientAcceptInvitationRequest, CLIENT_OIDC_CALLBACK_PATH, clientProviderListQuery, clientProviderListResponse, clientOidcStartQuery, clientOidcCallbackQuery, clientOidcExchangeRequest, clientOidcErrorCode, clientMcpInteraction, clientMcpInteractionDecisionResponse, mcpConsentGrant, mcpConsentGrantListResponse, clientIdentity, } from './client-auth.js';
37
37
  export type { ClientLoginRequest, ClientRefreshRequest, ClientLogoutRequest, ClientRegisterRequest, ClientVerifyEmailRequest, ClientResendVerificationRequest, ClientPasswordResetRequest, ClientPasswordResetConfirmRequest, ClientAcceptInvitationRequest, ClientProviderListQuery, ClientProviderListResponse, ClientOidcStartQuery, ClientOidcCallbackQuery, ClientOidcExchangeRequest, ClientOidcErrorCode, ClientMcpInteraction, ClientMcpInteractionDecisionResponse, McpConsentGrant, McpConsentGrantListResponse, ClientIdentity, } from './client-auth.js';
38
38
  export { clientRobotListItem, clientRobotListResponse } from './client-robots.js';
39
39
  export type { ClientRobotListItem, ClientRobotListResponse } from './client-robots.js';
40
- export { APP_USER_DISPLAY_NAME_MAX, APP_URL_PLACEHOLDERS, MAIL_TEMPLATE_VARIABLES, DEFAULT_MAIL_TEMPLATES, providerSlug, appUserStatus, appUser, appUserListResponse, createAppUserRequest, patchAppUserRequest, createAppInvitationRequest, appInvitation, pendingAppInvitation, appInvitationListResponse, appOidcProvider, appOidcProviderListResponse, createAppOidcProviderRequest, patchAppOidcProviderRequest, appUrlTemplate, allowedOrigin, emailDomain, appAuthConfig, putAppAuthConfigRequest, mailTemplateKind, appMailTemplate, appMailTemplateListResponse, putAppMailTemplateRequest, mailTemplatePreviewRequest, mailTemplatePreviewResponse, mailTemplateProblemDetails, mailOutcome, } from './app-users.js';
41
- export type { AppUserStatus, AppUser, AppUserListResponse, CreateAppUserRequest, PatchAppUserRequest, CreateAppInvitationRequest, AppInvitation, PendingAppInvitation, AppInvitationListResponse, AppOidcProvider, AppOidcProviderListResponse, CreateAppOidcProviderRequest, PatchAppOidcProviderRequest, AppAuthConfig, PutAppAuthConfigRequest, MailTemplateKind, AppMailTemplate, AppMailTemplateListResponse, PutAppMailTemplateRequest, MailTemplatePreviewRequest, MailTemplatePreviewResponse, MailTemplateProblemDetails, MailOutcome, } from './app-users.js';
40
+ export { APP_USER_DISPLAY_NAME_MAX, APP_URL_PLACEHOLDERS, MAIL_TEMPLATE_VARIABLES, DEFAULT_MAIL_TEMPLATES, providerSlug, appUserStatus, appUser, appUserListResponse, createAppUserRequest, patchAppUserRequest, createAppInvitationRequest, appInvitation, pendingAppInvitation, appInvitationListResponse, appOidcProvider, appOidcProviderListResponse, createAppOidcProviderRequest, patchAppOidcProviderRequest, appUrlTemplate, allowedOrigin, emailDomain, appAuthConfig, putAppAuthRegistrationRequest, putAppAuthUrlsRequest, putAppAuthMcpRequest, mailTemplateKind, appMailTemplate, appMailTemplateListResponse, putAppMailTemplateRequest, mailTemplatePreviewRequest, mailTemplatePreviewResponse, mailTemplateProblemDetails, mailOutcome, } from './app-users.js';
41
+ export type { AppUserStatus, AppUser, AppUserListResponse, CreateAppUserRequest, PatchAppUserRequest, CreateAppInvitationRequest, AppInvitation, PendingAppInvitation, AppInvitationListResponse, AppOidcProvider, AppOidcProviderListResponse, CreateAppOidcProviderRequest, PatchAppOidcProviderRequest, AppAuthConfig, PutAppAuthRegistrationRequest, PutAppAuthUrlsRequest, PutAppAuthMcpRequest, MailTemplateKind, AppMailTemplate, AppMailTemplateListResponse, PutAppMailTemplateRequest, MailTemplatePreviewRequest, MailTemplatePreviewResponse, MailTemplateProblemDetails, MailOutcome, } from './app-users.js';
42
42
  export { assetKind, URDF_ASSET_NAME, asset, urdfCompleteness, assetListResponse, assetsClearResponse, missingAssetQuery, assetSyncRequest, assetSyncResponse, assetSyncState, assetSyncStatus, assetFailure, assetFailureKind, assetStoreRefusedDetails, assetSyncBusyDetails, ROBOT_ASSET_STORE_BYTES, } from './assets.js';
43
43
  export type { AssetKind, Asset, UrdfCompleteness, AssetListResponse, AssetsClearResponse, MissingAssetQuery, AssetSyncRequest, AssetSyncResponse, AssetSyncState, AssetSyncStatus, AssetFailure, AssetFailureKind, AssetStoreRefusedDetails, AssetSyncBusyDetails, } from './assets.js';
44
44
  export { auditActor, auditEvent, auditQuery, auditListResponse, AUDIT_CSV_COLUMNS, AUDIT_RETENTION_DAYS } from './audit.js';
package/dist/index.js CHANGED
@@ -38,11 +38,11 @@ USER_DISPLAY_NAME_MAX, orgAdminTier, fleetlessUser, fleetlessUserListResponse, c
38
38
  mailStatus, tierRequiredDetails, passwordChangeRequest, passwordResetRequest, passwordResetConfirm, idpIssuer,
39
39
  // auth/me, org and member patches.
40
40
  authMeResponse, patchOrgRequest, patchAuthMeRequest, } from './identity.js';
41
- export { appIdentifier, app, appListResponse, createAppRequest, updateAppRequest, serverKeyToken, serverKey, serverKeyListResponse, createServerKeyResponse, role, roleListResponse, rolePermissions, } from './apps.js';
41
+ export { appIdentifier, app, appListResponse, appDeletionSummary, createAppRequest, updateAppRequest, serverKeyToken, serverKey, serverKeyListResponse, createServerKeyResponse, role, roleListResponse, rolePermissions, } from './apps.js';
42
42
  export { clientLoginRequest, clientRefreshRequest, clientLogoutRequest, clientRegisterRequest, clientVerifyEmailRequest, clientResendVerificationRequest, clientPasswordResetRequest, clientPasswordResetConfirmRequest, clientAcceptInvitationRequest, CLIENT_OIDC_CALLBACK_PATH, clientProviderListQuery, clientProviderListResponse, clientOidcStartQuery, clientOidcCallbackQuery, clientOidcExchangeRequest, clientOidcErrorCode, clientMcpInteraction, clientMcpInteractionDecisionResponse, mcpConsentGrant, mcpConsentGrantListResponse, clientIdentity, } from './client-auth.js';
43
43
  export { clientRobotListItem, clientRobotListResponse } from './client-robots.js';
44
44
  // The per-app identity space.
45
- export { APP_USER_DISPLAY_NAME_MAX, APP_URL_PLACEHOLDERS, MAIL_TEMPLATE_VARIABLES, DEFAULT_MAIL_TEMPLATES, providerSlug, appUserStatus, appUser, appUserListResponse, createAppUserRequest, patchAppUserRequest, createAppInvitationRequest, appInvitation, pendingAppInvitation, appInvitationListResponse, appOidcProvider, appOidcProviderListResponse, createAppOidcProviderRequest, patchAppOidcProviderRequest, appUrlTemplate, allowedOrigin, emailDomain, appAuthConfig, putAppAuthConfigRequest, mailTemplateKind, appMailTemplate, appMailTemplateListResponse, putAppMailTemplateRequest, mailTemplatePreviewRequest, mailTemplatePreviewResponse, mailTemplateProblemDetails, mailOutcome, } from './app-users.js';
45
+ export { APP_USER_DISPLAY_NAME_MAX, APP_URL_PLACEHOLDERS, MAIL_TEMPLATE_VARIABLES, DEFAULT_MAIL_TEMPLATES, providerSlug, appUserStatus, appUser, appUserListResponse, createAppUserRequest, patchAppUserRequest, createAppInvitationRequest, appInvitation, pendingAppInvitation, appInvitationListResponse, appOidcProvider, appOidcProviderListResponse, createAppOidcProviderRequest, patchAppOidcProviderRequest, appUrlTemplate, allowedOrigin, emailDomain, appAuthConfig, putAppAuthRegistrationRequest, putAppAuthUrlsRequest, putAppAuthMcpRequest, mailTemplateKind, appMailTemplate, appMailTemplateListResponse, putAppMailTemplateRequest, mailTemplatePreviewRequest, mailTemplatePreviewResponse, mailTemplateProblemDetails, mailOutcome, } from './app-users.js';
46
46
  export { assetKind, URDF_ASSET_NAME, asset, urdfCompleteness, assetListResponse, assetsClearResponse, missingAssetQuery, assetSyncRequest, assetSyncResponse, assetSyncState, assetSyncStatus, assetFailure, assetFailureKind, assetStoreRefusedDetails, assetSyncBusyDetails, ROBOT_ASSET_STORE_BYTES, } from './assets.js';
47
47
  export { auditActor, auditEvent, auditQuery, auditListResponse, AUDIT_CSV_COLUMNS, AUDIT_RETENTION_DAYS } from './audit.js';
48
48
  export { alertRowCondition, alertSeverity, alertState, datapointAlertRow, alertListResponse, orgFiringAlertsResponse, orgAlertsQuery, datapointDisplay, putDatapointDisplayRequest, } from './alerts.js';
package/dist/rest.d.ts CHANGED
@@ -1440,6 +1440,8 @@ export type HistoryResponse = z.infer<typeof historyResponse>;
1440
1440
  * |---|---|---|
1441
1441
  * | `DELETE /api/robots/:id` | — | `204`. `?force=true` to proceed while a live session is open; without it, `409 robot_in_use` |
1442
1442
  * | `GET /api/robots/:id/deletion-preview` | — | `robotDeletionSummary` — the same shape the audit event carries |
1443
+ * | `DELETE /api/apps/:id` | — | `204`. No `force` parameter — an app has no open-session hazard to force past, so the preview is the guard |
1444
+ * | `GET /api/apps/:id/deletion-preview` | — | `appDeletionSummary` — the same shape the audit event carries |
1443
1445
  * | `GET /api/org/health` | — | `resourceHealthListResponse`; `?robot_id=` narrows it to one robot |
1444
1446
  *
1445
1447
  * Plus `resourceHealthEvent`, pushed on the **developer** realtime socket
package/dist/rest.js CHANGED
@@ -1228,6 +1228,8 @@ export const historyResponse = z.union([historySamplesResponse, historyBucketsRe
1228
1228
  * |---|---|---|
1229
1229
  * | `DELETE /api/robots/:id` | — | `204`. `?force=true` to proceed while a live session is open; without it, `409 robot_in_use` |
1230
1230
  * | `GET /api/robots/:id/deletion-preview` | — | `robotDeletionSummary` — the same shape the audit event carries |
1231
+ * | `DELETE /api/apps/:id` | — | `204`. No `force` parameter — an app has no open-session hazard to force past, so the preview is the guard |
1232
+ * | `GET /api/apps/:id/deletion-preview` | — | `appDeletionSummary` — the same shape the audit event carries |
1231
1233
  * | `GET /api/org/health` | — | `resourceHealthListResponse`; `?robot_id=` narrows it to one robot |
1232
1234
  *
1233
1235
  * Plus `resourceHealthEvent`, pushed on the **developer** realtime socket
package/dist/routes.js CHANGED
@@ -1,11 +1,11 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
- import { appListResponse, createAppRequest, createServerKeyResponse, app as appSchema, role, roleListResponse, rolePermissions, serverKeyListResponse, updateAppRequest, } from './apps.js';
2
+ import { appListResponse, appDeletionSummary, createAppRequest, createServerKeyResponse, app as appSchema, role, roleListResponse, rolePermissions, serverKeyListResponse, updateAppRequest, } from './apps.js';
3
3
  import { alertListResponse, orgAlertsQuery, orgFiringAlertsResponse } from './alerts.js';
4
4
  import { asset, assetListResponse, assetsClearResponse, assetSyncRequest, assetSyncResponse, assetSyncStatus, missingAssetQuery } from './assets.js';
5
5
  import { auditListResponse, auditQuery } from './audit.js';
6
6
  import { CLIENT_OIDC_CALLBACK_PATH, clientAcceptInvitationRequest, clientIdentity, clientLoginRequest, clientLogoutRequest, clientMcpInteraction, clientMcpInteractionDecisionResponse, clientOidcCallbackQuery, clientOidcExchangeRequest, clientOidcStartQuery, clientPasswordResetConfirmRequest, clientPasswordResetRequest, clientProviderListQuery, clientProviderListResponse, clientRefreshRequest, clientRegisterRequest, clientResendVerificationRequest, clientVerifyEmailRequest, mcpConsentGrantListResponse, } from './client-auth.js';
7
7
  import { clientRobotListResponse } from './client-robots.js';
8
- import { appAuthConfig, appInvitation, appInvitationListResponse, appMailTemplate, appMailTemplateListResponse, appOidcProvider, appOidcProviderListResponse, appUser, appUserListResponse, createAppInvitationRequest, createAppOidcProviderRequest, createAppUserRequest, mailOutcome, mailTemplatePreviewRequest, mailTemplatePreviewResponse, patchAppOidcProviderRequest, patchAppUserRequest, putAppAuthConfigRequest, putAppMailTemplateRequest, } from './app-users.js';
8
+ import { appAuthConfig, appInvitation, appInvitationListResponse, appMailTemplate, appMailTemplateListResponse, appOidcProvider, appOidcProviderListResponse, appUser, appUserListResponse, createAppInvitationRequest, createAppOidcProviderRequest, createAppUserRequest, mailOutcome, mailTemplatePreviewRequest, mailTemplatePreviewResponse, patchAppOidcProviderRequest, patchAppUserRequest, putAppAuthMcpRequest, putAppAuthRegistrationRequest, putAppAuthUrlsRequest, putAppMailTemplateRequest, } from './app-users.js';
9
9
  import { acceptTeamInviteRequest, authMeResponse, createTeamInviteRequest, fleetlessUser, fleetlessUserListResponse, passwordChangeRequest, passwordResetConfirm, passwordResetRequest, patchAuthMeRequest, patchFleetlessUserRequest, patchOrgRequest, patchOrgResponse, pendingTeamInviteListResponse, refreshRequest, sessionTokens, signUpRequest, signUpResponse, teamInvite, tierChangeRequest, waitlistRequest, } from './identity.js';
10
10
  import { jobRunListResponse, jobRunQuery, jobRunSummary, jobRunSummaryQuery } from './jobs.js';
11
11
  import { MCP_APP_PATHS, mcpRobotDatasheet, mcpRolePreviewResponse } from './mcp.js';
@@ -295,6 +295,32 @@ export const ROUTES = [
295
295
  notes: 'A `default_role_id` naming a role of another app is refused: it is the one cross-app authorization check this shape can carry. ' +
296
296
  'Changing the robot set closes every live subscription the app\'s users hold, since a grant may no longer name a reachable robot.',
297
297
  },
298
+ {
299
+ method: 'GET', path: '/api/apps/:id/deletion-preview', section: 'apps',
300
+ summary: 'Reports what deleting the app would destroy, without destroying it.',
301
+ audience: 'developer', auth: 'developer', rateLimited: false, ownerTier: false, status: 200,
302
+ params: [{ name: 'id', description: 'The app\'s uuid, as returned by `POST /api/apps` or listed by `GET /api/apps`.' }],
303
+ query: null, request: null, response: appDeletionSummary,
304
+ errors: [...DEVELOPER_GUARD, 'invalid_uuid', 'not_found'], transport: 'http',
305
+ notes: 'The same shape the delete\'s own audit event carries, computed by the same function on purpose: the confirmation dialog and the eventual ' +
306
+ 'receipt agree by construction, and any difference between them is real drift rather than two estimates that quietly disagree. \n\n' +
307
+ '**No `force` parameter, unlike the robot pair this is modelled on.** A robot\'s open live session is a single nameable state whose ' +
308
+ 'interruption is its own hazard, which is why that route makes the caller pass `force` explicitly. An app has no equivalent state to ' +
309
+ 'force past, and inventing one would be a guess wearing a guard\'s clothes — this preview is the guard.',
310
+ },
311
+ {
312
+ method: 'DELETE', path: '/api/apps/:id', section: 'apps',
313
+ summary: 'Deletes an app and everything it produced.',
314
+ audience: 'developer', auth: 'developer', rateLimited: false, ownerTier: true, status: 204,
315
+ params: [{ name: 'id', description: 'The app\'s uuid, as returned by `POST /api/apps` or listed by `GET /api/apps`.' }],
316
+ query: null, request: null, response: null,
317
+ errors: [...DEVELOPER_GUARD, 'tier_required', 'invalid_uuid', 'not_found'], transport: 'http',
318
+ notes: 'Owner tier, and the gate runs **after** the org-scoped lookup: a developer-tier admin therefore sees the same `404` a stranger would ' +
319
+ 'for an app outside their org, rather than a tier refusal that confirms the id exists. A full cascade — its users, roles, server keys, ' +
320
+ 'invitations, OIDC provider configuration and mail templates all go, recorded once as `app.deleted` carrying an `appDeletionSummary`. ' +
321
+ 'Its robots are untouched: they belong to the org, not to the app. \n\n**No `force` parameter** — see ' +
322
+ '`GET /api/apps/:id/deletion-preview`.',
323
+ },
298
324
  {
299
325
  method: 'POST', path: '/api/apps/:id/roles', section: 'apps',
300
326
  summary: 'Creates a custom role on the app.',
@@ -681,22 +707,55 @@ export const ROUTES = [
681
707
  errors: [...DEVELOPER_GUARD, 'invalid_uuid', 'not_found'], transport: 'http',
682
708
  notes: 'One row per app, created with the app and never absent — an app that has configured nothing reads back the defaults rather than a ' +
683
709
  '`404`. `oidc_callback_url` is in the answer and not in the request: it is minted by the cloud from its own public base URL, is the same ' +
684
- 'for every app and every provider, and is the value a developer registers at their identity provider.',
710
+ 'for every app and every provider, and is the value a developer registers at their identity provider. It stays read-only on every slice ' +
711
+ 'write below for a second reason: a writable callback URL would let a caller point the return leg of an OIDC sign-in, which carries an ' +
712
+ 'authorization code, at a host they own. `updated_at` is read-only for a duller one: the server stamps it on every write, and a ' +
713
+ 'client-supplied value would be a lie about when the row last changed.',
714
+ },
715
+ {
716
+ method: 'PUT', path: '/api/apps/:id/auth-config/registration', section: 'apps',
717
+ summary: 'Replaces who may self-register, and from where.',
718
+ audience: 'developer', auth: 'developer', rateLimited: false, ownerTier: false, status: 200,
719
+ params: [{ name: 'id', description: 'The app\'s uuid, as returned by `POST /api/apps` or listed by `GET /api/apps`.' }],
720
+ query: null, request: putAppAuthRegistrationRequest, response: appAuthConfig,
721
+ errors: [...DEVELOPER_GUARD, 'invalid_uuid', 'validation_error', 'not_found'], transport: 'http',
722
+ notes: '**A replace, not a merge, and `.strict()`**: `self_registration`, `allowed_domains` and `allowed_origins` all arrive or the write is ' +
723
+ 'refused, so a client built against an older shape cannot silently clear a setting it does not know about. `oidc_callback_url` and ' +
724
+ '`updated_at` are the server\'s, refused in this body as in every slice\'s — see `GET`\'s notes for why. ' +
725
+ '\n\n`400 validation_error` is where the two field rules land: an entry in `allowed_domains` must be lowercase, since a capitalised one ' +
726
+ 'can never match a lowercased address, and an entry in `allowed_origins` must be a bare scheme-host-port with no path, since a browser ' +
727
+ 'sends nothing longer in its `Origin` header. Each refuses at configuration time rather than failing silently later. ' +
728
+ '\n\nThe merge is server-side against the stored row, so this write never disturbs the urls or mcp slice.',
729
+ },
730
+ {
731
+ method: 'PUT', path: '/api/apps/:id/auth-config/urls', section: 'apps',
732
+ summary: "Replaces the three pages Fleetless's mails point at.",
733
+ audience: 'developer', auth: 'developer', rateLimited: false, ownerTier: false, status: 200,
734
+ params: [{ name: 'id', description: 'The app\'s uuid, as returned by `POST /api/apps` or listed by `GET /api/apps`.' }],
735
+ query: null, request: putAppAuthUrlsRequest, response: appAuthConfig,
736
+ errors: [...DEVELOPER_GUARD, 'invalid_uuid', 'validation_error', 'not_found'], transport: 'http',
737
+ notes: '**A replace, not a merge, and `.strict()`**: `invite_url`, `verify_url` and `reset_url` all arrive or the write is refused, so a ' +
738
+ 'client built against an older shape cannot silently clear a setting it does not know about. `oidc_callback_url` and `updated_at` are ' +
739
+ 'the server\'s, refused in this body as in every slice\'s — see `GET`\'s notes for why. ' +
740
+ '\n\n`400 validation_error` is where the field rule lands: a URL template must be https (or `http` on `localhost`) and carry its ' +
741
+ 'placeholder exactly once — a second occurrence leaves one literal in a mailed link, refused here rather than failing silently once ' +
742
+ 'the mail is sent. ' +
743
+ '\n\nThe merge is server-side against the stored row, so this write never disturbs the registration or mcp slice.',
685
744
  },
686
745
  {
687
- method: 'PUT', path: '/api/apps/:id/auth-config', section: 'apps',
688
- summary: "Replaces the app's auth settings in one write.",
746
+ method: 'PUT', path: '/api/apps/:id/auth-config/mcp', section: 'apps',
747
+ summary: 'Replaces the MCP switch and its login URL together.',
689
748
  audience: 'developer', auth: 'developer', rateLimited: false, ownerTier: false, status: 200,
690
749
  params: [{ name: 'id', description: 'The app\'s uuid, as returned by `POST /api/apps` or listed by `GET /api/apps`.' }],
691
- query: null, request: putAppAuthConfigRequest, response: appAuthConfig,
750
+ query: null, request: putAppAuthMcpRequest, response: appAuthConfig,
692
751
  errors: [...DEVELOPER_GUARD, 'invalid_uuid', 'validation_error', 'not_found'], transport: 'http',
693
- notes: '**A replace, not a merge, and `.strict()`**: every field arrives or the write is refused, so a client built against an older shape ' +
694
- 'cannot silently clear a setting it does not know about. `oidc_callback_url` and `updated_at` are refused in the body — a writable ' +
695
- 'callback URL would let a caller point the return leg of an OIDC sign-in, which carries an authorization code, at a host they own. ' +
696
- '\n\n`400 validation_error` is where the three field rules land: a URL template must be https (or `http` on `localhost`) and carry its ' +
697
- 'placeholder exactly once, an origin must be a bare scheme-host-port with no path, and a domain must be lowercase. Each refuses at ' +
698
- 'configuration time because each would otherwise fail silently later — a second placeholder leaves one occurrence literal in a mailed ' +
699
- 'link, an origin with a path can never equal a browser\'s `Origin` header, and a capitalised domain can never match a lowercased address.',
752
+ notes: '**A replace, not a merge, and `.strict()`**: `mcp_enabled` and `mcp_login_url` both arrive or the write is refused, so a client built ' +
753
+ 'against an older shape cannot silently clear a setting it does not know about. `oidc_callback_url` and `updated_at` are the server\'s, ' +
754
+ 'refused in this body as in every slice\'s — see `GET`\'s notes for why. ' +
755
+ '\n\n`mcp_login_url` answers to the same rule as the `urls` slice\'s three templates — https (or `http` on `localhost`), its placeholder ' +
756
+ 'exactly once — refused as `400 validation_error` rather than left to fail mid-OAuth, in a client\'s browser where no console screen ' +
757
+ 'is watching. ' +
758
+ '\n\nThe merge is server-side against the stored row, so this write never disturbs the registration or urls slice.',
700
759
  },
701
760
  {
702
761
  method: 'GET', path: '/api/apps/:id/mail-templates', section: 'apps',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fleetless/contracts",
3
- "version": "2.0.0",
3
+ "version": "3.0.0",
4
4
  "description": "Fleetless wire contracts: the bridge-cloud protocol, the REST API schemas and the error codes, as zod schemas with generated JSON Schema and OpenAPI artifacts.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Dehne Robotik GmbH",
@@ -1,93 +0,0 @@
1
- {
2
- "$schema": "https://json-schema.org/draft/2020-12/schema",
3
- "type": "object",
4
- "properties": {
5
- "self_registration": {
6
- "type": "boolean",
7
- "description": "Whether a stranger may create an account in this app. Off refuses `POST /api/client/register` with `403 registration_closed`, and refuses an unknown identity at an OIDC callback with the same reasoning — one switch for one decision, whichever door the person arrives at."
8
- },
9
- "allowed_domains": {
10
- "maxItems": 50,
11
- "type": "array",
12
- "items": {
13
- "type": "string",
14
- "minLength": 1,
15
- "maxLength": 253,
16
- "pattern": "^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\\.)+[a-z]{2,63}$"
17
- },
18
- "description": "The email domains self-registration accepts, lowercase. An empty list means no domain restriction, not \"nobody\" — the switch above is what closes the door. **An invitation always bypasses this**, by password and through a provider alike."
19
- },
20
- "allowed_origins": {
21
- "maxItems": 20,
22
- "type": "array",
23
- "items": {
24
- "type": "string",
25
- "maxLength": 200
26
- },
27
- "description": "The origins the client auth API answers CORS for, and the only origins an OIDC `redirect_uri` may name. Bare origins: scheme, host and port, with no path — a browser sends nothing longer, so an entry carrying one could never match."
28
- },
29
- "mcp_enabled": {
30
- "type": "boolean",
31
- "description": "Whether this app serves an MCP endpoint at `/mcp/<identifier>`. Off refuses the whole OAuth surface for the app, not merely the tool calls, and is re-read on every request rather than cached off a token."
32
- },
33
- "invite_url": {
34
- "anyOf": [
35
- {
36
- "type": "string",
37
- "maxLength": 500
38
- },
39
- {
40
- "type": "null"
41
- }
42
- ],
43
- "description": "The page in the developer's app that accepts an invitation, with `{token}` where the token goes. `null` when unconfigured, and then an invitation still issues but `send_mail` is refused with `409 target_state_conflict` — there would be nowhere for the link to point."
44
- },
45
- "verify_url": {
46
- "anyOf": [
47
- {
48
- "type": "string",
49
- "maxLength": 500
50
- },
51
- {
52
- "type": "null"
53
- }
54
- ],
55
- "description": "The page that confirms a new address, with `{token}` where the token goes. Self-registration needs it: without a page to send people to, a registration would leave an account nobody can activate."
56
- },
57
- "reset_url": {
58
- "anyOf": [
59
- {
60
- "type": "string",
61
- "maxLength": 500
62
- },
63
- {
64
- "type": "null"
65
- }
66
- ],
67
- "description": "The page that takes a new password, with `{token}` where the token goes."
68
- },
69
- "mcp_login_url": {
70
- "anyOf": [
71
- {
72
- "type": "string",
73
- "maxLength": 500
74
- },
75
- {
76
- "type": "null"
77
- }
78
- ],
79
- "description": "The page an MCP authorization redirects to, with `{interaction}` where the interaction id goes. Not a token: the id names a pending request the server already holds, and the app authenticates the user itself before approving it."
80
- }
81
- },
82
- "required": [
83
- "self_registration",
84
- "allowed_domains",
85
- "allowed_origins",
86
- "mcp_enabled",
87
- "invite_url",
88
- "verify_url",
89
- "reset_url",
90
- "mcp_login_url"
91
- ],
92
- "additionalProperties": false
93
- }