@fleetless/contracts 6.1.0 → 6.2.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.
package/CHANGELOG.md CHANGED
@@ -9,6 +9,21 @@ 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`, which is
17
+ `null` when no size was announced and the body limit stopped the upload.
18
+ `ASSET_FILE_MAX_BYTES` is in `constants.json`. `plan_limit` and
19
+ `quota_exceeded` on an asset upload now mean only that the store is full.
20
+
21
+ ### Changed
22
+
23
+ - **`POST /api/bridge/assets`** lists `file_too_large` and `plan_limit` among
24
+ its errors, and its notes describe the per-file limit instead of a bare
25
+ `413`.
26
+
12
27
  ## [6.1.0] — 2026-10-03
13
28
 
14
29
  ### 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, and a body over the server's body limit without a truthful size answers the same code with `size_bytes: null`. 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 announced size, in bytes; `null` when no size was announced and the body limit stopped the upload."
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 by the server's body limit when
207
+ * it was not. 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 by the server's body limit when
262
+ * it was not. 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.
package/dist/errors.d.ts CHANGED
@@ -209,10 +209,24 @@ 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 announced size; it is `null` only when the sender announced none (or a
218
+ * false one) and the server's body limit stopped the upload, because then
219
+ * nobody counted the bytes.
220
+ */
221
+ export declare const fileTooLargeDetails: z.ZodObject<{
222
+ max_bytes: z.ZodNumber;
223
+ size_bytes: z.ZodNullable<z.ZodNumber>;
224
+ }, z.core.$strip>;
225
+ export type FileTooLargeDetails = z.infer<typeof fileTooLargeDetails>;
212
226
  /**
213
227
  * The codes in use today. The wire deliberately allows any string — this
214
228
  * list is the shared vocabulary, not a closed set, so a new refusal never
215
229
  * needs a contracts release before it can be reported honestly.
216
230
  */
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"];
231
+ 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
232
  export type ErrorCode = (typeof ERROR_CODES)[number];
package/dist/errors.js CHANGED
@@ -135,6 +135,23 @@ 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 announced size; it is `null` only when the sender announced none (or a
144
+ * false one) and the server's body limit stopped the upload, because then
145
+ * nobody counted the bytes.
146
+ */
147
+ export const fileTooLargeDetails = z.object({
148
+ max_bytes: z.number().int().positive().meta({
149
+ description: 'The most one asset file can be, in bytes: `ASSET_FILE_MAX_BYTES`, the same on every plan.',
150
+ }),
151
+ size_bytes: z.number().int().positive().nullable().meta({
152
+ description: 'The refused file\'s announced size, in bytes; `null` when no size was announced and the body limit stopped the upload.',
153
+ }),
154
+ });
138
155
  /**
139
156
  * The codes in use today. The wire deliberately allows any string — this
140
157
  * list is the shared vocabulary, not a closed set, so a new refusal never
@@ -995,4 +1012,13 @@ export const ERROR_CODES = [
995
1012
  * payment, or the platform's own move off the beta.
996
1013
  */
997
1014
  'org_locked',
1015
+ // 2026-10-03 — the per-file asset limit (#136).
1016
+ /**
1017
+ * `413`: one asset file is larger than `ASSET_FILE_MAX_BYTES`. Refused on
1018
+ * the announced size before the body is read, for every kind including the
1019
+ * URDF, or by the body limit when no size was announced. `details` is
1020
+ * `fileTooLargeDetails`. Not a store or plan limit, on any plan; retrying
1021
+ * does not help, only a smaller file does.
1022
+ */
1023
+ 'file_too_large',
998
1024
  ];
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`
package/dist/routes.js CHANGED
@@ -2852,16 +2852,17 @@ 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, and a body over the server\'s body limit without a truthful size answers the same code with `size_bytes: null`. ' +
2862
+ 'Retrying does not help. A file that fits but finds the store full answers `409 plan_limit` (or `409 quota_exceeded` for an organisation ' +
2863
+ 'still on the beta) carrying `store_bytes`, `used_bytes` and `size_bytes`, while the sync carries on with the next file. Rate limited per ' +
2864
+ 'robot inside that same hook, which is why `rateLimited` is `false`: there is no rate-limiting preHandler registered on this route. The ' +
2865
+ 'URDF itself is never refused for the store; only meshes and textures are charged against it.',
2865
2866
  },
2866
2867
  /* ------------------------------------ realtime and bridge transports */
2867
2868
  {
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.1",
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",