@fleetless/contracts 2.0.0 → 4.0.0-next.1

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.
@@ -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
+ }
@@ -13,6 +13,23 @@
13
13
  "format": "uuid",
14
14
  "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
15
15
  }
16
+ },
17
+ "error": {
18
+ "type": "object",
19
+ "properties": {
20
+ "code": {
21
+ "type": "string",
22
+ "minLength": 1
23
+ },
24
+ "message": {
25
+ "type": "string",
26
+ "minLength": 1
27
+ }
28
+ },
29
+ "required": [
30
+ "code",
31
+ "message"
32
+ ]
16
33
  }
17
34
  },
18
35
  "required": [
@@ -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
+ }
@@ -13,6 +13,24 @@
13
13
  "format": "uuid",
14
14
  "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
15
15
  }
16
+ },
17
+ "error": {
18
+ "type": "object",
19
+ "properties": {
20
+ "code": {
21
+ "type": "string",
22
+ "minLength": 1
23
+ },
24
+ "message": {
25
+ "type": "string",
26
+ "minLength": 1
27
+ }
28
+ },
29
+ "required": [
30
+ "code",
31
+ "message"
32
+ ],
33
+ "additionalProperties": false
16
34
  }
17
35
  },
18
36
  "required": [
@@ -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/errors.d.ts CHANGED
@@ -49,5 +49,5 @@ export type ParameterInvalidDetails = z.infer<typeof parameterInvalidDetails>;
49
49
  * list is the shared vocabulary, not a closed set, so a new refusal never
50
50
  * needs a contracts release before it can be reported honestly.
51
51
  */
52
- export declare const ERROR_CODES: readonly ["not_found", "validation_error", "bad_request", "unknown_datapoint", "invalid_token", "protocol_mismatch", "invalid_frame", "duplicate_slug", "reserved_slug", "unknown_slug", "unknown_field_path", "unknown_type", "unknown_topic", "invalid_rate", "invalid_range", "config_conflict", "no_data", "robot_offline", "bridge_timeout", "unauthorized", "forbidden", "invalid_credentials", "token_expired", "token_revoked", "email_taken", "identifier_taken", "weak_password", "account_blocked", "busy", "parameter_invalid", "job_lost", "publisher_busy", "unknown_command", "not_subscribable", "camera_offline", "no_snapshot_yet", "live_unavailable", "wrong_kind", "not_recorded", "not_aggregatable", "quota_exceeded", "credential_in_use", "goal_timeout", "robot_in_use", "robot_deletion_partial", "job_queue_full", "invalid_uuid", "rate_limited", "tier_required", "token_spent", "service_timeout", "asset_missing", "dynamic_registration_disabled", "client_limit_reached", "idp_unavailable", "mcp_disabled", "tool_not_available", "capability_required", "last_owner", "target_state_conflict", "signup_closed", "draft_not_a_document", "internal_error", "not_cancellable", "unsupported_media_type", "wrong_browser", "invalid_yaml", "unstorable_yaml", "registration_closed", "domain_not_allowed", "email_unverified", "origin_not_allowed", "template_invalid", "provider_disabled", "provider_misconfigured", "invalid_redirect_uri", "interaction_expired"];
52
+ export declare const ERROR_CODES: readonly ["not_found", "validation_error", "bad_request", "unknown_datapoint", "invalid_token", "protocol_mismatch", "invalid_frame", "duplicate_slug", "reserved_slug", "unknown_slug", "unknown_field_path", "unknown_type", "unknown_topic", "invalid_rate", "invalid_range", "config_conflict", "no_data", "robot_offline", "bridge_timeout", "unauthorized", "forbidden", "invalid_credentials", "token_expired", "token_revoked", "email_taken", "identifier_taken", "weak_password", "account_blocked", "busy", "parameter_invalid", "job_lost", "action_server_lost", "action_failed", "goal_rejected", "goal_send_failed", "result_failed", "goal_uncontrollable", "bridge_disconnected", "config_changed", "publisher_busy", "unknown_command", "not_subscribable", "camera_offline", "no_snapshot_yet", "live_unavailable", "wrong_kind", "not_recorded", "not_aggregatable", "quota_exceeded", "credential_in_use", "goal_timeout", "robot_in_use", "robot_deletion_partial", "job_queue_full", "invalid_uuid", "rate_limited", "tier_required", "token_spent", "service_timeout", "asset_missing", "dynamic_registration_disabled", "client_limit_reached", "idp_unavailable", "mcp_disabled", "tool_not_available", "capability_required", "last_owner", "target_state_conflict", "signup_closed", "draft_not_a_document", "internal_error", "not_cancellable", "unsupported_media_type", "wrong_browser", "invalid_yaml", "unstorable_yaml", "registration_closed", "domain_not_allowed", "email_unverified", "origin_not_allowed", "template_invalid", "provider_disabled", "provider_misconfigured", "invalid_redirect_uri", "interaction_expired"];
53
53
  export type ErrorCode = (typeof ERROR_CODES)[number];
package/dist/errors.js CHANGED
@@ -71,6 +71,13 @@ export const ERROR_CODES = [
71
71
  'no_data',
72
72
  // Talking to the robot.
73
73
  'robot_offline',
74
+ /**
75
+ * The cloud has heard nothing — heartbeat or real progress — from a
76
+ * running job for longer than it tolerates while the bridge is connected:
77
+ * `patience_ms` for a protocol-3 bridge, `JOB_HEARTBEAT_TIMEOUT_MS` for a
78
+ * protocol-4 one once it has heard from the job at all. `job.error.code`
79
+ * on `lost`.
80
+ */
74
81
  'bridge_timeout',
75
82
  // Identity and rights. `forbidden` is deliberately the answer both
76
83
  // for "your role does not grant this" and for "there is no such slug":
@@ -140,6 +147,37 @@ export const ERROR_CODES = [
140
147
  'parameter_invalid',
141
148
  /** The bridge could not account for this job after a restart. */
142
149
  'job_lost',
150
+ /**
151
+ * The robot's action server vanished mid-goal — the bridge's own liveness
152
+ * check found `server_is_ready()` false for three seconds straight and
153
+ * gave up waiting for it to come back. A `job.error.code` on `lost`: the
154
+ * outcome the goal actually reached is unknown, so `lost` — not `failed` —
155
+ * is the honest state, and this code says why.
156
+ */
157
+ 'action_server_lost',
158
+ /** The action ended with a ROS status other than succeeded; `job.error.code` on `failed`. */
159
+ 'action_failed',
160
+ /** The action server rejected the goal outright; `job.error.code` on `failed`. */
161
+ 'goal_rejected',
162
+ /** Sending the goal to the action server itself raised; `job.error.code` on `failed`. */
163
+ 'goal_send_failed',
164
+ /** Asking the action server for its result raised; `job.error.code` on `failed`. */
165
+ 'result_failed',
166
+ /** A goal accepted after its own timeout could not then be cancelled; `job.error.code` on `failed`. */
167
+ 'goal_uncontrollable',
168
+ /**
169
+ * The robot stayed offline for longer than `JOB_OFFLINE_GRACE_MS` while a
170
+ * job was running. A late real outcome, if the robot reconnects and the
171
+ * bridge still has it, corrects this — it is not final the way a genuine
172
+ * bridge report is. `job.error.code` on `lost`.
173
+ */
174
+ 'bridge_disconnected',
175
+ /**
176
+ * The job's action or service no longer exists in the published
177
+ * configuration — a republish invalidated it while it was running.
178
+ * `job.error.code` on `cancelled`.
179
+ */
180
+ 'config_changed',
143
181
  /** Another user holds this publisher and has not been quiet long enough. */
144
182
  'publisher_busy',
145
183
  /** A well-formed realtime frame this server does not know — the socket stays open. */