@fleetless/contracts 1.2.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.
Files changed (68) hide show
  1. package/CHANGELOG.md +39 -1
  2. package/artifacts/constants.json +30 -4
  3. package/artifacts/openapi.json +882 -80
  4. package/artifacts/routes.json +217 -7
  5. package/artifacts/schema/app-deletion-summary.schema.json +51 -0
  6. package/artifacts/schema/apply-error.schema.json +2 -1
  7. package/artifacts/schema/asset-list-response.schema.json +77 -12
  8. package/artifacts/schema/asset-sync-status.schema.json +35 -8
  9. package/artifacts/schema/asset.schema.json +2 -3
  10. package/artifacts/schema/assets-clear-response.schema.json +23 -0
  11. package/artifacts/schema/bridge-asset-progress.schema.json +14 -8
  12. package/artifacts/schema/bridge-config-applied.schema.json +2 -1
  13. package/artifacts/schema/bridge-link-mode.schema.json +36 -0
  14. package/artifacts/schema/bridge-state.schema.json +6 -1
  15. package/artifacts/schema/client-robot-list-item.schema.json +6 -1
  16. package/artifacts/schema/client-robot-list-response.schema.json +6 -1
  17. package/artifacts/schema/cloud-config.schema.json +90 -5
  18. package/artifacts/schema/cloud-hello-ok.schema.json +45 -0
  19. package/artifacts/schema/cloud-ping.schema.json +27 -1
  20. package/artifacts/schema/config-draft-response.schema.json +90 -5
  21. package/artifacts/schema/config-state.schema.json +2 -1
  22. package/artifacts/schema/config-version-response.schema.json +90 -5
  23. package/artifacts/schema/datapoint-config.schema.json +5 -0
  24. package/artifacts/schema/datapoint-frame.schema.json +4 -0
  25. package/artifacts/schema/datapoint-list-response.schema.json +2 -2
  26. package/artifacts/schema/joint-state-put-request.schema.json +23 -0
  27. package/artifacts/schema/joint-state-put-response.schema.json +24 -0
  28. package/artifacts/schema/org-quota-usage-counts.schema.json +0 -5
  29. package/artifacts/schema/org-quota-usage.schema.json +1 -12
  30. package/artifacts/schema/org-quotas.schema.json +1 -7
  31. package/artifacts/schema/put-app-auth-mcp-request.schema.json +27 -0
  32. package/artifacts/schema/put-app-auth-registration-request.schema.json +36 -0
  33. package/artifacts/schema/put-app-auth-urls-request.schema.json +48 -0
  34. package/artifacts/schema/robot-config-doc.schema.json +90 -5
  35. package/artifacts/schema/robot-deletion-summary.schema.json +2 -1
  36. package/artifacts/schema/robot-detail-response.schema.json +63 -2
  37. package/artifacts/schema/robot-list-item.schema.json +15 -1
  38. package/artifacts/schema/robot-list-response.schema.json +15 -1
  39. package/artifacts/schema/robot-token-rotate-response.schema.json +15 -0
  40. package/artifacts/schema-outgoing/bridge-asset-progress.schema.json +14 -8
  41. package/artifacts/schema-outgoing/bridge-config-applied.schema.json +2 -1
  42. package/artifacts/schema-outgoing/bridge-link-mode.schema.json +37 -0
  43. package/artifacts/schema-outgoing/datapoint-frame.schema.json +4 -0
  44. package/dist/app-users.d.ts +25 -7
  45. package/dist/app-users.js +24 -6
  46. package/dist/apps.d.ts +22 -0
  47. package/dist/apps.js +33 -0
  48. package/dist/assets.d.ts +85 -50
  49. package/dist/assets.js +152 -62
  50. package/dist/audit.d.ts +1 -1
  51. package/dist/audit.js +1 -1
  52. package/dist/client-robots.d.ts +2 -0
  53. package/dist/common.d.ts +10 -0
  54. package/dist/common.js +16 -1
  55. package/dist/config.d.ts +69 -1
  56. package/dist/config.js +86 -6
  57. package/dist/errors.d.ts +1 -1
  58. package/dist/errors.js +1 -8
  59. package/dist/index.d.ts +12 -12
  60. package/dist/index.js +6 -6
  61. package/dist/protocol.d.ts +150 -71
  62. package/dist/protocol.js +144 -87
  63. package/dist/rest.d.ts +139 -35
  64. package/dist/rest.js +100 -66
  65. package/dist/routes.js +129 -21
  66. package/package.json +1 -1
  67. package/artifacts/schema/bridge-pressure.schema.json +0 -292
  68. package/artifacts/schema/put-app-auth-config-request.schema.json +0 -93
package/dist/routes.js CHANGED
@@ -1,16 +1,16 @@
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
- import { asset, assetListResponse, assetSyncRequest, assetSyncResponse, assetSyncStatus, missingAssetQuery } from './assets.js';
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';
12
12
  import { authorizationServerMetadata, dynamicClientRegistrationRequest, dynamicClientRegistrationResponse, oauthAuthorizeQuery, oauthRedirectResponse, oauthTokenRequest, oauthTokenResponse, protectedResourceMetadata, } from './oauth.js';
13
- import { cameraListResponse, cancelRequest, configDraftResponse, configVersionResponse, configVersionsResponse, createRobotRequest, createRobotResponse, datapointListResponse, datapointValue, exposureListResponse, fetchTypesRequest, fetchTypesResponse, historyQuery, historyResponse, introspectionResponse, invokeOrServiceResponse, invokeRequest, jobResponse, liveSessionResponse, orgHealthQuery, orgLatencyQuery, orgLatencyResponse, orgQuotaUsage, orgUsageQuery, orgUsageResponse, patchRobotRequest, patchRobotResponse, publishConfigResponse, publishRequest, putConfigDraftRequest, putRobotDetailsRequest, putRobotDetailsResponse, releaseLiveQuery, renameSlugRequest, renameSlugResponse, robotDeleteQuery, resourceHealthListResponse, robotDeletionSummary, robotDetailResponse, robotJobsResponse, robotListResponse, slugUsageResponse, snapshotMetaResponse, typesResponse, } from './rest.js';
13
+ import { cameraListResponse, cancelRequest, configDraftResponse, configVersionResponse, configVersionsResponse, createRobotRequest, createRobotResponse, datapointListResponse, datapointValue, exposureListResponse, fetchTypesRequest, fetchTypesResponse, historyQuery, historyResponse, introspectionResponse, invokeOrServiceResponse, invokeRequest, jobResponse, liveSessionResponse, orgHealthQuery, orgLatencyQuery, orgLatencyResponse, orgQuotaUsage, orgUsageQuery, orgUsageResponse, patchRobotRequest, patchRobotResponse, publishConfigResponse, publishRequest, putConfigDraftRequest, putRobotDetailsRequest, putRobotDetailsResponse, robotTokenRotateResponse, jointStatePutRequest, jointStatePutResponse, releaseLiveQuery, renameSlugRequest, renameSlugResponse, robotDeleteQuery, resourceHealthListResponse, robotDeletionSummary, robotDetailResponse, robotJobsResponse, robotListResponse, slugUsageResponse, snapshotMetaResponse, typesResponse, } from './rest.js';
14
14
  export const ROUTE_SECTIONS = [
15
15
  { id: 'health', title: 'Health' },
16
16
  { id: 'developer-auth', title: 'Developer auth' },
@@ -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.',
685
729
  },
686
730
  {
687
- method: 'PUT', path: '/api/apps/:id/auth-config', section: 'apps',
688
- summary: "Replaces the app's auth settings in one write.",
731
+ method: 'PUT', path: '/api/apps/:id/auth-config/urls', section: 'apps',
732
+ summary: "Replaces the three pages Fleetless's mails point at.",
689
733
  audience: 'developer', auth: 'developer', rateLimited: false, ownerTier: false, status: 200,
690
734
  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,
735
+ query: null, request: putAppAuthUrlsRequest, response: appAuthConfig,
692
736
  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.',
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.',
744
+ },
745
+ {
746
+ method: 'PUT', path: '/api/apps/:id/auth-config/mcp', section: 'apps',
747
+ summary: 'Replaces the MCP switch and its login URL together.',
748
+ audience: 'developer', auth: 'developer', rateLimited: false, ownerTier: false, status: 200,
749
+ params: [{ name: 'id', description: 'The app\'s uuid, as returned by `POST /api/apps` or listed by `GET /api/apps`.' }],
750
+ query: null, request: putAppAuthMcpRequest, response: appAuthConfig,
751
+ errors: [...DEVELOPER_GUARD, 'invalid_uuid', 'validation_error', 'not_found'], transport: 'http',
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',
@@ -1664,6 +1723,40 @@ export const ROUTES = [
1664
1723
  'and the event carries no `details`, because the one interesting value here is the token. `max_robots` is checked before anything is ' +
1665
1724
  'created, which is only safe because robot deletion exists.',
1666
1725
  },
1726
+ {
1727
+ method: 'POST', path: '/api/robots/:id/token/rotate', section: 'robots',
1728
+ summary: 'Mints a new bridge token for the robot and invalidates the old one.',
1729
+ audience: 'developer', auth: 'developer', rateLimited: false, ownerTier: true, status: 201,
1730
+ params: [{ name: 'id', description: 'The robot\'s uuid, as returned by `POST /api/robots` or listed by `GET /api/robots`.' }],
1731
+ query: null, request: null, response: robotTokenRotateResponse,
1732
+ errors: [...DEVELOPER_GUARD, 'tier_required', 'invalid_uuid', 'not_found'], transport: 'http',
1733
+ notes: 'Owner tier, behind the org-scoped lookup, so a developer-tier admin sees the `404` a stranger would for a robot outside their org rather ' +
1734
+ 'than a tier refusal that confirms the id exists. `token` is the only moment the new secret exists outside the caller\'s hands — the cloud ' +
1735
+ 'stores a hash — so a caller who loses it rotates again. Audited as `robot.token_rotated`, with no `details`: the one interesting value ' +
1736
+ 'here is the token. \n\n**It stops the bridge that is connected right now.** The old secret is gone the instant the hash is replaced, so ' +
1737
+ 'the cloud closes that socket with `CLOSE_TOKEN_ROTATED` rather than leaving a bridge speaking on a credential nothing would accept ' +
1738
+ 'again. A bridge that does not know the code reconnects and is refused at hello as `invalid_token`, which is the honest answer and ends ' +
1739
+ 'the same way. **The robot is offline until somebody puts the new token on it** — this is a deliberate interruption, not a background ' +
1740
+ 'rekey, and a fleet cannot be rotated without a visit to each robot.',
1741
+ },
1742
+ {
1743
+ method: 'PUT', path: '/api/robots/:id/urdf/joint-state', section: 'robots',
1744
+ summary: 'Chooses the datapoint whose joint positions move the robot\'s URDF, or clears it.',
1745
+ audience: 'developer', auth: 'developer', rateLimited: false, ownerTier: false, status: 200,
1746
+ params: [{ name: 'id', description: 'The robot\'s uuid, as returned by `POST /api/robots` or listed by `GET /api/robots`.' }],
1747
+ query: null, request: jointStatePutRequest, response: jointStatePutResponse,
1748
+ errors: [...DEVELOPER_GUARD, 'invalid_uuid', 'not_found', 'validation_error'], transport: 'http',
1749
+ notes: '**What qualifies**: a datapoint of the **published** configuration whose ROS type is `sensor_msgs/msg/JointState` and which carries no ' +
1750
+ '`field` — the whole message, because positions and names arrive together and a single extracted field is half of a pose. Anything else ' +
1751
+ 'is a `validation_error` naming that rule rather than a stored mapping that renders a battery reading as a robot. `{ "slug": null }` ' +
1752
+ 'clears it, which is why the field is required and nullable rather than optional. \n\n**The mapping cannot outlive what it points at.** ' +
1753
+ 'Every successful publish re-checks it against the new document and clears it when it no longer qualifies, recording ' +
1754
+ '`robot.joint_state_cleared` with the version that did it; a slug rename rewrites it like every other reference the editor already ' +
1755
+ 'rewrites; deleting the robot takes it along. Every write through this route — a slug or `null` — is on the record too, as ' +
1756
+ '`robot.joint_state_set` with the actor and the slug, so a clear a person made is never mistaken for one a publish made. The stored ' +
1757
+ 'value reads back on `GET /api/robots/:id/assets` as `joint_state_slug`, so a renderer fetches the URDF, the meshes and the mapping ' +
1758
+ 'from one place.',
1759
+ },
1667
1760
  {
1668
1761
  method: 'GET', path: '/api/robots', section: 'robots',
1669
1762
  summary: "Lists the org's robots with their connection state and exposure counts.",
@@ -2233,6 +2326,20 @@ export const ROUTES = [
2233
2326
  notes: 'Developer sessions only, like starting a sync: the guard admits three caller kinds and the handler answers `401 unauthorized` to the ' +
2234
2327
  'other two. A sync belonging to another robot reads exactly like one that never existed, which is why the robot is resolved first.',
2235
2328
  },
2329
+ {
2330
+ method: 'DELETE', path: '/api/robots/:id/assets', section: 'assets',
2331
+ summary: "Empties a robot's asset store: every URDF, mesh and texture, gone at once.",
2332
+ audience: 'client', auth: 'developer_or_client', rateLimited: false, ownerTier: true, status: 200,
2333
+ params: [{ name: 'id', description: 'The robot\'s uuid, as returned by `POST /api/robots` or listed by `GET /api/robots`.' }],
2334
+ query: null, request: null, response: assetsClearResponse,
2335
+ errors: [...CLIENT_GUARD, 'tier_required', 'invalid_uuid', 'not_found', 'busy'], transport: 'http',
2336
+ notes: 'The store\'s escape hatch: a full store is never a dead end, and this is the blunt third of the three answers to it — the URDF upload ' +
2337
+ 'is exempt from the gate, reconcile after a sync already frees what the new URDF stopped referencing, and this route lets an Owner clear ' +
2338
+ 'the robot outright. Owner tier, unconditionally, like starting a sync. Removes every asset of the robot and resets its store to `0`; ' +
2339
+ "the next sync fills it again. It does not touch the bridge's availability report — `urdf_available` still answers from the connected " +
2340
+ 'robot, unrelated to what this cloud happens to have stored. A clear while a sync is running is `409 busy` naming that sync\'s ' +
2341
+ 'details, the same refusal starting a second sync gets, because deleting under a running upload would leave the store counter wrong.',
2342
+ },
2236
2343
  /* ------------------------------------------------ org (quotas and fleet reads) */
2237
2344
  {
2238
2345
  method: 'GET', path: '/api/org/quotas', section: 'org',
@@ -2306,15 +2413,16 @@ export const ROUTES = [
2306
2413
  summary: 'Takes one asset file from a robot during a sync.',
2307
2414
  audience: 'internal', auth: 'robot_upload', rateLimited: false, ownerTier: false, status: 201,
2308
2415
  params: [], query: null, request: null, response: asset,
2309
- errors: ['unauthorized', 'rate_limited', 'asset_too_large', 'validation_error', 'not_found', 'quota_exceeded', 'bad_request'], transport: 'http',
2416
+ errors: ['unauthorized', 'rate_limited', 'validation_error', 'not_found', 'quota_exceeded', 'bad_request'], transport: 'http',
2310
2417
  notes: 'The body is the **raw file bytes**, not JSON, so it has no request schema; everything about the file — its kind, its name, its sync id ' +
2311
2418
  'and its announced size — rides in the `x-fleetless-asset-*` headers `ASSET_UPLOAD_HEADERS` names. The credential is a short-lived ' +
2312
2419
  'upload token minted by `POST /api/robots/:id/assets/sync`, verified in a `preParsing` hook so a refusal precedes the work rather than ' +
2313
- 'following it: a `preHandler` would already have buffered the whole file. The announced size is refused there too, before a single byte ' +
2314
- 'is read — it is an announcement and not a proof, so it only ever rejects early and never accepts early, and a body that lies small is ' +
2315
- 'still caught by the real length check. Past both, the server\'s own body limit answers a bare `413 bad_request` with neither ceiling nor ' +
2316
- 'size in it. Rate limited per robot inside that same hook, which is why `rateLimited` is `false`: there is no rate-limiting preHandler ' +
2317
- 'registered on this route.',
2420
+ 'following it: a `preHandler` would already have buffered the whole file. **Nothing is refused for its own size** — the robot\'s asset ' +
2421
+ 'store is the only limit, so the announced size is checked there against `ROBOT_ASSET_STORE_BYTES` and a file with no room left answers ' +
2422
+ '`409 quota_exceeded` carrying `store_bytes`, `used_bytes` and `size_bytes`, while the sync carries on with the next file. Past that, ' +
2423
+ 'the server\'s own body limit answers a bare `413 bad_request` with none of those numbers in it. Rate limited per robot inside that same ' +
2424
+ 'hook, which is why `rateLimited` is `false`: there is no rate-limiting preHandler registered on this route. The URDF itself is never ' +
2425
+ 'refused for the store; only meshes and textures are charged against it.',
2318
2426
  },
2319
2427
  /* ------------------------------------ realtime and bridge transports */
2320
2428
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fleetless/contracts",
3
- "version": "1.2.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,292 +0,0 @@
1
- {
2
- "$schema": "https://json-schema.org/draft/2020-12/schema",
3
- "type": "object",
4
- "properties": {
5
- "link": {
6
- "type": "object",
7
- "properties": {
8
- "rate_bps": {
9
- "anyOf": [
10
- {
11
- "type": "number",
12
- "minimum": 0
13
- },
14
- {
15
- "type": "null"
16
- }
17
- ]
18
- },
19
- "snapshot_max_bytes": {
20
- "type": "integer",
21
- "minimum": 0,
22
- "maximum": 9007199254740991
23
- }
24
- },
25
- "required": [
26
- "rate_bps",
27
- "snapshot_max_bytes"
28
- ]
29
- },
30
- "tiers": {
31
- "type": "object",
32
- "properties": {
33
- "0": {
34
- "type": "object",
35
- "properties": {
36
- "sent": {
37
- "type": "integer",
38
- "minimum": 0,
39
- "maximum": 9007199254740991
40
- },
41
- "bytes": {
42
- "type": "integer",
43
- "minimum": 0,
44
- "maximum": 9007199254740991
45
- },
46
- "drops": {
47
- "type": "integer",
48
- "minimum": 0,
49
- "maximum": 9007199254740991
50
- },
51
- "high_water": {
52
- "type": "integer",
53
- "minimum": 0,
54
- "maximum": 9007199254740991
55
- }
56
- },
57
- "required": [
58
- "sent",
59
- "bytes",
60
- "drops",
61
- "high_water"
62
- ]
63
- },
64
- "1": {
65
- "type": "object",
66
- "properties": {
67
- "sent": {
68
- "type": "integer",
69
- "minimum": 0,
70
- "maximum": 9007199254740991
71
- },
72
- "bytes": {
73
- "type": "integer",
74
- "minimum": 0,
75
- "maximum": 9007199254740991
76
- },
77
- "drops": {
78
- "type": "integer",
79
- "minimum": 0,
80
- "maximum": 9007199254740991
81
- },
82
- "high_water": {
83
- "type": "integer",
84
- "minimum": 0,
85
- "maximum": 9007199254740991
86
- }
87
- },
88
- "required": [
89
- "sent",
90
- "bytes",
91
- "drops",
92
- "high_water"
93
- ]
94
- },
95
- "2": {
96
- "type": "object",
97
- "properties": {
98
- "sent": {
99
- "type": "integer",
100
- "minimum": 0,
101
- "maximum": 9007199254740991
102
- },
103
- "bytes": {
104
- "type": "integer",
105
- "minimum": 0,
106
- "maximum": 9007199254740991
107
- },
108
- "drops": {
109
- "type": "integer",
110
- "minimum": 0,
111
- "maximum": 9007199254740991
112
- },
113
- "high_water": {
114
- "type": "integer",
115
- "minimum": 0,
116
- "maximum": 9007199254740991
117
- }
118
- },
119
- "required": [
120
- "sent",
121
- "bytes",
122
- "drops",
123
- "high_water"
124
- ]
125
- },
126
- "3": {
127
- "type": "object",
128
- "properties": {
129
- "sent": {
130
- "type": "integer",
131
- "minimum": 0,
132
- "maximum": 9007199254740991
133
- },
134
- "bytes": {
135
- "type": "integer",
136
- "minimum": 0,
137
- "maximum": 9007199254740991
138
- },
139
- "drops": {
140
- "type": "integer",
141
- "minimum": 0,
142
- "maximum": 9007199254740991
143
- },
144
- "high_water": {
145
- "type": "integer",
146
- "minimum": 0,
147
- "maximum": 9007199254740991
148
- }
149
- },
150
- "required": [
151
- "sent",
152
- "bytes",
153
- "drops",
154
- "high_water"
155
- ]
156
- },
157
- "4": {
158
- "type": "object",
159
- "properties": {
160
- "sent": {
161
- "type": "integer",
162
- "minimum": 0,
163
- "maximum": 9007199254740991
164
- },
165
- "bytes": {
166
- "type": "integer",
167
- "minimum": 0,
168
- "maximum": 9007199254740991
169
- },
170
- "drops": {
171
- "type": "integer",
172
- "minimum": 0,
173
- "maximum": 9007199254740991
174
- },
175
- "high_water": {
176
- "type": "integer",
177
- "minimum": 0,
178
- "maximum": 9007199254740991
179
- }
180
- },
181
- "required": [
182
- "sent",
183
- "bytes",
184
- "drops",
185
- "high_water"
186
- ]
187
- },
188
- "5": {
189
- "type": "object",
190
- "properties": {
191
- "sent": {
192
- "type": "integer",
193
- "minimum": 0,
194
- "maximum": 9007199254740991
195
- },
196
- "bytes": {
197
- "type": "integer",
198
- "minimum": 0,
199
- "maximum": 9007199254740991
200
- },
201
- "drops": {
202
- "type": "integer",
203
- "minimum": 0,
204
- "maximum": 9007199254740991
205
- },
206
- "high_water": {
207
- "type": "integer",
208
- "minimum": 0,
209
- "maximum": 9007199254740991
210
- }
211
- },
212
- "required": [
213
- "sent",
214
- "bytes",
215
- "drops",
216
- "high_water"
217
- ]
218
- }
219
- },
220
- "additionalProperties": false
221
- },
222
- "video": {
223
- "type": "object",
224
- "properties": {
225
- "active_streams": {
226
- "type": "integer",
227
- "minimum": 0,
228
- "maximum": 9007199254740991
229
- },
230
- "bitrate_sum_kbps": {
231
- "type": "integer",
232
- "minimum": 0,
233
- "maximum": 9007199254740991
234
- },
235
- "uplink_kbps": {
236
- "anyOf": [
237
- {
238
- "type": "integer",
239
- "minimum": 0,
240
- "maximum": 9007199254740991
241
- },
242
- {
243
- "type": "null"
244
- }
245
- ]
246
- },
247
- "override_kbps": {
248
- "anyOf": [
249
- {
250
- "type": "integer",
251
- "minimum": 0,
252
- "maximum": 9007199254740991
253
- },
254
- {
255
- "type": "null"
256
- }
257
- ]
258
- },
259
- "video_budget_kbps": {
260
- "anyOf": [
261
- {
262
- "type": "integer",
263
- "minimum": 0,
264
- "maximum": 9007199254740991
265
- },
266
- {
267
- "type": "null"
268
- }
269
- ]
270
- },
271
- "reserve_kbps": {
272
- "type": "integer",
273
- "minimum": 0,
274
- "maximum": 9007199254740991
275
- }
276
- },
277
- "required": [
278
- "active_streams",
279
- "bitrate_sum_kbps",
280
- "uplink_kbps",
281
- "override_kbps",
282
- "video_budget_kbps",
283
- "reserve_kbps"
284
- ]
285
- }
286
- },
287
- "required": [
288
- "link",
289
- "tiers",
290
- "video"
291
- ]
292
- }
@@ -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
- }