@fleetless/contracts 6.1.0 → 6.2.0-next.2

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
@@ -9,6 +9,28 @@ version.
9
9
 
10
10
  ## [Unreleased]
11
11
 
12
+ ### Added
13
+
14
+ - **`file_too_large`** (`413`): one asset file is larger than
15
+ `ASSET_FILE_MAX_BYTES` (one gigabyte, every plan, the URDF included).
16
+ `details` is `fileTooLargeDetails`: `max_bytes` and `size_bytes`, the
17
+ announced size or, when none (or a false one) was announced, the bytes that
18
+ arrived; `null` only when the body limit stopped the upload.
19
+ `ASSET_FILE_MAX_BYTES` is in `constants.json`. `plan_limit` and
20
+ `quota_exceeded` on an asset upload now mean only that the store is full.
21
+ - **`plan_limit` in `clientOidcErrorCode`.** A federated sign-in that would
22
+ create an app user beyond the org's plan `app_users` limit now redirects
23
+ with `?error=plan_limit`, so an app can tell that the plan is full.
24
+ `quota_exceeded` keeps one meaning there: the `max_end_users` protection
25
+ ceiling, which also covers orgs still on the beta. A consumer that switches
26
+ exhaustively over `ClientOidcErrorCode` gains one case.
27
+
28
+ ### Changed
29
+
30
+ - **`POST /api/bridge/assets`** lists `file_too_large` and `plan_limit` among
31
+ its errors, and its notes describe the per-file limit instead of a bare
32
+ `413`.
33
+
12
34
  ## [6.1.0] — 2026-10-03
13
35
 
14
36
  ### Added
@@ -23,6 +23,7 @@
23
23
  "size": "x-fleetless-asset-size"
24
24
  },
25
25
  "ROBOT_ASSET_STORE_BYTES": 1000000000,
26
+ "ASSET_FILE_MAX_BYTES": 1000000000,
26
27
  "SNAPSHOT_HEADERS": {
27
28
  "ageMs": "x-fleetless-age-ms",
28
29
  "timestampMs": "x-fleetless-timestamp-ms",
@@ -6488,11 +6488,13 @@
6488
6488
  "rate_limited",
6489
6489
  "validation_error",
6490
6490
  "not_found",
6491
+ "file_too_large",
6492
+ "plan_limit",
6491
6493
  "quota_exceeded",
6492
6494
  "bad_request"
6493
6495
  ],
6494
6496
  "transport": "http",
6495
- "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 and its announced size — rides in the `x-fleetless-asset-*` headers `ASSET_UPLOAD_HEADERS` names. The credential is a short-lived upload token minted by `POST /api/robots/:id/assets/sync`, verified in a `preParsing` hook so a refusal precedes the work rather than following it: a `preHandler` would already have buffered the whole file. **Nothing is refused for its own size** — the robot's asset 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 `409 quota_exceeded` carrying `store_bytes`, `used_bytes` and `size_bytes`, while the sync carries on with the next file. Past that, 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 hook, which is why `rateLimited` is `false`: there is no rate-limiting preHandler registered on this route. The URDF itself is never refused for the store; only meshes and textures are charged against it."
6497
+ "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 and its announced size — rides in the `x-fleetless-asset-*` headers `ASSET_UPLOAD_HEADERS` names. The credential is a short-lived upload token minted by `POST /api/robots/:id/assets/sync`, verified in a `preParsing` hook so a refusal precedes the work rather than following it: a `preHandler` would already have buffered the whole file. **One file can be at most `ASSET_FILE_MAX_BYTES`**, on every plan and for every kind, the URDF included: an announced size over it answers `413 file_too_large` with `max_bytes` and `size_bytes` before a byte is buffered; without a truthful size, a body over the limit answers the same code with the bytes that arrived as `size_bytes`, or with `size_bytes: null` when it ran past the server's body limit and nobody counted the bytes. Retrying does not help. A file that fits but finds the store full answers `409 plan_limit` (or `409 quota_exceeded` for an organisation still on the beta) carrying `store_bytes`, `used_bytes` and `size_bytes`, while the sync carries on with the next file. Rate limited per robot inside that same hook, which is why `rateLimited` is `false`: there is no rate-limiting preHandler registered on this route. The URDF itself is never refused for the store; only meshes and textures are charged against it."
6496
6498
  },
6497
6499
  {
6498
6500
  "method": "GET",
@@ -0,0 +1,30 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "type": "object",
4
+ "properties": {
5
+ "max_bytes": {
6
+ "type": "integer",
7
+ "exclusiveMinimum": 0,
8
+ "maximum": 9007199254740991,
9
+ "description": "The most one asset file can be, in bytes: `ASSET_FILE_MAX_BYTES`, the same on every plan."
10
+ },
11
+ "size_bytes": {
12
+ "anyOf": [
13
+ {
14
+ "type": "integer",
15
+ "exclusiveMinimum": 0,
16
+ "maximum": 9007199254740991
17
+ },
18
+ {
19
+ "type": "null"
20
+ }
21
+ ],
22
+ "description": "The refused file's size, in bytes: the announced size, or the bytes that arrived when none (or a false one) was announced; `null` only when the body limit stopped the upload and nobody counted the bytes."
23
+ }
24
+ },
25
+ "required": [
26
+ "max_bytes",
27
+ "size_bytes"
28
+ ],
29
+ "additionalProperties": false
30
+ }
package/dist/assets.d.ts CHANGED
@@ -196,6 +196,20 @@ export type AssetSyncResponse = z.infer<typeof assetSyncResponse>;
196
196
  * the receiver enforces is two numbers that agree until one of them moves.
197
197
  */
198
198
  export declare const ROBOT_ASSET_STORE_BYTES = 1000000000;
199
+ /**
200
+ * **How large one asset file may be, on every plan.**
201
+ *
202
+ * Its own number, not the store's, although both are a gigabyte today: the
203
+ * store says how much a robot may keep, this says how much the cloud will
204
+ * take in one request. A file over it is refused `413 file_too_large` with
205
+ * `fileTooLargeDetails`, before a byte is buffered when the size was
206
+ * announced (`ASSET_UPLOAD_HEADERS.size`), and otherwise on the bytes that
207
+ * arrived, or by the server's body limit when the body ran past it. That refusal is not a store or plan limit, applies to the URDF
208
+ * as well, and retrying does not help.
209
+ *
210
+ * In `constants.json` for the same reason as `ROBOT_ASSET_STORE_BYTES`.
211
+ */
212
+ export declare const ASSET_FILE_MAX_BYTES = 1000000000;
199
213
  /**
200
214
  * What a full store tells the caller — the same discipline as `job_queue_full`
201
215
  * and `publisher_busy`: a refusal that names a state and no number leaves the
@@ -233,9 +247,11 @@ export type AssetStoreRefusedDetails = z.infer<typeof assetStoreRefusedDetails>;
233
247
  * unexamined.
234
248
  *
235
249
  * There was a fourth, `too_large`, for a file over a per-file ceiling. That
236
- * ceiling is gone — a robot has one store and nothing is refused for its own
237
- * size — so the kind had no producer left and one fewer thing to branch on is
238
- * the whole of the gain.
250
+ * ceiling is gone from this enum, not from the cloud: a file over
251
+ * `ASSET_FILE_MAX_BYTES` is refused by the cloud as `413 file_too_large`; a
252
+ * producer that relays it reports `refused` with no details — so the kind
253
+ * had no producer left here and one fewer thing to branch on is the whole of
254
+ * the gain.
239
255
  *
240
256
  * A consumer that cannot act on the distinction may still print `reference`
241
257
  * alone and lose nothing it had before.
package/dist/assets.js CHANGED
@@ -251,6 +251,20 @@ export const assetSyncResponse = z.object({
251
251
  * the receiver enforces is two numbers that agree until one of them moves.
252
252
  */
253
253
  export const ROBOT_ASSET_STORE_BYTES = 1_000_000_000;
254
+ /**
255
+ * **How large one asset file may be, on every plan.**
256
+ *
257
+ * Its own number, not the store's, although both are a gigabyte today: the
258
+ * store says how much a robot may keep, this says how much the cloud will
259
+ * take in one request. A file over it is refused `413 file_too_large` with
260
+ * `fileTooLargeDetails`, before a byte is buffered when the size was
261
+ * announced (`ASSET_UPLOAD_HEADERS.size`), and otherwise on the bytes that
262
+ * arrived, or by the server's body limit when the body ran past it. That refusal is not a store or plan limit, applies to the URDF
263
+ * as well, and retrying does not help.
264
+ *
265
+ * In `constants.json` for the same reason as `ROBOT_ASSET_STORE_BYTES`.
266
+ */
267
+ export const ASSET_FILE_MAX_BYTES = 1_000_000_000;
254
268
  /**
255
269
  * What a full store tells the caller — the same discipline as `job_queue_full`
256
270
  * and `publisher_busy`: a refusal that names a state and no number leaves the
@@ -293,9 +307,11 @@ export const assetStoreRefusedDetails = z.object({
293
307
  * unexamined.
294
308
  *
295
309
  * There was a fourth, `too_large`, for a file over a per-file ceiling. That
296
- * ceiling is gone — a robot has one store and nothing is refused for its own
297
- * size — so the kind had no producer left and one fewer thing to branch on is
298
- * the whole of the gain.
310
+ * ceiling is gone from this enum, not from the cloud: a file over
311
+ * `ASSET_FILE_MAX_BYTES` is refused by the cloud as `413 file_too_large`; a
312
+ * producer that relays it reports `refused` with no details — so the kind
313
+ * had no producer left here and one fewer thing to branch on is the whole of
314
+ * the gain.
299
315
  *
300
316
  * A consumer that cannot act on the distinction may still print `reference`
301
317
  * alone and lose nothing it had before.
@@ -373,14 +373,23 @@ export type ClientOidcExchangeRequest = z.infer<typeof clientOidcExchangeRequest
373
373
  * `provider_misconfigured`, `provider_disabled` — the provider's or the
374
374
  * developer's to fix, and the app can say so.
375
375
  * - `invalid_request` — the start parameters did not hold up.
376
- * - `quota_exceeded` — the org has as many app users as its `max_end_users`
377
- * quota allows, so no account can be created for this identity. Named rather
378
- * than folded into `no_access`, for `domain_not_allowed`'s reason: it is not
379
- * about the person, the app can say what happened, and the remedy belongs to
380
- * the developer rather than to whoever is trying to sign in. It is raised
376
+ * - `quota_exceeded` — the org is at the protection ceiling: it has as many
377
+ * app users as its `max_end_users` quota allows, so no account can be
378
+ * created for this identity. This also covers an org still on the beta,
379
+ * whose plan is not enforced yet. Named rather than folded into
380
+ * `no_access`, for `domain_not_allowed`'s reason: it is not about the
381
+ * person, the app can say what happened, and the remedy belongs to the
382
+ * developer rather than to whoever is trying to sign in. It is raised
381
383
  * **only where an account would be created** — an identity that already has
382
384
  * one signs in at the quota exactly as it does under it, because refusing a
383
385
  * sign-in would turn a protection limit into an outage.
386
+ * - `plan_limit` — the org's plan has no room for another app user: it is at
387
+ * its plan's `app_users` limit (fleetless/fleetless#103), counted across
388
+ * every app of the org with pending invitations included. Checked before
389
+ * the protection ceiling, and raised, like `quota_exceeded`, **only where
390
+ * an account would be created**. The remedy is a higher plan or an add-on,
391
+ * which is why it is not `quota_exceeded`. It is the same code the other
392
+ * app-user doors answer as `409 plan_limit`.
384
393
  */
385
394
  export declare const clientOidcErrorCode: z.ZodEnum<{
386
395
  no_access: "no_access";
@@ -395,6 +404,7 @@ export declare const clientOidcErrorCode: z.ZodEnum<{
395
404
  provider_disabled: "provider_disabled";
396
405
  invalid_request: "invalid_request";
397
406
  quota_exceeded: "quota_exceeded";
407
+ plan_limit: "plan_limit";
398
408
  }>;
399
409
  export type ClientOidcErrorCode = z.infer<typeof clientOidcErrorCode>;
400
410
  /**
@@ -435,14 +435,23 @@ export const clientOidcExchangeRequest = z
435
435
  * `provider_misconfigured`, `provider_disabled` — the provider's or the
436
436
  * developer's to fix, and the app can say so.
437
437
  * - `invalid_request` — the start parameters did not hold up.
438
- * - `quota_exceeded` — the org has as many app users as its `max_end_users`
439
- * quota allows, so no account can be created for this identity. Named rather
440
- * than folded into `no_access`, for `domain_not_allowed`'s reason: it is not
441
- * about the person, the app can say what happened, and the remedy belongs to
442
- * the developer rather than to whoever is trying to sign in. It is raised
438
+ * - `quota_exceeded` — the org is at the protection ceiling: it has as many
439
+ * app users as its `max_end_users` quota allows, so no account can be
440
+ * created for this identity. This also covers an org still on the beta,
441
+ * whose plan is not enforced yet. Named rather than folded into
442
+ * `no_access`, for `domain_not_allowed`'s reason: it is not about the
443
+ * person, the app can say what happened, and the remedy belongs to the
444
+ * developer rather than to whoever is trying to sign in. It is raised
443
445
  * **only where an account would be created** — an identity that already has
444
446
  * one signs in at the quota exactly as it does under it, because refusing a
445
447
  * sign-in would turn a protection limit into an outage.
448
+ * - `plan_limit` — the org's plan has no room for another app user: it is at
449
+ * its plan's `app_users` limit (fleetless/fleetless#103), counted across
450
+ * every app of the org with pending invitations included. Checked before
451
+ * the protection ceiling, and raised, like `quota_exceeded`, **only where
452
+ * an account would be created**. The remedy is a higher plan or an add-on,
453
+ * which is why it is not `quota_exceeded`. It is the same code the other
454
+ * app-user doors answer as `409 plan_limit`.
446
455
  */
447
456
  export const clientOidcErrorCode = z.enum([
448
457
  'no_access',
@@ -457,6 +466,7 @@ export const clientOidcErrorCode = z.enum([
457
466
  'provider_disabled',
458
467
  'invalid_request',
459
468
  'quota_exceeded',
469
+ 'plan_limit',
460
470
  ]);
461
471
  /* ------------------------------------------------ MCP, delegated login -- */
462
472
  /**
package/dist/errors.d.ts CHANGED
@@ -209,10 +209,26 @@ export declare const orgLockedDetails: z.ZodObject<{
209
209
  }>;
210
210
  }, z.core.$strip>;
211
211
  export type OrgLockedDetails = z.infer<typeof orgLockedDetails>;
212
+ /**
213
+ * The `details` of a `413 file_too_large` refusal (2026-10-03,
214
+ * fleetless/fleetless#136): one file is larger than `ASSET_FILE_MAX_BYTES`.
215
+ * Not a store and not a plan limit — those are `409 plan_limit` and
216
+ * `409 quota_exceeded`, and mean only "the store is full". `size_bytes` is
217
+ * the file's size: the announced one when the refusal came before the body
218
+ * was read, or the bytes that arrived when no size (or a false one) was
219
+ * announced and the body still fit the server's body limit. It is `null` only
220
+ * when the body limit stopped the upload, because then nobody counted the
221
+ * bytes.
222
+ */
223
+ export declare const fileTooLargeDetails: z.ZodObject<{
224
+ max_bytes: z.ZodNumber;
225
+ size_bytes: z.ZodNullable<z.ZodNumber>;
226
+ }, z.core.$strip>;
227
+ export type FileTooLargeDetails = z.infer<typeof fileTooLargeDetails>;
212
228
  /**
213
229
  * The codes in use today. The wire deliberately allows any string — this
214
230
  * list is the shared vocabulary, not a closed set, so a new refusal never
215
231
  * needs a contracts release before it can be reported honestly.
216
232
  */
217
- export declare const ERROR_CODES: readonly ["not_found", "validation_error", "bad_request", "unknown_datapoint", "invalid_token", "protocol_mismatch", "bridge_too_old", "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", "cancel_rejected", "job_lost", "job_unknown_to_bridge", "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", "role_name_taken", "role_in_use", "last_role", "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", "invalid_code", "method_not_allowed", "plan_limit", "plan_required", "org_locked"];
233
+ export declare const ERROR_CODES: readonly ["not_found", "validation_error", "bad_request", "unknown_datapoint", "invalid_token", "protocol_mismatch", "bridge_too_old", "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", "cancel_rejected", "job_lost", "job_unknown_to_bridge", "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", "role_name_taken", "role_in_use", "last_role", "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", "invalid_code", "method_not_allowed", "plan_limit", "plan_required", "org_locked", "file_too_large"];
218
234
  export type ErrorCode = (typeof ERROR_CODES)[number];
package/dist/errors.js CHANGED
@@ -135,6 +135,25 @@ export const orgLockedDetails = z.object({
135
135
  description: '`payment`: a payment is missing. `migration`: the org did not choose what stays when the beta ended.',
136
136
  }),
137
137
  });
138
+ /**
139
+ * The `details` of a `413 file_too_large` refusal (2026-10-03,
140
+ * fleetless/fleetless#136): one file is larger than `ASSET_FILE_MAX_BYTES`.
141
+ * Not a store and not a plan limit — those are `409 plan_limit` and
142
+ * `409 quota_exceeded`, and mean only "the store is full". `size_bytes` is
143
+ * the file's size: the announced one when the refusal came before the body
144
+ * was read, or the bytes that arrived when no size (or a false one) was
145
+ * announced and the body still fit the server's body limit. It is `null` only
146
+ * when the body limit stopped the upload, because then nobody counted the
147
+ * bytes.
148
+ */
149
+ export const fileTooLargeDetails = z.object({
150
+ max_bytes: z.number().int().positive().meta({
151
+ description: 'The most one asset file can be, in bytes: `ASSET_FILE_MAX_BYTES`, the same on every plan.',
152
+ }),
153
+ size_bytes: z.number().int().positive().nullable().meta({
154
+ description: 'The refused file\'s size, in bytes: the announced size, or the bytes that arrived when none (or a false one) was announced; `null` only when the body limit stopped the upload and nobody counted the bytes.',
155
+ }),
156
+ });
138
157
  /**
139
158
  * The codes in use today. The wire deliberately allows any string — this
140
159
  * list is the shared vocabulary, not a closed set, so a new refusal never
@@ -975,6 +994,10 @@ export const ERROR_CODES = [
975
994
  * The existing `quota_exceeded` protection ceiling is still checked, and
976
995
  * only after this one: it exists to stop runaway consumption, not to tell
977
996
  * a developer what their plan allows.
997
+ *
998
+ * At an OIDC callback the same refusal reaches the app as the
999
+ * `clientOidcErrorCode` of the same name, redirected rather than answered
1000
+ * as JSON.
978
1001
  */
979
1002
  'plan_limit',
980
1003
  /**
@@ -995,4 +1018,14 @@ export const ERROR_CODES = [
995
1018
  * payment, or the platform's own move off the beta.
996
1019
  */
997
1020
  'org_locked',
1021
+ // 2026-10-03 — the per-file asset limit (#136).
1022
+ /**
1023
+ * `413`: one asset file is larger than `ASSET_FILE_MAX_BYTES`. Refused on
1024
+ * the announced size before the body is read, for every kind including the
1025
+ * URDF, otherwise on the bytes that arrived, or by the body limit when the
1026
+ * body ran past it. `details` is
1027
+ * `fileTooLargeDetails`. Not a store or plan limit, on any plan; retrying
1028
+ * does not help, only a smaller file does.
1029
+ */
1030
+ 'file_too_large',
998
1031
  ];
package/dist/index.d.ts CHANGED
@@ -41,14 +41,14 @@ export { clientRobotListItem, clientRobotListResponse } from './client-robots.js
41
41
  export type { ClientRobotListItem, ClientRobotListResponse } from './client-robots.js';
42
42
  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, appSignInMethods, appTwoFactorPolicy, appHomeUrl, hostedAccent, HOSTED_LOGO_MAX_BYTES, HOSTED_LOGO_TYPES, appHostedPages, putAppAuthRegistrationRequest, putAppAuthSignInRequest, putAppAuthUrlsRequest, putAppAuthMcpRequest, putAppAuthLookRequest, mailTemplateKind, appMailTemplate, appMailTemplateListResponse, putAppMailTemplateRequest, mailTemplatePreviewRequest, mailTemplatePreviewResponse, mailTemplateProblemDetails, mailOutcome, } from './app-users.js';
43
43
  export type { AppUserStatus, AppUser, AppUserListResponse, CreateAppUserRequest, PatchAppUserRequest, CreateAppInvitationRequest, AppInvitation, PendingAppInvitation, AppInvitationListResponse, AppOidcProvider, AppOidcProviderListResponse, CreateAppOidcProviderRequest, PatchAppOidcProviderRequest, AppAuthConfig, AppSignInMethods, AppTwoFactorPolicy, AppHostedPages, PutAppAuthRegistrationRequest, PutAppAuthSignInRequest, PutAppAuthUrlsRequest, PutAppAuthMcpRequest, PutAppAuthLookRequest, MailTemplateKind, AppMailTemplate, AppMailTemplateListResponse, PutAppMailTemplateRequest, MailTemplatePreviewRequest, MailTemplatePreviewResponse, MailTemplateProblemDetails, MailOutcome, } from './app-users.js';
44
- 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';
44
+ export { assetKind, URDF_ASSET_NAME, asset, urdfCompleteness, assetListResponse, assetsClearResponse, missingAssetQuery, assetSyncRequest, assetSyncResponse, assetSyncState, assetSyncStatus, assetFailure, assetFailureKind, assetStoreRefusedDetails, assetSyncBusyDetails, ROBOT_ASSET_STORE_BYTES, ASSET_FILE_MAX_BYTES, } from './assets.js';
45
45
  export type { AssetKind, Asset, UrdfCompleteness, AssetListResponse, AssetsClearResponse, MissingAssetQuery, AssetSyncRequest, AssetSyncResponse, AssetSyncState, AssetSyncStatus, AssetFailure, AssetFailureKind, AssetStoreRefusedDetails, AssetSyncBusyDetails, } from './assets.js';
46
46
  export { auditActor, auditEvent, auditQuery, auditListResponse, AUDIT_CSV_COLUMNS, AUDIT_RETENTION_DAYS } from './audit.js';
47
47
  export type { AuditActor, AuditEvent, AuditQuery, AuditListResponse } from './audit.js';
48
48
  export { alertRowCondition, alertSeverity, alertState, datapointAlertRow, alertListResponse, orgFiringAlertsResponse, orgAlertsQuery, datapointDisplay, putDatapointDisplayRequest, } from './alerts.js';
49
49
  export type { AlertRowCondition, AlertSeverity, AlertState, DatapointAlertRow, AlertListResponse, OrgFiringAlertsResponse, OrgAlertsQuery, DatapointDisplay, PutDatapointDisplayRequest, } from './alerts.js';
50
- export { apiError, parameterViolation, parameterInvalidDetails, cancelRejectedDetails, invalidCodeDetails, ERROR_CODES, planLimitDetails, assetPlanLimitDetails, planRequiredDetails, orgLockedDetails, } from './errors.js';
51
- export type { ApiError, ParameterViolation, ParameterInvalidDetails, CancelRejectedDetails, InvalidCodeDetails, ErrorCode, PlanLimitDetails, AssetPlanLimitDetails, PlanRequiredDetails, OrgLockedDetails, } from './errors.js';
50
+ export { apiError, parameterViolation, parameterInvalidDetails, cancelRejectedDetails, invalidCodeDetails, ERROR_CODES, planLimitDetails, assetPlanLimitDetails, planRequiredDetails, orgLockedDetails, fileTooLargeDetails, } from './errors.js';
51
+ export type { ApiError, ParameterViolation, ParameterInvalidDetails, CancelRejectedDetails, InvalidCodeDetails, ErrorCode, PlanLimitDetails, AssetPlanLimitDetails, PlanRequiredDetails, OrgLockedDetails, FileTooLargeDetails, } from './errors.js';
52
52
  export { oauthErrorCode, oauthError, oauthRedirectResponse, oauthCodeTokenRequest, oauthRefreshTokenRequest, oauthTokenRequest, oauthTokenResponse, redirectUri, codeChallengeMethod, oauthAuthorizeQuery, dynamicClientRegistrationRequest, MCP_DCR_MAX_REDIRECT_URIS, dynamicClientRegistrationResponse, authorizationServerMetadata, protectedResourceMetadata, } from './oauth.js';
53
53
  export type { OauthErrorCode, OauthError, OauthRedirectResponse, OauthCodeTokenRequest, OauthRefreshTokenRequest, OauthTokenRequest, OauthTokenResponse, RedirectUri, OauthAuthorizeQuery, DynamicClientRegistrationRequest, DynamicClientRegistrationResponse, AuthorizationServerMetadata, ProtectedResourceMetadata, } from './oauth.js';
54
54
  export { ROUTES, ROUTE_SECTIONS, IN_HANDLER_ROUTES, developerSignInRoutes } from './routes.js';
package/dist/index.js CHANGED
@@ -49,12 +49,14 @@ export { clientLoginRequest, clientRefreshRequest, clientLogoutRequest, clientRe
49
49
  export { clientRobotListItem, clientRobotListResponse } from './client-robots.js';
50
50
  // The per-app identity space.
51
51
  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, appSignInMethods, appTwoFactorPolicy, appHomeUrl, hostedAccent, HOSTED_LOGO_MAX_BYTES, HOSTED_LOGO_TYPES, appHostedPages, putAppAuthRegistrationRequest, putAppAuthSignInRequest, putAppAuthUrlsRequest, putAppAuthMcpRequest, putAppAuthLookRequest, mailTemplateKind, appMailTemplate, appMailTemplateListResponse, putAppMailTemplateRequest, mailTemplatePreviewRequest, mailTemplatePreviewResponse, mailTemplateProblemDetails, mailOutcome, } from './app-users.js';
52
- 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';
52
+ export { assetKind, URDF_ASSET_NAME, asset, urdfCompleteness, assetListResponse, assetsClearResponse, missingAssetQuery, assetSyncRequest, assetSyncResponse, assetSyncState, assetSyncStatus, assetFailure, assetFailureKind, assetStoreRefusedDetails, assetSyncBusyDetails, ROBOT_ASSET_STORE_BYTES, ASSET_FILE_MAX_BYTES, } from './assets.js';
53
53
  export { auditActor, auditEvent, auditQuery, auditListResponse, AUDIT_CSV_COLUMNS, AUDIT_RETENTION_DAYS } from './audit.js';
54
54
  export { alertRowCondition, alertSeverity, alertState, datapointAlertRow, alertListResponse, orgFiringAlertsResponse, orgAlertsQuery, datapointDisplay, putDatapointDisplayRequest, } from './alerts.js';
55
55
  export { apiError, parameterViolation, parameterInvalidDetails, cancelRejectedDetails, invalidCodeDetails, ERROR_CODES,
56
56
  // 2026-10-02 — plans (#103). The plan errors (I-3).
57
- planLimitDetails, assetPlanLimitDetails, planRequiredDetails, orgLockedDetails, } from './errors.js';
57
+ planLimitDetails, assetPlanLimitDetails, planRequiredDetails, orgLockedDetails,
58
+ // 2026-10-03 — the per-file asset limit (#136).
59
+ fileTooLargeDetails, } from './errors.js';
58
60
  export { oauthErrorCode, oauthError, oauthRedirectResponse, oauthCodeTokenRequest, oauthRefreshTokenRequest, oauthTokenRequest, oauthTokenResponse, redirectUri, codeChallengeMethod, oauthAuthorizeQuery, dynamicClientRegistrationRequest, MCP_DCR_MAX_REDIRECT_URIS, dynamicClientRegistrationResponse, authorizationServerMetadata, protectedResourceMetadata, } from './oauth.js';
59
61
  export { ROUTES, ROUTE_SECTIONS, IN_HANDLER_ROUTES, developerSignInRoutes } from './routes.js';
60
62
  // 2026-10-02 — plans (#103). The catalogue (I-1) only: `PLANS` and `ADDONS`
@@ -260,10 +260,10 @@ export type DatapointEvent = z.infer<typeof datapointEvent>;
260
260
  */
261
261
  export declare const liveSessionEndReason: z.ZodEnum<{
262
262
  unknown: "unknown";
263
+ plan_limit: "plan_limit";
263
264
  publish_failed: "publish_failed";
264
265
  robot_offline: "robot_offline";
265
266
  config_changed: "config_changed";
266
- plan_limit: "plan_limit";
267
267
  released_by_peer: "released_by_peer";
268
268
  revoked: "revoked";
269
269
  expired: "expired";
@@ -292,10 +292,10 @@ export declare const liveSessionEvent: z.ZodObject<{
292
292
  state: z.ZodLiteral<"ended">;
293
293
  reason: z.ZodEnum<{
294
294
  unknown: "unknown";
295
+ plan_limit: "plan_limit";
295
296
  publish_failed: "publish_failed";
296
297
  robot_offline: "robot_offline";
297
298
  config_changed: "config_changed";
298
- plan_limit: "plan_limit";
299
299
  released_by_peer: "released_by_peer";
300
300
  revoked: "revoked";
301
301
  expired: "expired";
package/dist/routes.js CHANGED
@@ -2852,16 +2852,18 @@ export const ROUTES = [
2852
2852
  summary: 'Takes one asset file from a robot during a sync.',
2853
2853
  audience: 'internal', auth: 'robot_upload', rateLimited: false, ownerTier: false, status: 201,
2854
2854
  params: [], query: null, request: null, response: asset,
2855
- errors: ['unauthorized', 'rate_limited', 'validation_error', 'not_found', 'quota_exceeded', 'bad_request'], transport: 'http',
2855
+ errors: ['unauthorized', 'rate_limited', 'validation_error', 'not_found', 'file_too_large', 'plan_limit', 'quota_exceeded', 'bad_request'], transport: 'http',
2856
2856
  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 ' +
2857
2857
  'and its announced size — rides in the `x-fleetless-asset-*` headers `ASSET_UPLOAD_HEADERS` names. The credential is a short-lived ' +
2858
2858
  'upload token minted by `POST /api/robots/:id/assets/sync`, verified in a `preParsing` hook so a refusal precedes the work rather than ' +
2859
- 'following it: a `preHandler` would already have buffered the whole file. **Nothing is refused for its own size** — the robot\'s asset ' +
2860
- '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 ' +
2861
- '`409 quota_exceeded` carrying `store_bytes`, `used_bytes` and `size_bytes`, while the sync carries on with the next file. Past that, ' +
2862
- '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 ' +
2863
- 'hook, which is why `rateLimited` is `false`: there is no rate-limiting preHandler registered on this route. The URDF itself is never ' +
2864
- 'refused for the store; only meshes and textures are charged against it.',
2859
+ 'following it: a `preHandler` would already have buffered the whole file. **One file can be at most `ASSET_FILE_MAX_BYTES`**, on every ' +
2860
+ 'plan and for every kind, the URDF included: an announced size over it answers `413 file_too_large` with `max_bytes` and `size_bytes` ' +
2861
+ 'before a byte is buffered; without a truthful size, a body over the limit answers the same code with the bytes that arrived as `size_bytes`, ' +
2862
+ 'or with `size_bytes: null` when it ran past the server\'s body limit and nobody counted the bytes. ' +
2863
+ 'Retrying does not help. A file that fits but finds the store full answers `409 plan_limit` (or `409 quota_exceeded` for an organisation ' +
2864
+ 'still on the beta) carrying `store_bytes`, `used_bytes` and `size_bytes`, while the sync carries on with the next file. Rate limited per ' +
2865
+ 'robot inside that same hook, which is why `rateLimited` is `false`: there is no rate-limiting preHandler registered on this route. The ' +
2866
+ 'URDF itself is never refused for the store; only meshes and textures are charged against it.',
2865
2867
  },
2866
2868
  /* ------------------------------------ realtime and bridge transports */
2867
2869
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fleetless/contracts",
3
- "version": "6.1.0",
3
+ "version": "6.2.0-next.2",
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",