@fleetless/contracts 1.0.0 → 1.0.3
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 +97 -2
- package/CODE_OF_CONDUCT.md +83 -0
- package/CONTRIBUTING.md +136 -0
- package/README.md +49 -13
- package/SECURITY.md +55 -0
- package/artifacts/openapi.json +11 -11
- package/artifacts/routes.json +12 -12
- package/artifacts/schema/create-app-oidc-provider-request.schema.json +1 -1
- package/dist/alerts.d.ts +23 -28
- package/dist/alerts.js +23 -29
- package/dist/app-users.d.ts +18 -19
- package/dist/app-users.js +18 -20
- package/dist/apps.d.ts +21 -25
- package/dist/apps.js +42 -52
- package/dist/assets.d.ts +70 -132
- package/dist/assets.js +130 -223
- package/dist/audit.d.ts +14 -15
- package/dist/audit.js +28 -55
- package/dist/client-auth.d.ts +9 -9
- package/dist/client-auth.js +8 -9
- package/dist/common.d.ts +29 -37
- package/dist/common.js +28 -37
- package/dist/config-issues.d.ts +23 -25
- package/dist/config-issues.js +17 -17
- package/dist/config.d.ts +37 -44
- package/dist/config.js +145 -187
- package/dist/errors.d.ts +4 -3
- package/dist/errors.js +83 -116
- package/dist/identity.d.ts +24 -27
- package/dist/identity.js +23 -27
- package/dist/index.d.ts +4 -4
- package/dist/index.js +14 -15
- package/dist/introspection.d.ts +7 -6
- package/dist/introspection.js +6 -6
- package/dist/jobs.d.ts +16 -16
- package/dist/jobs.js +24 -29
- package/dist/mcp.d.ts +14 -15
- package/dist/mcp.js +12 -14
- package/dist/oauth.d.ts +21 -27
- package/dist/oauth.js +33 -43
- package/dist/protocol.d.ts +51 -62
- package/dist/protocol.js +107 -139
- package/dist/realtime.d.ts +53 -68
- package/dist/realtime.js +78 -104
- package/dist/rest.d.ts +183 -244
- package/dist/rest.js +305 -399
- package/dist/routes.d.ts +4 -3
- package/dist/routes.js +33 -32
- package/package.json +12 -7
package/dist/audit.js
CHANGED
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import { wireSeqCursor, wireTimestampMs } from './common.js';
|
|
4
4
|
/**
|
|
5
|
-
|
|
6
|
-
*
|
|
5
|
+
/**
|
|
6
|
+
* Audit. Every state-changing interaction is recorded and **every entry carries
|
|
7
|
+
* its actor — never anonymous**. Reads are not audited.
|
|
7
8
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* 2026-08-10) — same shape of work as the history API.
|
|
9
|
+
* What is written: logins, failed logins, user management, configuration
|
|
10
|
+
* publishes, bridge connect and disconnect. The log is filterable, exportable
|
|
11
|
+
* as CSV, and kept for ninety days.
|
|
12
12
|
*/
|
|
13
13
|
/**
|
|
14
14
|
* The kinds of actor the platform knows. `label` is what a human reads in the
|
|
@@ -16,7 +16,7 @@ import { wireSeqCursor, wireTimestampMs } from './common.js';
|
|
|
16
16
|
* resolve four different id kinds to render a row.
|
|
17
17
|
*
|
|
18
18
|
* **`end_user` stays, and it stays for the rows already written.** The
|
|
19
|
-
* two
|
|
19
|
+
* split into two identity spaces replaced the org's one user pool with
|
|
20
20
|
* Fleetless users and per-app app users; every new row an app user writes
|
|
21
21
|
* carries `app_user`. But an audit log is the one thing this platform must
|
|
22
22
|
* never rewrite, and there are stored rows whose `kind` is `end_user`. Dropping
|
|
@@ -24,9 +24,8 @@ import { wireSeqCursor, wireTimestampMs } from './common.js';
|
|
|
24
24
|
* cannot be read back is worse than one carrying a retired word.
|
|
25
25
|
*
|
|
26
26
|
* So this enum is deliberately **wider than what any producer emits**: nothing
|
|
27
|
-
* writes `end_user` any more, and nothing may start again. That is
|
|
28
|
-
*
|
|
29
|
-
* is said here rather than inferred from a `grep` somebody runs in a year.
|
|
27
|
+
* writes `end_user` any more, and nothing may start again. That is said here
|
|
28
|
+
* rather than left to be inferred from a search somebody runs in a year.
|
|
30
29
|
*
|
|
31
30
|
* `developer` is a Fleetless user. It kept its name through both redesigns
|
|
32
31
|
* because it was always right about what it named: the person who configures
|
|
@@ -42,8 +41,7 @@ export const auditEvent = z.object({
|
|
|
42
41
|
org_id: z.uuid(),
|
|
43
42
|
at: z.iso.datetime(),
|
|
44
43
|
/**
|
|
45
|
-
* A monotonic counter, ascending in write order, unique across the log
|
|
46
|
-
* (W6b).
|
|
44
|
+
* A monotonic counter, ascending in write order, unique across the log.
|
|
47
45
|
*
|
|
48
46
|
* `at` is not a total order. Two events written in the same millisecond —
|
|
49
47
|
* a login and the config publish it enables, a cascade writing several
|
|
@@ -55,11 +53,7 @@ export const auditEvent = z.object({
|
|
|
55
53
|
*
|
|
56
54
|
* It is also the only correct **cursor** for paging this log, for the same
|
|
57
55
|
* reason: a cursor that is not unique either skips rows or repeats them at
|
|
58
|
-
* every page boundary.
|
|
59
|
-
* the route returns the whole log — and that is stated here rather than
|
|
60
|
-
* implied, because a contract that describes a capability the API does not
|
|
61
|
-
* have is the defect this project keeps finding. When paging is added it
|
|
62
|
-
* uses this field; nothing else in this shape can carry it.
|
|
56
|
+
* every page boundary. Nothing else in this shape can carry one.
|
|
63
57
|
*
|
|
64
58
|
* Required, not optional: an event without a sequence cannot be ordered
|
|
65
59
|
* against one that has it, and a log with two orderings has none.
|
|
@@ -94,12 +88,7 @@ export const auditEvent = z.object({
|
|
|
94
88
|
details: z.record(z.string(), z.unknown()).nullable(),
|
|
95
89
|
});
|
|
96
90
|
/**
|
|
97
|
-
* **How this log is read
|
|
98
|
-
*
|
|
99
|
-
* Until now `GET /api/audit` returned the **whole** log — no filters, no
|
|
100
|
-
* cursor. `auditEvent.seq`'s own comment has said so plainly since W6b rather
|
|
101
|
-
* than describing a capability the API does not have; this shape builds
|
|
102
|
-
* exactly what that comment announced.
|
|
91
|
+
* **How this log is read.**
|
|
103
92
|
*
|
|
104
93
|
* **The cursor is `seq`, and no other field can be.** `at` is not a total
|
|
105
94
|
* order: two events written in the same millisecond sort differently on every
|
|
@@ -109,10 +98,6 @@ export const auditEvent = z.object({
|
|
|
109
98
|
*
|
|
110
99
|
* `before_seq` rather than `after_seq`, because this log is read **newest
|
|
111
100
|
* first**: the next page is older, not newer.
|
|
112
|
-
*
|
|
113
|
-
* **Filters are part of the same work, not a later garnish.** A console view
|
|
114
|
-
* without them is a page with nothing to filter by — the register row says
|
|
115
|
-
* exactly that, which is why the two rows are one piece of work.
|
|
116
101
|
*/
|
|
117
102
|
/**
|
|
118
103
|
* A unix-millisecond bound a Postgres `timestamptz` can actually hold.
|
|
@@ -129,7 +114,7 @@ export const auditQuery = z.object({
|
|
|
129
114
|
/** Only events with a smaller `seq` — the next, older page. */
|
|
130
115
|
before_seq: wireSeqCursor.optional(),
|
|
131
116
|
/**
|
|
132
|
-
*
|
|
117
|
+
* The same shape as `historyQuery.limit`: a union whose input branch
|
|
133
118
|
* **is the wire**. A `z.coerce` cannot be published — zod renders the
|
|
134
119
|
* coercion's result in either `io` direction, so the artifact would describe
|
|
135
120
|
* a shape a query string can never carry.
|
|
@@ -162,36 +147,25 @@ export const auditQuery = z.object({
|
|
|
162
147
|
/**
|
|
163
148
|
* Only events by this actor.
|
|
164
149
|
*
|
|
165
|
-
* **`z.uuid()`, because the column is one
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
* Not a SQL-injection finding — Drizzle parameterises, and `' or 1=1--`
|
|
171
|
-
* failed at the same cast. It is a **500 where a 400 belongs**, and a 500 is
|
|
172
|
-
* the answer that explains nothing.
|
|
150
|
+
* **`z.uuid()`, because the column is one.** A looser string type lets any
|
|
151
|
+
* non-uuid value reach the database as a uuid parameter, where the cast
|
|
152
|
+
* throws: `?actor_id=not-a-uuid` then answers **500 `internal_error`** rather
|
|
153
|
+
* than refusing the value.
|
|
173
154
|
*
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
-
* deletion — and the brand-new filter, whose field has exactly that shape,
|
|
177
|
-
* is the one that did not get it. A rule applied to the sites in front of
|
|
178
|
-
* you is not a rule applied to the class.
|
|
155
|
+
* Not an injection question — the query is parameterised either way. It is a
|
|
156
|
+
* **500 where a 400 belongs**, and a 500 is the answer that explains nothing.
|
|
179
157
|
*/
|
|
180
158
|
actor_id: z.uuid().optional(),
|
|
181
159
|
/** Only events about this kind of target, e.g. `robot`. */
|
|
182
160
|
target_kind: z.string().min(1).max(40).optional(),
|
|
183
161
|
/**
|
|
184
162
|
* Absolute bounds in unix milliseconds, **half-open `[from, to)`** — the
|
|
185
|
-
* same rule the history shapes follow
|
|
163
|
+
* same rule the history shapes follow.
|
|
186
164
|
*
|
|
187
|
-
* **Bounded to years 1..9999
|
|
188
|
-
*
|
|
189
|
-
*
|
|
190
|
-
*
|
|
191
|
-
* `253402300800000` → 500 (Argus-W9). `history-query.ts`'s `parseTimeExprMs`
|
|
192
|
-
* already carries exactly this range, with M3's reasoning for why
|
|
193
|
-
* `Number.isSafeInteger` is wider than what a timestamp can be; this is that
|
|
194
|
-
* same number, not a second one that happens to agree.
|
|
165
|
+
* **Bounded to years 1..9999.** `nonnegative()` alone admits instants a
|
|
166
|
+
* timestamp column has no representation for, and the route answers 500
|
|
167
|
+
* rather than refusing the value. `Number.isSafeInteger` is wider than what a
|
|
168
|
+
* timestamp can be, so the bound is stated rather than inherited.
|
|
195
169
|
*/
|
|
196
170
|
from_ms: auditTimestampMs.optional(),
|
|
197
171
|
to_ms: auditTimestampMs.optional(),
|
|
@@ -216,7 +190,7 @@ export const auditListResponse = z.object({
|
|
|
216
190
|
next_cursor: z.number().int().positive().nullable(),
|
|
217
191
|
});
|
|
218
192
|
/**
|
|
219
|
-
* **What a CSV export of this log looks like
|
|
193
|
+
* **What a CSV export of this log looks like.**
|
|
220
194
|
*
|
|
221
195
|
* The column order lives here because otherwise the cloud and the console
|
|
222
196
|
* would each carry their own, and nobody would notice them drifting apart
|
|
@@ -229,10 +203,9 @@ export const auditListResponse = z.object({
|
|
|
229
203
|
*/
|
|
230
204
|
export const AUDIT_CSV_COLUMNS = ['seq', 'at', 'actor_kind', 'actor_id', 'action', 'target_kind', 'target_id', 'target_label', 'details'];
|
|
231
205
|
/**
|
|
232
|
-
*
|
|
206
|
+
* The audit log is kept for **90 days**.
|
|
233
207
|
*
|
|
234
|
-
* A constant here so
|
|
235
|
-
*
|
|
236
|
-
* there is no purge touching audit rows at all.
|
|
208
|
+
* A constant here so no consumer derives it a second time — the same reasoning
|
|
209
|
+
* as `ASSET_UPLOAD_MAX_BYTES`.
|
|
237
210
|
*/
|
|
238
211
|
export const AUDIT_RETENTION_DAYS = 90;
|
package/dist/client-auth.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
1
2
|
import { z } from 'zod';
|
|
2
3
|
/**
|
|
3
|
-
* **The client auth API: the whole of what an app user's browser talks to
|
|
4
|
-
* (spec `2026-09-05-app-user-auth`, §4).
|
|
4
|
+
* **The client auth API: the whole of what an app user's browser talks to.**
|
|
5
5
|
*
|
|
6
|
-
* Fleetless shows an app user **no page
|
|
6
|
+
* Fleetless shows an app user **no page**. The developer's own UI owns
|
|
7
7
|
* every screen — login, registration, verification, invitation acceptance,
|
|
8
8
|
* password reset, the provider buttons, the MCP consent — and calls these
|
|
9
9
|
* routes as JSON. The hosted, app-branded login and consent pages this file
|
|
@@ -23,7 +23,7 @@ import { z } from 'zod';
|
|
|
23
23
|
* anything else is parsed as a JWT. That rule is written down once, here, so
|
|
24
24
|
* the SDK and the cloud cannot drift into disagreeing about it.
|
|
25
25
|
*
|
|
26
|
-
* **The enumeration discipline is
|
|
26
|
+
* **The enumeration discipline is deliberate, not a preference:**
|
|
27
27
|
* `register`, `resend-verification` and `password/reset` answer `202` for every
|
|
28
28
|
* policy-allowed request whether or not the address exists, and `login` answers
|
|
29
29
|
* the identical `invalid_credentials` for a wrong password, a `blocked` account
|
|
@@ -61,7 +61,7 @@ export declare const clientLogoutRequest: z.ZodObject<{
|
|
|
61
61
|
}, z.core.$strip>;
|
|
62
62
|
export type ClientLogoutRequest = z.infer<typeof clientLogoutRequest>;
|
|
63
63
|
/**
|
|
64
|
-
* **Self-registration**
|
|
64
|
+
* **Self-registration** — and the account it creates cannot log in yet.
|
|
65
65
|
*
|
|
66
66
|
* `register` writes the user as `pending_verification` and mails the app's
|
|
67
67
|
* `verify_url`. Without that step the domain whitelist would prove nothing:
|
|
@@ -221,7 +221,7 @@ export type ClientOidcStartQuery = z.infer<typeof clientOidcStartQuery>;
|
|
|
221
221
|
* `state` is the one required field because it is the one Fleetless minted: it
|
|
222
222
|
* resolves the `oidc_interactions` row that holds the app's `redirect_uri`,
|
|
223
223
|
* and without it there is nowhere to send any answer, success or failure. That
|
|
224
|
-
* is the single case where the cloud renders a page of its own
|
|
224
|
+
* is the single case where the cloud renders a page of its own.
|
|
225
225
|
*
|
|
226
226
|
* It exists as a schema rather than as four parameters read by hand because
|
|
227
227
|
* the manifest forbids the second: a documented route whose prose names a
|
|
@@ -244,7 +244,7 @@ export type ClientOidcExchangeRequest = z.infer<typeof clientOidcExchangeRequest
|
|
|
244
244
|
/**
|
|
245
245
|
* **Why a federated sign-in ended without a session, in a code the app can
|
|
246
246
|
* branch on** — carried back to the app's own `redirect_uri` as `error`, not
|
|
247
|
-
* rendered by Fleetless
|
|
247
|
+
* rendered by Fleetless. The only Fleetless-rendered page in this flow is
|
|
248
248
|
* the one for a state that can no longer be resolved to a redirect URI, because
|
|
249
249
|
* then there is nowhere to send the answer.
|
|
250
250
|
*
|
|
@@ -293,7 +293,7 @@ export declare const clientOidcErrorCode: z.ZodEnum<{
|
|
|
293
293
|
export type ClientOidcErrorCode = z.infer<typeof clientOidcErrorCode>;
|
|
294
294
|
/**
|
|
295
295
|
* **A pending MCP authorization, as the app's own consent screen reads it**
|
|
296
|
-
*
|
|
296
|
+
* Fleetless renders no page here either: `authorize` redirects to the
|
|
297
297
|
* app's `mcp_login_url` with an interaction id, the app authenticates the user
|
|
298
298
|
* with its normal UI, shows this, and approves or denies through the API.
|
|
299
299
|
*
|
|
@@ -388,7 +388,7 @@ export type McpConsentGrantListResponse = z.infer<typeof mcpConsentGrantListResp
|
|
|
388
388
|
* quietest way for a cut like this to go wrong.
|
|
389
389
|
*
|
|
390
390
|
* **`act` is gone.** It named the org admin behind an impersonation (the RFC
|
|
391
|
-
* 8693 pattern). Impersonation is deleted with no successor
|
|
391
|
+
* 8693 pattern). Impersonation is deleted with no successor, so a field
|
|
392
392
|
* that could still arrive would describe a delegation nothing can mint — and a
|
|
393
393
|
* client rendering "you are acting as …" from it would be showing a state the
|
|
394
394
|
* platform cannot enter.
|
package/dist/client-auth.js
CHANGED
|
@@ -4,10 +4,9 @@ import { appIdentifier } from './apps.js';
|
|
|
4
4
|
import { APP_USER_DISPLAY_NAME_MAX, providerSlug } from './app-users.js';
|
|
5
5
|
import { password } from './identity.js';
|
|
6
6
|
/**
|
|
7
|
-
* **The client auth API: the whole of what an app user's browser talks to
|
|
8
|
-
* (spec `2026-09-05-app-user-auth`, §4).
|
|
7
|
+
* **The client auth API: the whole of what an app user's browser talks to.**
|
|
9
8
|
*
|
|
10
|
-
* Fleetless shows an app user **no page
|
|
9
|
+
* Fleetless shows an app user **no page**. The developer's own UI owns
|
|
11
10
|
* every screen — login, registration, verification, invitation acceptance,
|
|
12
11
|
* password reset, the provider buttons, the MCP consent — and calls these
|
|
13
12
|
* routes as JSON. The hosted, app-branded login and consent pages this file
|
|
@@ -27,7 +26,7 @@ import { password } from './identity.js';
|
|
|
27
26
|
* anything else is parsed as a JWT. That rule is written down once, here, so
|
|
28
27
|
* the SDK and the cloud cannot drift into disagreeing about it.
|
|
29
28
|
*
|
|
30
|
-
* **The enumeration discipline is
|
|
29
|
+
* **The enumeration discipline is deliberate, not a preference:**
|
|
31
30
|
* `register`, `resend-verification` and `password/reset` answer `202` for every
|
|
32
31
|
* policy-allowed request whether or not the address exists, and `login` answers
|
|
33
32
|
* the identical `invalid_credentials` for a wrong password, a `blocked` account
|
|
@@ -74,7 +73,7 @@ export const clientLogoutRequest = z.object({
|
|
|
74
73
|
});
|
|
75
74
|
/* ---------------------------------------------- registration and mails -- */
|
|
76
75
|
/**
|
|
77
|
-
* **Self-registration**
|
|
76
|
+
* **Self-registration** — and the account it creates cannot log in yet.
|
|
78
77
|
*
|
|
79
78
|
* `register` writes the user as `pending_verification` and mails the app's
|
|
80
79
|
* `verify_url`. Without that step the domain whitelist would prove nothing:
|
|
@@ -272,7 +271,7 @@ export const clientOidcStartQuery = z.object({
|
|
|
272
271
|
* `state` is the one required field because it is the one Fleetless minted: it
|
|
273
272
|
* resolves the `oidc_interactions` row that holds the app's `redirect_uri`,
|
|
274
273
|
* and without it there is nowhere to send any answer, success or failure. That
|
|
275
|
-
* is the single case where the cloud renders a page of its own
|
|
274
|
+
* is the single case where the cloud renders a page of its own.
|
|
276
275
|
*
|
|
277
276
|
* It exists as a schema rather than as four parameters read by hand because
|
|
278
277
|
* the manifest forbids the second: a documented route whose prose names a
|
|
@@ -307,7 +306,7 @@ export const clientOidcExchangeRequest = z
|
|
|
307
306
|
/**
|
|
308
307
|
* **Why a federated sign-in ended without a session, in a code the app can
|
|
309
308
|
* branch on** — carried back to the app's own `redirect_uri` as `error`, not
|
|
310
|
-
* rendered by Fleetless
|
|
309
|
+
* rendered by Fleetless. The only Fleetless-rendered page in this flow is
|
|
311
310
|
* the one for a state that can no longer be resolved to a redirect URI, because
|
|
312
311
|
* then there is nowhere to send the answer.
|
|
313
312
|
*
|
|
@@ -356,7 +355,7 @@ export const clientOidcErrorCode = z.enum([
|
|
|
356
355
|
/* ------------------------------------------------ MCP, delegated login -- */
|
|
357
356
|
/**
|
|
358
357
|
* **A pending MCP authorization, as the app's own consent screen reads it**
|
|
359
|
-
*
|
|
358
|
+
* Fleetless renders no page here either: `authorize` redirects to the
|
|
360
359
|
* app's `mcp_login_url` with an interaction id, the app authenticates the user
|
|
361
360
|
* with its normal UI, shows this, and approves or denies through the API.
|
|
362
361
|
*
|
|
@@ -457,7 +456,7 @@ export const mcpConsentGrantListResponse = z.object({
|
|
|
457
456
|
* quietest way for a cut like this to go wrong.
|
|
458
457
|
*
|
|
459
458
|
* **`act` is gone.** It named the org admin behind an impersonation (the RFC
|
|
460
|
-
* 8693 pattern). Impersonation is deleted with no successor
|
|
459
|
+
* 8693 pattern). Impersonation is deleted with no successor, so a field
|
|
461
460
|
* that could still arrive would describe a delegation nothing can mint — and a
|
|
462
461
|
* client rendering "you are acting as …" from it would be showing a state the
|
|
463
462
|
* platform cannot enter.
|
package/dist/common.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
1
2
|
import { z } from 'zod';
|
|
2
3
|
/**
|
|
3
4
|
* Names shared by every layer: the Fleetless slug and the ROS names it is
|
|
4
|
-
* deliberately decoupled from
|
|
5
|
+
* deliberately decoupled from.
|
|
5
6
|
*
|
|
6
7
|
* They live here rather than in `protocol.ts` so the exposure model
|
|
7
8
|
* (`config.ts`) and the bridge protocol can both use them without importing
|
|
@@ -18,16 +19,11 @@ import { z } from 'zod';
|
|
|
18
19
|
* Each is used **twice**: as the message zod itself produces, here, and as
|
|
19
20
|
* `patternErrorMessage` in `config.ts`'s exported JSON Schema, which is a
|
|
20
21
|
* published artifact that other tools validate against and that a person
|
|
21
|
-
* reads.
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* forever and invisibly — the shape that had `buildAcceptUrl` mailing one URL
|
|
27
|
-
* three ways. They are therefore one constant with two readers rather than two
|
|
28
|
-
* strings that happen to agree, and `config-zod-messages.test.ts` asserts the
|
|
29
|
-
* two readings are the same string at all 24 pattern positions the document
|
|
30
|
-
* has.
|
|
22
|
+
* reads. Either way it is a second spelling of a live rule, and an unwatched
|
|
23
|
+
* second spelling drifts word for word, forever and invisibly. They are
|
|
24
|
+
* therefore one constant with two readers rather than two strings that happen
|
|
25
|
+
* to agree, and `config-zod-messages.test.ts` asserts the two readings are the
|
|
26
|
+
* same string at every pattern position the document has.
|
|
31
27
|
*
|
|
32
28
|
* **They are exported because the pattern and its sentence must not be able to
|
|
33
29
|
* move apart**, and the pattern is here while the schema annotation is in
|
|
@@ -35,15 +31,13 @@ import { z } from 'zod';
|
|
|
35
31
|
* document — the two URL schemes and the capture-device path — are constants in
|
|
36
32
|
* `config.ts` beside their own patterns, on the same rule.
|
|
37
33
|
*
|
|
38
|
-
* **
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
* 2026-09-03). What it does reach is the sentence a *parser* produces, in every
|
|
46
|
-
* layer that parses one of these names — which is the improvement, not a cost.
|
|
34
|
+
* **Putting the sentence here changes no published artifact.** A `.meta()` on
|
|
35
|
+
* `slug` would reach dozens of the published schemas — which is why `mapKey` in
|
|
36
|
+
* `config.ts` carries the annotation and `slug` does not. A message on a
|
|
37
|
+
* `.regex()` check is a different thing: zod renders no error message into JSON
|
|
38
|
+
* Schema at all, so every artifact is byte-identical either way. What it does
|
|
39
|
+
* reach is the sentence a *parser* produces, in every layer that parses one of
|
|
40
|
+
* these names — which is the improvement, not a cost.
|
|
47
41
|
*/
|
|
48
42
|
export declare const SLUG_RULE = "A name is lower-case: it starts with a letter, continues with letters and digits, and joins further words with a single underscore \u2014 `battery_voltage`. Capitals, dashes, dots, spaces, a leading digit and a doubled or trailing underscore are all refused.";
|
|
49
43
|
/**
|
|
@@ -51,7 +45,7 @@ export declare const SLUG_RULE = "A name is lower-case: it starts with a letter,
|
|
|
51
45
|
* message name. Lowercase, underscore-separated, letter-initial, 2..63
|
|
52
46
|
* characters, no leading/trailing/doubled underscores.
|
|
53
47
|
*
|
|
54
|
-
* Names are stable and decoupled from ROS names
|
|
48
|
+
* Names are stable and decoupled from ROS names — renaming a
|
|
55
49
|
* topic on the robot must never break a client app. The reverse also holds
|
|
56
50
|
* and costs more: changing a name breaks every client, role grant and MCP
|
|
57
51
|
* tool name that uses it.
|
|
@@ -67,9 +61,9 @@ export declare const rosName: z.ZodString;
|
|
|
67
61
|
export declare const ROS_TYPE_NAME_RULE = "A ROS 2 type name has three segments: the package, then `msg`, `srv` or `action`, then the type \u2014 `sensor_msgs/msg/BatteryState`, `std_srvs/srv/Trigger`, `nav2_msgs/action/NavigateToPose`. The middle segment is the one usually left out. The package is lower-case with underscores; the type itself is letters and digits, conventionally CamelCase.";
|
|
68
62
|
/**
|
|
69
63
|
* A ROS interface type as ROS 2 spells it: `pkg/msg/Type`, `pkg/srv/Type`,
|
|
70
|
-
* `pkg/action/Type`.
|
|
71
|
-
*
|
|
72
|
-
*
|
|
64
|
+
* `pkg/action/Type`. Introspection resolves a field tree for each of the
|
|
65
|
+
* three; a message has one flat list, a service and an action have one tree
|
|
66
|
+
* per part.
|
|
73
67
|
*/
|
|
74
68
|
export declare const rosTypeName: z.ZodString;
|
|
75
69
|
export declare const FIELD_PATH_RULE = "A field path is dotted and lower-case, and each segment may index at most one array level \u2014 `voltage`, `pose.position.x`, `ranges[0]`. ROS 2 has no nested arrays, so a second index on one segment could name nothing that exists.";
|
|
@@ -77,15 +71,15 @@ export declare const FIELD_PATH_RULE = "A field path is dotted and lower-case, a
|
|
|
77
71
|
* A path into a message: dot-separated field names, each carrying **at most
|
|
78
72
|
* one** array index, e.g. `percentage`, `pose.position.x`, `ranges[0]`,
|
|
79
73
|
* `poses[0].pose.position.x`. `null` in a datapoint config means *the whole
|
|
80
|
-
* message
|
|
74
|
+
* message*: one field or one whole topic, never several topics.
|
|
81
75
|
*
|
|
82
76
|
* One index per segment is not a preference but the shape of the target: ROS 2
|
|
83
77
|
* IDL has `float64[]`, `float64[3]` and `float64[<=10]`, and no nested or
|
|
84
78
|
* multi-dimensional arrays at all. A second index on one segment — `a[0][1]` —
|
|
85
79
|
* could therefore denote nothing on any message that exists. The bridge has
|
|
86
80
|
* always refused it (`sampling.py`'s `FieldPathError`, *"ROS has no nested
|
|
87
|
-
* arrays"*)
|
|
88
|
-
* AI-generated document
|
|
81
|
+
* arrays"*). A grammar that said otherwise would let a hand-written or
|
|
82
|
+
* AI-generated document pass the cloud and then fail at the robot as a
|
|
89
83
|
* `config_applied` error — the latest and worst place to learn it.
|
|
90
84
|
*/
|
|
91
85
|
export declare const fieldPath: z.ZodString;
|
|
@@ -95,14 +89,12 @@ export declare const fieldPath: z.ZodString;
|
|
|
95
89
|
*
|
|
96
90
|
* The union's input branch **is the wire** — a `z.coerce` cannot be published,
|
|
97
91
|
* because zod renders the coercion's result in either `io` direction, so the
|
|
98
|
-
* artifact would describe a shape a query string can never carry
|
|
99
|
-
*
|
|
100
|
-
* The year bound is
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
* here from `audit.ts` when `jobRunQuery` needed the same guard; a second copy
|
|
105
|
-
* would have been a second policy for one decision.
|
|
92
|
+
* artifact would describe a shape a query string can never carry.
|
|
93
|
+
*
|
|
94
|
+
* The year bound is not decorative. `nonnegative()` alone admits instants a
|
|
95
|
+
* timestamp column has no representation for, and the route answers 500 rather
|
|
96
|
+
* than refusing the value. It lives here, once, because more than one query
|
|
97
|
+
* needs it and a second copy would be a second policy for one decision.
|
|
106
98
|
*/
|
|
107
99
|
/**
|
|
108
100
|
* A `seq` cursor as a **query string** actually carries it.
|
|
@@ -131,8 +123,8 @@ export declare const wireTimestampMs: z.ZodPipe<z.ZodPipe<z.ZodUnion<readonly [z
|
|
|
131
123
|
*
|
|
132
124
|
* Lives here, not in `protocol.ts`, for the same reason `slug` and friends
|
|
133
125
|
* do: `config.ts`'s `configState.applied_errors` is the REST shape the
|
|
134
|
-
* console reads this same error through
|
|
135
|
-
*
|
|
126
|
+
* console reads this same error through, and `protocol.ts` already imports
|
|
127
|
+
* from `config.ts`
|
|
136
128
|
* (`credentialRef`, `robotConfigDoc`) — so `config.ts` importing back from
|
|
137
129
|
* `protocol.ts` would be a cycle. One definition, reachable from both
|
|
138
130
|
* without either importing the other.
|
package/dist/common.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
/**
|
|
4
4
|
* Names shared by every layer: the Fleetless slug and the ROS names it is
|
|
5
|
-
* deliberately decoupled from
|
|
5
|
+
* deliberately decoupled from.
|
|
6
6
|
*
|
|
7
7
|
* They live here rather than in `protocol.ts` so the exposure model
|
|
8
8
|
* (`config.ts`) and the bridge protocol can both use them without importing
|
|
@@ -19,16 +19,11 @@ import { z } from 'zod';
|
|
|
19
19
|
* Each is used **twice**: as the message zod itself produces, here, and as
|
|
20
20
|
* `patternErrorMessage` in `config.ts`'s exported JSON Schema, which is a
|
|
21
21
|
* published artifact that other tools validate against and that a person
|
|
22
|
-
* reads.
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* forever and invisibly — the shape that had `buildAcceptUrl` mailing one URL
|
|
28
|
-
* three ways. They are therefore one constant with two readers rather than two
|
|
29
|
-
* strings that happen to agree, and `config-zod-messages.test.ts` asserts the
|
|
30
|
-
* two readings are the same string at all 24 pattern positions the document
|
|
31
|
-
* has.
|
|
22
|
+
* reads. Either way it is a second spelling of a live rule, and an unwatched
|
|
23
|
+
* second spelling drifts word for word, forever and invisibly. They are
|
|
24
|
+
* therefore one constant with two readers rather than two strings that happen
|
|
25
|
+
* to agree, and `config-zod-messages.test.ts` asserts the two readings are the
|
|
26
|
+
* same string at every pattern position the document has.
|
|
32
27
|
*
|
|
33
28
|
* **They are exported because the pattern and its sentence must not be able to
|
|
34
29
|
* move apart**, and the pattern is here while the schema annotation is in
|
|
@@ -36,15 +31,13 @@ import { z } from 'zod';
|
|
|
36
31
|
* document — the two URL schemes and the capture-device path — are constants in
|
|
37
32
|
* `config.ts` beside their own patterns, on the same rule.
|
|
38
33
|
*
|
|
39
|
-
* **
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
* 2026-09-03). What it does reach is the sentence a *parser* produces, in every
|
|
47
|
-
* layer that parses one of these names — which is the improvement, not a cost.
|
|
34
|
+
* **Putting the sentence here changes no published artifact.** A `.meta()` on
|
|
35
|
+
* `slug` would reach dozens of the published schemas — which is why `mapKey` in
|
|
36
|
+
* `config.ts` carries the annotation and `slug` does not. A message on a
|
|
37
|
+
* `.regex()` check is a different thing: zod renders no error message into JSON
|
|
38
|
+
* Schema at all, so every artifact is byte-identical either way. What it does
|
|
39
|
+
* reach is the sentence a *parser* produces, in every layer that parses one of
|
|
40
|
+
* these names — which is the improvement, not a cost.
|
|
48
41
|
*/
|
|
49
42
|
export const SLUG_RULE = 'A name is lower-case: it starts with a letter, continues with letters and digits, and joins further words with a single underscore — `battery_voltage`. Capitals, dashes, dots, spaces, a leading digit and a doubled or trailing underscore are all refused.';
|
|
50
43
|
/**
|
|
@@ -52,7 +45,7 @@ export const SLUG_RULE = 'A name is lower-case: it starts with a letter, continu
|
|
|
52
45
|
* message name. Lowercase, underscore-separated, letter-initial, 2..63
|
|
53
46
|
* characters, no leading/trailing/doubled underscores.
|
|
54
47
|
*
|
|
55
|
-
* Names are stable and decoupled from ROS names
|
|
48
|
+
* Names are stable and decoupled from ROS names — renaming a
|
|
56
49
|
* topic on the robot must never break a client app. The reverse also holds
|
|
57
50
|
* and costs more: changing a name breaks every client, role grant and MCP
|
|
58
51
|
* tool name that uses it.
|
|
@@ -75,9 +68,9 @@ export const rosName = z
|
|
|
75
68
|
export const ROS_TYPE_NAME_RULE = 'A ROS 2 type name has three segments: the package, then `msg`, `srv` or `action`, then the type — `sensor_msgs/msg/BatteryState`, `std_srvs/srv/Trigger`, `nav2_msgs/action/NavigateToPose`. The middle segment is the one usually left out. The package is lower-case with underscores; the type itself is letters and digits, conventionally CamelCase.';
|
|
76
69
|
/**
|
|
77
70
|
* A ROS interface type as ROS 2 spells it: `pkg/msg/Type`, `pkg/srv/Type`,
|
|
78
|
-
* `pkg/action/Type`.
|
|
79
|
-
*
|
|
80
|
-
*
|
|
71
|
+
* `pkg/action/Type`. Introspection resolves a field tree for each of the
|
|
72
|
+
* three; a message has one flat list, a service and an action have one tree
|
|
73
|
+
* per part.
|
|
81
74
|
*/
|
|
82
75
|
export const rosTypeName = z
|
|
83
76
|
.string()
|
|
@@ -88,15 +81,15 @@ export const FIELD_PATH_RULE = 'A field path is dotted and lower-case, and each
|
|
|
88
81
|
* A path into a message: dot-separated field names, each carrying **at most
|
|
89
82
|
* one** array index, e.g. `percentage`, `pose.position.x`, `ranges[0]`,
|
|
90
83
|
* `poses[0].pose.position.x`. `null` in a datapoint config means *the whole
|
|
91
|
-
* message
|
|
84
|
+
* message*: one field or one whole topic, never several topics.
|
|
92
85
|
*
|
|
93
86
|
* One index per segment is not a preference but the shape of the target: ROS 2
|
|
94
87
|
* IDL has `float64[]`, `float64[3]` and `float64[<=10]`, and no nested or
|
|
95
88
|
* multi-dimensional arrays at all. A second index on one segment — `a[0][1]` —
|
|
96
89
|
* could therefore denote nothing on any message that exists. The bridge has
|
|
97
90
|
* always refused it (`sampling.py`'s `FieldPathError`, *"ROS has no nested
|
|
98
|
-
* arrays"*)
|
|
99
|
-
* AI-generated document
|
|
91
|
+
* arrays"*). A grammar that said otherwise would let a hand-written or
|
|
92
|
+
* AI-generated document pass the cloud and then fail at the robot as a
|
|
100
93
|
* `config_applied` error — the latest and worst place to learn it.
|
|
101
94
|
*/
|
|
102
95
|
export const fieldPath = z
|
|
@@ -109,14 +102,12 @@ export const fieldPath = z
|
|
|
109
102
|
*
|
|
110
103
|
* The union's input branch **is the wire** — a `z.coerce` cannot be published,
|
|
111
104
|
* because zod renders the coercion's result in either `io` direction, so the
|
|
112
|
-
* artifact would describe a shape a query string can never carry
|
|
113
|
-
*
|
|
114
|
-
* The year bound is
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
* here from `audit.ts` when `jobRunQuery` needed the same guard; a second copy
|
|
119
|
-
* would have been a second policy for one decision.
|
|
105
|
+
* artifact would describe a shape a query string can never carry.
|
|
106
|
+
*
|
|
107
|
+
* The year bound is not decorative. `nonnegative()` alone admits instants a
|
|
108
|
+
* timestamp column has no representation for, and the route answers 500 rather
|
|
109
|
+
* than refusing the value. It lives here, once, because more than one query
|
|
110
|
+
* needs it and a second copy would be a second policy for one decision.
|
|
120
111
|
*/
|
|
121
112
|
/**
|
|
122
113
|
* A `seq` cursor as a **query string** actually carries it.
|
|
@@ -158,8 +149,8 @@ export const wireTimestampMs = z
|
|
|
158
149
|
*
|
|
159
150
|
* Lives here, not in `protocol.ts`, for the same reason `slug` and friends
|
|
160
151
|
* do: `config.ts`'s `configState.applied_errors` is the REST shape the
|
|
161
|
-
* console reads this same error through
|
|
162
|
-
*
|
|
152
|
+
* console reads this same error through, and `protocol.ts` already imports
|
|
153
|
+
* from `config.ts`
|
|
163
154
|
* (`credentialRef`, `robotConfigDoc`) — so `config.ts` importing back from
|
|
164
155
|
* `protocol.ts` would be a cycle. One definition, reachable from both
|
|
165
156
|
* without either importing the other.
|