@fleetless/contracts 1.1.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +32 -1
- package/artifacts/constants.json +30 -4
- package/artifacts/openapi.json +667 -98
- package/artifacts/routes.json +97 -6
- package/artifacts/schema/apply-error.schema.json +2 -1
- package/artifacts/schema/asset-list-response.schema.json +77 -12
- package/artifacts/schema/asset-sync-status.schema.json +35 -8
- package/artifacts/schema/asset.schema.json +2 -3
- package/artifacts/schema/assets-clear-response.schema.json +23 -0
- package/artifacts/schema/authorization-server-metadata.schema.json +1 -1
- package/artifacts/schema/bridge-asset-progress.schema.json +14 -8
- package/artifacts/schema/bridge-config-applied.schema.json +2 -1
- package/artifacts/schema/bridge-link-mode.schema.json +36 -0
- package/artifacts/schema/bridge-state.schema.json +6 -1
- package/artifacts/schema/client-robot-list-item.schema.json +6 -1
- package/artifacts/schema/client-robot-list-response.schema.json +6 -1
- package/artifacts/schema/cloud-config.schema.json +90 -5
- package/artifacts/schema/cloud-hello-ok.schema.json +45 -0
- package/artifacts/schema/cloud-ping.schema.json +27 -1
- package/artifacts/schema/config-draft-response.schema.json +90 -5
- package/artifacts/schema/config-state.schema.json +2 -1
- package/artifacts/schema/config-version-response.schema.json +90 -5
- package/artifacts/schema/datapoint-config.schema.json +5 -0
- package/artifacts/schema/datapoint-frame.schema.json +4 -0
- package/artifacts/schema/datapoint-list-response.schema.json +2 -2
- package/artifacts/schema/dynamic-client-registration-request.schema.json +1 -1
- package/artifacts/schema/dynamic-client-registration-response.schema.json +1 -1
- package/artifacts/schema/joint-state-put-request.schema.json +23 -0
- package/artifacts/schema/joint-state-put-response.schema.json +24 -0
- package/artifacts/schema/oauth-token-request.schema.json +79 -41
- package/artifacts/schema/oauth-token-response.schema.json +1 -1
- package/artifacts/schema/org-quota-usage-counts.schema.json +0 -5
- package/artifacts/schema/org-quota-usage.schema.json +1 -12
- package/artifacts/schema/org-quotas.schema.json +1 -7
- package/artifacts/schema/robot-config-doc.schema.json +90 -5
- package/artifacts/schema/robot-deletion-summary.schema.json +2 -1
- package/artifacts/schema/robot-detail-response.schema.json +63 -2
- package/artifacts/schema/robot-list-item.schema.json +15 -1
- package/artifacts/schema/robot-list-response.schema.json +15 -1
- package/artifacts/schema/robot-token-rotate-response.schema.json +15 -0
- package/artifacts/schema-outgoing/bridge-asset-progress.schema.json +14 -8
- package/artifacts/schema-outgoing/bridge-config-applied.schema.json +2 -1
- package/artifacts/schema-outgoing/bridge-link-mode.schema.json +37 -0
- package/artifacts/schema-outgoing/datapoint-frame.schema.json +4 -0
- package/dist/assets.d.ts +85 -50
- package/dist/assets.js +152 -62
- package/dist/audit.d.ts +1 -1
- package/dist/audit.js +1 -1
- package/dist/client-robots.d.ts +2 -0
- package/dist/common.d.ts +10 -0
- package/dist/common.js +16 -1
- package/dist/config.d.ts +69 -1
- package/dist/config.js +86 -6
- package/dist/errors.d.ts +1 -1
- package/dist/errors.js +1 -8
- package/dist/index.d.ts +10 -10
- package/dist/index.js +5 -5
- package/dist/oauth.d.ts +34 -19
- package/dist/oauth.js +39 -24
- package/dist/protocol.d.ts +150 -71
- package/dist/protocol.js +144 -87
- package/dist/rest.d.ts +137 -35
- package/dist/rest.js +98 -66
- package/dist/routes.js +68 -19
- package/package.json +1 -1
- package/artifacts/schema/bridge-pressure.schema.json +0 -292
package/dist/rest.js
CHANGED
|
@@ -38,6 +38,42 @@ export const createRobotResponse = z.object({
|
|
|
38
38
|
robot,
|
|
39
39
|
token: robotToken,
|
|
40
40
|
});
|
|
41
|
+
/**
|
|
42
|
+
* What a rotation hands back: the new token, once.
|
|
43
|
+
*
|
|
44
|
+
* The same shape as the creation response minus the robot, because nothing
|
|
45
|
+
* about the robot changed — only its credential. `createRobotResponse`'s own
|
|
46
|
+
* rule applies unchanged: the cloud stores a hash, so this is the only moment
|
|
47
|
+
* the raw token exists outside the caller's hands.
|
|
48
|
+
*/
|
|
49
|
+
export const robotTokenRotateResponse = z.object({
|
|
50
|
+
token: robotToken.meta({
|
|
51
|
+
description: 'The robot\'s new bridge token. Returned exactly once; the previous token stops working at the bridge\'s next hello.',
|
|
52
|
+
}),
|
|
53
|
+
});
|
|
54
|
+
/**
|
|
55
|
+
* Which datapoint drives the joints of this robot's URDF, or none.
|
|
56
|
+
*
|
|
57
|
+
* `null` is the clearing value, which is why `slug` is required rather than
|
|
58
|
+
* optional: an absent field and a cleared mapping would be the same request
|
|
59
|
+
* and mean different things, and the one a client sends by accident is the
|
|
60
|
+
* first.
|
|
61
|
+
*
|
|
62
|
+
* The cloud refuses a slug that is not a whole-message
|
|
63
|
+
* `sensor_msgs/msg/JointState` datapoint of the **published** document — a
|
|
64
|
+
* mapping that may point anywhere is a viewer animating a battery reading.
|
|
65
|
+
*/
|
|
66
|
+
export const jointStatePutRequest = z.object({
|
|
67
|
+
slug: slug.nullable().meta({
|
|
68
|
+
description: 'The datapoint to read joint positions from, or `null` to choose none. It must name a whole-message `sensor_msgs/msg/JointState` datapoint of the published configuration; anything else is a `validation_error` naming the rule.',
|
|
69
|
+
}),
|
|
70
|
+
});
|
|
71
|
+
/** The mapping as it now stands — the same field `GET /api/robots/:id/assets` reports. */
|
|
72
|
+
export const jointStatePutResponse = z.object({
|
|
73
|
+
joint_state_slug: slug.nullable().meta({
|
|
74
|
+
description: 'The stored mapping after the call, `null` when none is chosen. The same value `assetListResponse.joint_state_slug` carries.',
|
|
75
|
+
}),
|
|
76
|
+
});
|
|
41
77
|
/**
|
|
42
78
|
* How many things a robot exposes, per kind.
|
|
43
79
|
*
|
|
@@ -47,18 +83,17 @@ export const createRobotResponse = z.object({
|
|
|
47
83
|
*
|
|
48
84
|
* **Counted from the published configuration, and excluding the built-ins.**
|
|
49
85
|
* `GET /api/robots/:id/exposures` answers *which* slugs and prepends the
|
|
50
|
-
*
|
|
51
|
-
* `
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
* prevent.
|
|
86
|
+
* built-in datapoints — `bridge_state` and `robot_details` — as
|
|
87
|
+
* `builtin: true`; this answers *how many* and counts only what somebody
|
|
88
|
+
* configured. So a robot with an empty published config reports
|
|
89
|
+
* `datapoints: 0` here and two entries there. That is intentional, and it is
|
|
90
|
+
* written on both sides so the disagreement is never mistaken for a bug.
|
|
91
|
+
*
|
|
92
|
+
* The number was "three" while `bridge_pressure` existed and is "two" since
|
|
93
|
+
* protocol 3 dropped it; the cloud builds that prefix from its own built-in
|
|
94
|
+
* set rather than a literal, so the next built-in moves this count again.
|
|
95
|
+
* Read the count off that set, not off this sentence, before filing the bug
|
|
96
|
+
* this comment exists to prevent.
|
|
62
97
|
*/
|
|
63
98
|
export const exposureCounts = z.object({
|
|
64
99
|
datapoints: z.number().int().nonnegative(),
|
|
@@ -67,12 +102,22 @@ export const exposureCounts = z.object({
|
|
|
67
102
|
publishers: z.number().int().nonnegative(),
|
|
68
103
|
cameras: z.number().int().nonnegative(),
|
|
69
104
|
});
|
|
105
|
+
/**
|
|
106
|
+
* Where a robot's bridge stands against the protocol window.
|
|
107
|
+
* `refused`: its last hello was refused for its version — it is offline
|
|
108
|
+
* until upgraded. Computed by the cloud from `protocol_version` and
|
|
109
|
+
* `last_hello_error`, never stored.
|
|
110
|
+
*/
|
|
111
|
+
export const protocolStatusValue = z.enum(['current', 'deprecated', 'refused']);
|
|
70
112
|
/** A robot as listed, with its current built-in `bridge_state`. */
|
|
71
113
|
export const robotListItem = z.object({
|
|
72
114
|
...robot.shape,
|
|
73
115
|
bridge_state: bridgeState,
|
|
74
116
|
/** Required, not optional: "we did not look" and "it exposes nothing" must not render the same. */
|
|
75
117
|
exposes: exposureCounts,
|
|
118
|
+
protocol_status: protocolStatusValue.optional().meta({
|
|
119
|
+
description: 'Where this robot\'s bridge stands against the protocol window: `current`, `deprecated` (still served, sunset date on the detail), or `refused` (its last hello was refused for its version; offline until upgraded). Absent from a cloud older than 0.21.0; read absence as `current`.',
|
|
120
|
+
}),
|
|
76
121
|
});
|
|
77
122
|
export const robotListResponse = z.object({
|
|
78
123
|
robots: z.array(robotListItem),
|
|
@@ -103,6 +148,18 @@ export const datapointValue = z.object({
|
|
|
103
148
|
export const robotDetailResponse = z.object({
|
|
104
149
|
...robotListItem.shape,
|
|
105
150
|
bridge_version: z.string().min(1).nullable(),
|
|
151
|
+
protocol_version: z.number().int().positive().nullable().optional().meta({
|
|
152
|
+
description: 'The protocol version the bridge announced in its last accepted hello; `null` before the first. Absent from a cloud older than 0.21.0.',
|
|
153
|
+
}),
|
|
154
|
+
protocol: z
|
|
155
|
+
.object({
|
|
156
|
+
status: protocolStatusValue.meta({ description: 'Same values as `protocol_status`.' }),
|
|
157
|
+
sunset_at: z.iso.date().nullable().meta({
|
|
158
|
+
description: 'ISO date the announced version stops being served; `null` when current or unknown.',
|
|
159
|
+
}),
|
|
160
|
+
})
|
|
161
|
+
.optional()
|
|
162
|
+
.meta({ description: 'The window verdict for `protocol_version`.' }),
|
|
106
163
|
/**
|
|
107
164
|
* Cleared (set back to null) by the next successful hello from this
|
|
108
165
|
* robot's bridge — a warning that outlives the condition it warns
|
|
@@ -232,7 +289,7 @@ export const fetchTypesResponse = z.object({
|
|
|
232
289
|
export const datapointDescriptor = z.object({
|
|
233
290
|
slug: slug.meta({ description: 'The name a client reads this datapoint by.' }),
|
|
234
291
|
builtin: z.boolean().meta({
|
|
235
|
-
description: '`true` for the datapoints every robot has — `bridge_state
|
|
292
|
+
description: '`true` for the datapoints every robot has — `bridge_state` and `robot_details` — and `false` for everything the published configuration adds.',
|
|
236
293
|
}),
|
|
237
294
|
unit: z.string().nullable().meta({
|
|
238
295
|
description: 'The unit the value carries **after** any scale and offset, shown beside the number so nobody has to guess whether `15` means percent, volts or minutes. `null` when the configuration names none.',
|
|
@@ -250,7 +307,7 @@ export const datapointDescriptor = z.object({
|
|
|
250
307
|
});
|
|
251
308
|
export const datapointListResponse = z.object({
|
|
252
309
|
datapoints: z.array(datapointDescriptor).meta({
|
|
253
|
-
description: 'Everything a client may read on this robot: the
|
|
310
|
+
description: 'Everything a client may read on this robot: the two built-ins, plus every datapoint the published configuration exposes and the caller\'s role grants.',
|
|
254
311
|
}),
|
|
255
312
|
});
|
|
256
313
|
/**
|
|
@@ -801,22 +858,21 @@ export const ASSET_UPLOAD_HEADERS = {
|
|
|
801
858
|
nameEncoded: 'x-fleetless-asset-name-encoded',
|
|
802
859
|
syncId: 'x-fleetless-sync-id',
|
|
803
860
|
/**
|
|
804
|
-
* **The announced size, and it is what makes
|
|
805
|
-
* all.**
|
|
861
|
+
* **The announced size, and it is what makes a structured store refusal
|
|
862
|
+
* reachable at all.**
|
|
806
863
|
*
|
|
807
864
|
* A server-side body limit is applied by the content-type parser, before the
|
|
808
|
-
* handler runs, so an
|
|
809
|
-
* `413` carrying
|
|
810
|
-
*
|
|
865
|
+
* handler runs, so an upload with no room left can only be refused with a
|
|
866
|
+
* bare `413` carrying none of the three numbers — and the refusal
|
|
867
|
+
* `assetStoreRefusedDetails` describes would have no producer.
|
|
811
868
|
*
|
|
812
|
-
* With the size announced in a header the
|
|
813
|
-
*
|
|
814
|
-
* a
|
|
815
|
-
* into memory.
|
|
869
|
+
* With the size announced in a header the cloud can check `used + size`
|
|
870
|
+
* against `ROBOT_ASSET_STORE_BYTES` where it can still say something: before
|
|
871
|
+
* a byte is buffered, with all three numbers.
|
|
816
872
|
*
|
|
817
873
|
* The header is an **announcement, not a proof**: a sender can lie. The
|
|
818
|
-
*
|
|
819
|
-
* only makes the refusal answerable.
|
|
874
|
+
* store still applies to the bytes that arrive — this does not replace
|
|
875
|
+
* enforcement, it only makes the refusal answerable.
|
|
820
876
|
*/
|
|
821
877
|
size: 'x-fleetless-asset-size',
|
|
822
878
|
};
|
|
@@ -1233,22 +1289,25 @@ export const robotDeletionSummary = z.object({
|
|
|
1233
1289
|
bytes_freed: z.number().int().nonnegative(),
|
|
1234
1290
|
cameras: z.array(slug),
|
|
1235
1291
|
/**
|
|
1236
|
-
* Assets destroyed with the robot, and **`asset_bytes_freed` is what
|
|
1237
|
-
*
|
|
1292
|
+
* Assets destroyed with the robot, and **`asset_bytes_freed` is what the
|
|
1293
|
+
* robot's own store gives back** — every distinct mesh or texture blob it
|
|
1294
|
+
* holds, counted once, URDF excluded.
|
|
1238
1295
|
*
|
|
1239
|
-
* Storage is content-addressed,
|
|
1240
|
-
*
|
|
1241
|
-
*
|
|
1242
|
-
*
|
|
1243
|
-
*
|
|
1244
|
-
*
|
|
1245
|
-
*
|
|
1296
|
+
* Storage is content-addressed, but the store and its 1 GB ceiling are now
|
|
1297
|
+
* per robot: a blob another robot also references stays in the object
|
|
1298
|
+
* store but is still credited here, because each robot's counter carries
|
|
1299
|
+
* it regardless of what else points at the same bytes. Same reasoning that
|
|
1300
|
+
* keeps `cameras` out of `slug_count`: this summary is read aloud to a
|
|
1301
|
+
* human, and a number that is nearly right is worse here than an absent
|
|
1302
|
+
* one.
|
|
1246
1303
|
*
|
|
1247
1304
|
* `asset_count` is the plain count of the robot's asset rows, all of which
|
|
1248
1305
|
* do go away.
|
|
1249
1306
|
*/
|
|
1250
1307
|
asset_count: z.number().int().nonnegative(),
|
|
1251
|
-
asset_bytes_freed: z.number().int().nonnegative()
|
|
1308
|
+
asset_bytes_freed: z.number().int().nonnegative().meta({
|
|
1309
|
+
description: 'What the robot\'s store gives back: every distinct mesh or texture blob it holds, counted once, URDF excluded; a blob another robot also references stays in the object store but is still credited here, because each robot\'s counter carries it.',
|
|
1310
|
+
}),
|
|
1252
1311
|
/**
|
|
1253
1312
|
* How many rows of run history go with the robot — every recorded
|
|
1254
1313
|
* invocation of one of its actions or services, up to
|
|
@@ -1460,31 +1519,6 @@ export const orgQuotas = z.object({
|
|
|
1460
1519
|
max_retention_bytes: z.number().int().nonnegative(),
|
|
1461
1520
|
max_retention_writes_per_minute: z.number().int().nonnegative(),
|
|
1462
1521
|
max_realtime_connections: z.number().int().positive(),
|
|
1463
|
-
/**
|
|
1464
|
-
* Asset storage — **its own dial, not part of `max_retention_bytes`.** A
|
|
1465
|
-
* sync grows storage in jumps and time series grow steadily; one dial would
|
|
1466
|
-
* let the first crowd out the second, and the org that hit its limit would be
|
|
1467
|
-
* told to look at the wrong thing.
|
|
1468
|
-
*
|
|
1469
|
-
* **Counted per distinct blob *this org references* — not per asset row, and
|
|
1470
|
-
* not per object the platform stores on its behalf.** The two readings are
|
|
1471
|
-
* indistinguishable from the number alone and a customer is entitled to know
|
|
1472
|
-
* which one they are being charged for.
|
|
1473
|
-
*
|
|
1474
|
-
* Within an org, sharing is free: two robots referencing the same mesh cost
|
|
1475
|
-
* one copy, which is what dedup means to a customer, and anything else
|
|
1476
|
-
* charges an org twice for a fleet of identical robots — the normal case.
|
|
1477
|
-
*
|
|
1478
|
-
* **Across orgs, sharing is not free.** Storage stays globally
|
|
1479
|
-
* content-addressed (one object per sha256; that efficiency is real), but
|
|
1480
|
-
* accounting is per-org: an org is charged for each distinct blob it
|
|
1481
|
-
* references and credited when its own last reference goes, whether or not
|
|
1482
|
-
* the blob survives for somebody else. Global refcounting would make the
|
|
1483
|
-
* first org to sync a blob pay for it forever while every later org stored it
|
|
1484
|
-
* free — a quota evadable by anyone whose mesh someone else had already
|
|
1485
|
-
* uploaded, and an org's own number would depend on who got there first.
|
|
1486
|
-
*/
|
|
1487
|
-
max_asset_storage_bytes: z.number().int().nonnegative(),
|
|
1488
1522
|
});
|
|
1489
1523
|
/**
|
|
1490
1524
|
* What an org is **actually using**, per quota.
|
|
@@ -1506,7 +1540,6 @@ export const orgQuotaUsageCounts = z.object({
|
|
|
1506
1540
|
max_apps: z.number().int().nonnegative(),
|
|
1507
1541
|
max_end_users: z.number().int().nonnegative(),
|
|
1508
1542
|
max_retention_bytes: z.number().int().nonnegative(),
|
|
1509
|
-
max_asset_storage_bytes: z.number().int().nonnegative(),
|
|
1510
1543
|
max_retention_writes_per_minute: z.number().int().nonnegative(),
|
|
1511
1544
|
max_realtime_connections: z.number().int().nonnegative(),
|
|
1512
1545
|
}).partial();
|
|
@@ -1685,11 +1718,10 @@ export const USAGE_WINDOW_MAX_DAYS = 366;
|
|
|
1685
1718
|
/**
|
|
1686
1719
|
* The five things the meter records.
|
|
1687
1720
|
*
|
|
1688
|
-
* Storage is two metrics and not one summed byte count
|
|
1689
|
-
*
|
|
1690
|
-
*
|
|
1691
|
-
*
|
|
1692
|
-
* the quota.
|
|
1721
|
+
* Storage is two metrics and not one summed byte count: a sync grows storage
|
|
1722
|
+
* in jumps and time series grow steadily, and one number would let the first
|
|
1723
|
+
* crowd out the second on the invoice. The org that outgrew its bill would be
|
|
1724
|
+
* told to look at the wrong thing.
|
|
1693
1725
|
*/
|
|
1694
1726
|
export const usageMetric = z.enum(['api_calls', 'live_session_ms', 'retention_bytes', 'asset_bytes', 'robot_online_ms']);
|
|
1695
1727
|
/**
|
package/dist/routes.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
2
|
import { appListResponse, 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';
|
|
@@ -10,7 +10,7 @@ import { acceptTeamInviteRequest, authMeResponse, createTeamInviteRequest, fleet
|
|
|
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' },
|
|
@@ -948,8 +948,8 @@ export const ROUTES = [
|
|
|
948
948
|
'caller earned. The shape is deliberately **not** strict, which is the schema agreeing with §3.1 rather than a gap in it — a conforming ' +
|
|
949
949
|
'client sends `client_uri`, `logo_uri` and `software_id`, and both the schema and the server ignore them. `client_name` and ' +
|
|
950
950
|
'`redirect_uris` are the two fields read; `grant_types`, `response_types` and `scope` are accepted and ignored. What comes back is what ' +
|
|
951
|
-
'was actually granted, which §3.2.1 allows a server to substitute — this authorization server
|
|
952
|
-
'
|
|
951
|
+
'was actually granted, which §3.2.1 allows a server to substitute — this authorization server grants `authorization_code` and ' +
|
|
952
|
+
'`refresh_token` to every registration. The registration carries a TTL. Refusals ' +
|
|
953
953
|
'are `oauthError`; the rate limiter answers `apiError`.',
|
|
954
954
|
},
|
|
955
955
|
{
|
|
@@ -1023,10 +1023,11 @@ export const ROUTES = [
|
|
|
1023
1023
|
audience: 'client', auth: 'none', rateLimited: false, ownerTier: false, status: 200,
|
|
1024
1024
|
params: [], query: null, request: oauthTokenRequest, response: oauthTokenResponse,
|
|
1025
1025
|
errors: [], transport: 'http',
|
|
1026
|
-
notes: '
|
|
1027
|
-
'
|
|
1028
|
-
'
|
|
1029
|
-
'single-use, PKCE-verified, and its
|
|
1026
|
+
notes: '`authorization_code` mints an `mcp_session` access token bound to the central resource and a refresh token; `refresh_token` rotates that pair, ' +
|
|
1027
|
+
'and the presented refresh token is consumed — a second presentation revokes the session, as on `/api/auth/refresh`. The refresh token lives ninety days ' +
|
|
1028
|
+
'from its last use and is bound to the `client_id` it was issued to. A refresh re-reads the Fleetless user, so a removed account cannot refresh. ' +
|
|
1029
|
+
'Refusals are RFC 6749 §5.2\'s `oauthError`, so this route emits none of the codes in this reference. The code is single-use, PKCE-verified, and its ' +
|
|
1030
|
+
'`resource` must match the audience it was authorized for; a `resource` on a refresh must match the session\'s audience, and is checked before the token is consumed.',
|
|
1030
1031
|
},
|
|
1031
1032
|
/* ------------------------------- developer auth (the console\'s OAuth portal) */
|
|
1032
1033
|
{
|
|
@@ -1248,8 +1249,8 @@ export const ROUTES = [
|
|
|
1248
1249
|
'accepts rather than what it parses, for the reason that row gives: §3.2.2 needs two distinguishable refusals and one `safeParse` ' +
|
|
1249
1250
|
'failure offers one. `client_name` and `redirect_uris` are read; `grant_types`, `response_types` and `scope` are accepted and ignored, ' +
|
|
1250
1251
|
'and what comes back is what was actually ' +
|
|
1251
|
-
'granted, which §3.2.1 allows —
|
|
1252
|
-
'
|
|
1252
|
+
'granted, which §3.2.1 allows — this authorization server grants `authorization_code` and `refresh_token` to every registration. ' +
|
|
1253
|
+
'The registration carries a TTL. \n\n**The registration is scoped to this app.** A `client_id` minted here ' +
|
|
1253
1254
|
'authorizes at this app\'s endpoint and nowhere else, so a client registered against one app cannot walk into another\'s authorize with ' +
|
|
1254
1255
|
'it, and a developer who switches MCP off is not left with strangers\' registrations valid somewhere adjacent. \n\nRefusals are ' +
|
|
1255
1256
|
'`oauthError`; the rate limiter and `404 not_found` answer `apiError`. That `404` covers an unknown identifier **and** an app with the ' +
|
|
@@ -1288,9 +1289,8 @@ export const ROUTES = [
|
|
|
1288
1289
|
audience: 'client', auth: 'none', rateLimited: false, ownerTier: false, status: 200,
|
|
1289
1290
|
params: [APP_IDENTIFIER], query: null, request: oauthTokenRequest, response: oauthTokenResponse,
|
|
1290
1291
|
errors: [], transport: 'http',
|
|
1291
|
-
notes: '
|
|
1292
|
-
'
|
|
1293
|
-
'optional and stays empty. **The `aud` is this app\'s endpoint URL on the canonical public base**, and the code\'s `resource` must match ' +
|
|
1292
|
+
notes: '`authorization_code`, PKCE-verified and single-use, and `refresh_token`, which rotates the pair the exchange minted; the refresh token lives ninety days from its last use, is bound to its client and to this app, and a refresh re-reads the app user\'s status and their standing consent to the client, so a block or a withdrawn consent ends the session at its next refresh at the latest. ' +
|
|
1293
|
+
'**The `aud` is this app\'s endpoint URL on the canonical public base**, and the code\'s `resource` must match ' +
|
|
1294
1294
|
'it — that is the whole of what stops a token minted for one app being spent at another\'s endpoint. \n\n**Every refusal is RFC 6749 ' +
|
|
1295
1295
|
'§5.2\'s `oauthError`, so this route emits none of the codes in this reference — including the ones about the app.** An unknown ' +
|
|
1296
1296
|
'identifier and a switched-off app are `invalid_client` here, not the `404` and `403` the authorize route beside it answers. The ' +
|
|
@@ -1664,6 +1664,40 @@ export const ROUTES = [
|
|
|
1664
1664
|
'and the event carries no `details`, because the one interesting value here is the token. `max_robots` is checked before anything is ' +
|
|
1665
1665
|
'created, which is only safe because robot deletion exists.',
|
|
1666
1666
|
},
|
|
1667
|
+
{
|
|
1668
|
+
method: 'POST', path: '/api/robots/:id/token/rotate', section: 'robots',
|
|
1669
|
+
summary: 'Mints a new bridge token for the robot and invalidates the old one.',
|
|
1670
|
+
audience: 'developer', auth: 'developer', rateLimited: false, ownerTier: true, status: 201,
|
|
1671
|
+
params: [{ name: 'id', description: 'The robot\'s uuid, as returned by `POST /api/robots` or listed by `GET /api/robots`.' }],
|
|
1672
|
+
query: null, request: null, response: robotTokenRotateResponse,
|
|
1673
|
+
errors: [...DEVELOPER_GUARD, 'tier_required', 'invalid_uuid', 'not_found'], transport: 'http',
|
|
1674
|
+
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 ' +
|
|
1675
|
+
'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 ' +
|
|
1676
|
+
'stores a hash — so a caller who loses it rotates again. Audited as `robot.token_rotated`, with no `details`: the one interesting value ' +
|
|
1677
|
+
'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 ' +
|
|
1678
|
+
'the cloud closes that socket with `CLOSE_TOKEN_ROTATED` rather than leaving a bridge speaking on a credential nothing would accept ' +
|
|
1679
|
+
'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 ' +
|
|
1680
|
+
'the same way. **The robot is offline until somebody puts the new token on it** — this is a deliberate interruption, not a background ' +
|
|
1681
|
+
'rekey, and a fleet cannot be rotated without a visit to each robot.',
|
|
1682
|
+
},
|
|
1683
|
+
{
|
|
1684
|
+
method: 'PUT', path: '/api/robots/:id/urdf/joint-state', section: 'robots',
|
|
1685
|
+
summary: 'Chooses the datapoint whose joint positions move the robot\'s URDF, or clears it.',
|
|
1686
|
+
audience: 'developer', auth: 'developer', rateLimited: false, ownerTier: false, status: 200,
|
|
1687
|
+
params: [{ name: 'id', description: 'The robot\'s uuid, as returned by `POST /api/robots` or listed by `GET /api/robots`.' }],
|
|
1688
|
+
query: null, request: jointStatePutRequest, response: jointStatePutResponse,
|
|
1689
|
+
errors: [...DEVELOPER_GUARD, 'invalid_uuid', 'not_found', 'validation_error'], transport: 'http',
|
|
1690
|
+
notes: '**What qualifies**: a datapoint of the **published** configuration whose ROS type is `sensor_msgs/msg/JointState` and which carries no ' +
|
|
1691
|
+
'`field` — the whole message, because positions and names arrive together and a single extracted field is half of a pose. Anything else ' +
|
|
1692
|
+
'is a `validation_error` naming that rule rather than a stored mapping that renders a battery reading as a robot. `{ "slug": null }` ' +
|
|
1693
|
+
'clears it, which is why the field is required and nullable rather than optional. \n\n**The mapping cannot outlive what it points at.** ' +
|
|
1694
|
+
'Every successful publish re-checks it against the new document and clears it when it no longer qualifies, recording ' +
|
|
1695
|
+
'`robot.joint_state_cleared` with the version that did it; a slug rename rewrites it like every other reference the editor already ' +
|
|
1696
|
+
'rewrites; deleting the robot takes it along. Every write through this route — a slug or `null` — is on the record too, as ' +
|
|
1697
|
+
'`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 ' +
|
|
1698
|
+
'value reads back on `GET /api/robots/:id/assets` as `joint_state_slug`, so a renderer fetches the URDF, the meshes and the mapping ' +
|
|
1699
|
+
'from one place.',
|
|
1700
|
+
},
|
|
1667
1701
|
{
|
|
1668
1702
|
method: 'GET', path: '/api/robots', section: 'robots',
|
|
1669
1703
|
summary: "Lists the org's robots with their connection state and exposure counts.",
|
|
@@ -2233,6 +2267,20 @@ export const ROUTES = [
|
|
|
2233
2267
|
notes: 'Developer sessions only, like starting a sync: the guard admits three caller kinds and the handler answers `401 unauthorized` to the ' +
|
|
2234
2268
|
'other two. A sync belonging to another robot reads exactly like one that never existed, which is why the robot is resolved first.',
|
|
2235
2269
|
},
|
|
2270
|
+
{
|
|
2271
|
+
method: 'DELETE', path: '/api/robots/:id/assets', section: 'assets',
|
|
2272
|
+
summary: "Empties a robot's asset store: every URDF, mesh and texture, gone at once.",
|
|
2273
|
+
audience: 'client', auth: 'developer_or_client', rateLimited: false, ownerTier: true, status: 200,
|
|
2274
|
+
params: [{ name: 'id', description: 'The robot\'s uuid, as returned by `POST /api/robots` or listed by `GET /api/robots`.' }],
|
|
2275
|
+
query: null, request: null, response: assetsClearResponse,
|
|
2276
|
+
errors: [...CLIENT_GUARD, 'tier_required', 'invalid_uuid', 'not_found', 'busy'], transport: 'http',
|
|
2277
|
+
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 ' +
|
|
2278
|
+
'is exempt from the gate, reconcile after a sync already frees what the new URDF stopped referencing, and this route lets an Owner clear ' +
|
|
2279
|
+
'the robot outright. Owner tier, unconditionally, like starting a sync. Removes every asset of the robot and resets its store to `0`; ' +
|
|
2280
|
+
"the next sync fills it again. It does not touch the bridge's availability report — `urdf_available` still answers from the connected " +
|
|
2281
|
+
'robot, unrelated to what this cloud happens to have stored. A clear while a sync is running is `409 busy` naming that sync\'s ' +
|
|
2282
|
+
'details, the same refusal starting a second sync gets, because deleting under a running upload would leave the store counter wrong.',
|
|
2283
|
+
},
|
|
2236
2284
|
/* ------------------------------------------------ org (quotas and fleet reads) */
|
|
2237
2285
|
{
|
|
2238
2286
|
method: 'GET', path: '/api/org/quotas', section: 'org',
|
|
@@ -2306,15 +2354,16 @@ export const ROUTES = [
|
|
|
2306
2354
|
summary: 'Takes one asset file from a robot during a sync.',
|
|
2307
2355
|
audience: 'internal', auth: 'robot_upload', rateLimited: false, ownerTier: false, status: 201,
|
|
2308
2356
|
params: [], query: null, request: null, response: asset,
|
|
2309
|
-
errors: ['unauthorized', 'rate_limited', '
|
|
2357
|
+
errors: ['unauthorized', 'rate_limited', 'validation_error', 'not_found', 'quota_exceeded', 'bad_request'], transport: 'http',
|
|
2310
2358
|
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
2359
|
'and its announced size — rides in the `x-fleetless-asset-*` headers `ASSET_UPLOAD_HEADERS` names. The credential is a short-lived ' +
|
|
2312
2360
|
'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.
|
|
2314
|
-
'
|
|
2315
|
-
'
|
|
2316
|
-
'
|
|
2317
|
-
'registered on this route.'
|
|
2361
|
+
'following it: a `preHandler` would already have buffered the whole file. **Nothing is refused for its own size** — the robot\'s asset ' +
|
|
2362
|
+
'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 ' +
|
|
2363
|
+
'`409 quota_exceeded` carrying `store_bytes`, `used_bytes` and `size_bytes`, while the sync carries on with the next file. Past that, ' +
|
|
2364
|
+
'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 ' +
|
|
2365
|
+
'hook, which is why `rateLimited` is `false`: there is no rate-limiting preHandler registered on this route. The URDF itself is never ' +
|
|
2366
|
+
'refused for the store; only meshes and textures are charged against it.',
|
|
2318
2367
|
},
|
|
2319
2368
|
/* ------------------------------------ realtime and bridge transports */
|
|
2320
2369
|
{
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fleetless/contracts",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.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",
|