@fleetless/contracts 1.0.2 → 1.0.4

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/dist/app-users.js CHANGED
@@ -2,8 +2,7 @@
2
2
  import { z } from 'zod';
3
3
  import { idpIssuer, mailStatus, password } from './identity.js';
4
4
  /**
5
- * **App users: the per-app identity space** (spec `2026-09-05-app-user-auth`,
6
- * D1–D7).
5
+ * **App users: the per-app identity space.**
7
6
  *
8
7
  * The 2026-08-29 model put developers and end users into one pool per org,
9
8
  * tied apps to groups, and let an org admin enter an app only by
@@ -22,7 +21,7 @@ import { idpIssuer, mailStatus, password } from './identity.js';
22
21
  * as unrelated accounts, and a Fleetless user who wants to use an app
23
22
  * registers or is invited like anybody else.
24
23
  *
25
- * **Fleetless shows an app user no page** (D2). The developer's own UI owns
24
+ * **Fleetless shows an app user no page**. The developer's own UI owns
26
25
  * every screen and calls the JSON client-auth API (`client-auth.ts`). The one
27
26
  * Fleetless-rendered surface an app user can reach is the problem page for an
28
27
  * OIDC callback whose state no longer resolves to a redirect URI — every other
@@ -68,7 +67,7 @@ export const appUserStatus = z.enum(['pending_verification', 'active', 'blocked'
68
67
  * **A user of one app.** Not a user of the org: `app_id` is the whole scope,
69
68
  * and the uniqueness constraint the cloud enforces is `(app_id, lower(email))`
70
69
  * rather than a global one. The same person at two apps of one org is two
71
- * unrelated rows, by design (D1).
70
+ * unrelated rows, by design.
72
71
  */
73
72
  export const appUser = z.object({
74
73
  id: z.uuid().meta({
@@ -148,8 +147,8 @@ export const createAppUserRequest = z
148
147
  * offering it is a refusal rather than a silently dropped field.
149
148
  *
150
149
  * **`status` admits only `active` and `blocked`.** `pending_verification` is
151
- * reached once, by self-registration, and left by spending the mailed token
152
- * (D6). A developer able to set it back could void a verified address without
150
+ * reached once, by self-registration, and left by spending the mailed token.
151
+ * A developer able to set it back could void a verified address without
153
152
  * the user ever seeing a mail, and there is no route out of that state that
154
153
  * does not require a token nobody re-sent. So the narrower enum is the rule,
155
154
  * stated in the schema rather than left to a handler to remember.
@@ -196,7 +195,7 @@ export const createAppInvitationRequest = z
196
195
  * An app that has configured none has nowhere for it to point, so there is no
197
196
  * link to hand back — `null` says that outright, where an absent key would be
198
197
  * indistinguishable from a mapper that dropped the field and a fabricated
199
- * Fleetless-hosted URL would name a page this product does not serve (D2).
198
+ * Fleetless-hosted URL would name a page this product does not serve.
200
199
  */
201
200
  export const appInvitation = z.object({
202
201
  id: z.uuid().meta({ description: 'The invitation, as listed and revoked by the developer.' }),
@@ -231,7 +230,7 @@ export const appInvitationListResponse = z.object({
231
230
  }),
232
231
  });
233
232
  /**
234
- * **An app's OIDC provider, as read back** (D4). Any number per app, unlike
233
+ * **An app's OIDC provider, as read back**. Any number per app, unlike
235
234
  * the group provider this replaces — a developer serving two customers needs
236
235
  * two, and the old at-most-one rule was a property of groups rather than of
237
236
  * identity.
@@ -305,7 +304,7 @@ export const createAppOidcProviderRequest = z
305
304
  description: 'The client secret, **write-only**: it is stored encrypted and comes back through nothing — not the read, not this route\'s own answer, not an audit detail. Required on create, since a provider with no secret cannot exchange a code; the minimum length refuses a value that is a misconfiguration rather than a secret.',
306
305
  }),
307
306
  scopes: z.array(z.string().min(1).max(60)).min(1).max(20).default(['openid', 'email', 'profile']).meta({
308
- description: 'The scopes to request. Defaults to `openid email profile`, which is what the linking rules in this design actually read: the subject, the address and its verified flag, and a name.',
307
+ description: 'The scopes to request. Defaults to `openid email profile`, which is what account linking actually reads: the subject, the address and its verified flag, and a name.',
309
308
  }),
310
309
  link_verified_emails: z.boolean().default(false).meta({
311
310
  description: 'Whether a federated login may join an existing app user by verified address. **Defaults to off**, because relaxing later is additive and admitting duplicates now and tightening afterwards is not.',
@@ -438,16 +437,15 @@ export const emailDomain = z
438
437
  .max(253)
439
438
  .regex(/^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$/, 'must be a lowercase domain name with at least two labels');
440
439
  /**
441
- * **The app's auth settings: one row per app, configured by a Fleetless user**
442
- * (D3).
440
+ * **The app's auth settings: one row per app, configured by a Fleetless user.**
443
441
  *
444
442
  * `self_registration` and `allowed_domains` are **one policy for one
445
443
  * decision** — they govern registration by password and registration through
446
- * an identity provider alike (D4). An invitation always bypasses both, because
444
+ * an identity provider alike. An invitation always bypasses both, because
447
445
  * a developer inviting somebody by hand has already made the decision the
448
446
  * whitelist automates.
449
447
  *
450
- * The four URLs are what makes D2 work: Fleetless mails a link, and the link
448
+ * The four URLs are what makes that work: Fleetless mails a link, and the link
451
449
  * points into the developer's app. An app that has configured none of them
452
450
  * still works for password login — it simply cannot send a mail that leads
453
451
  * anywhere, and `send_mail` is refused rather than silently sending a dead
@@ -494,7 +492,7 @@ export const putAppAuthConfigRequest = appAuthConfig
494
492
  .omit({ oidc_callback_url: true, updated_at: true })
495
493
  .strict();
496
494
  /**
497
- * The three mails a developer may replace with their own template (D5).
495
+ * The three mails a developer may replace with their own template.
498
496
  * Mails to *Fleetless* users — a team invitation, a console password reset —
499
497
  * stay Fleetless default and are deliberately not customisable: they are
500
498
  * about this platform, not about the developer's product.
package/dist/apps.js CHANGED
@@ -124,7 +124,8 @@ export const updateAppRequest = z.object({
124
124
  name: z.string().min(1).max(120).optional(),
125
125
  robot_ids: z.array(z.uuid()).optional(),
126
126
  /**
127
- * `app.default_role_id`'s write half — an app *setting*, which is where D1
127
+ * `app.default_role_id`'s write half — an app *setting*, which is where the
128
+ * two-identity-space model
128
129
  * put the default role, so it belongs on the app's own PATCH and not on a
129
130
  * route of its own.
130
131
  *
package/dist/audit.d.ts CHANGED
@@ -15,7 +15,7 @@ import { z } from 'zod';
15
15
  * resolve four different id kinds to render a row.
16
16
  *
17
17
  * **`end_user` stays, and it stays for the rows already written.** The
18
- * two-space cut (2026-09-05, D1) replaced the org's one user pool with
18
+ * split into two identity spaces replaced the org's one user pool with
19
19
  * Fleetless users and per-app app users; every new row an app user writes
20
20
  * carries `app_user`. But an audit log is the one thing this platform must
21
21
  * never rewrite, and there are stored rows whose `kind` is `end_user`. Dropping
@@ -23,9 +23,8 @@ import { z } from 'zod';
23
23
  * cannot be read back is worse than one carrying a retired word.
24
24
  *
25
25
  * So this enum is deliberately **wider than what any producer emits**: nothing
26
- * writes `end_user` any more, and nothing may start again. That is the kind of
27
- * claim this repository has been wrong about before by leaving it unsaid, so it
28
- * is said here rather than inferred from a `grep` somebody runs in a year.
26
+ * writes `end_user` any more, and nothing may start again. That is said here
27
+ * rather than left to be inferred from a search somebody runs in a year.
29
28
  *
30
29
  * `developer` is a Fleetless user. It kept its name through both redesigns
31
30
  * because it was always right about what it named: the person who configures
package/dist/audit.js CHANGED
@@ -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-space cut (2026-09-05, D1) replaced the org's one user pool with
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 the kind of
28
- * claim this repository has been wrong about before by leaving it unsaid, so it
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
@@ -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** (D6) — and the account it creates cannot log in yet.
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 (D2).
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 (D2). The only Fleetless-rendered page in this flow is
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
- * (D7). Fleetless renders no page here either: `authorize` redirects to the
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 (D1), so a field
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.
@@ -73,7 +73,7 @@ export const clientLogoutRequest = z.object({
73
73
  });
74
74
  /* ---------------------------------------------- registration and mails -- */
75
75
  /**
76
- * **Self-registration** (D6) — and the account it creates cannot log in yet.
76
+ * **Self-registration** — and the account it creates cannot log in yet.
77
77
  *
78
78
  * `register` writes the user as `pending_verification` and mails the app's
79
79
  * `verify_url`. Without that step the domain whitelist would prove nothing:
@@ -271,7 +271,7 @@ export const clientOidcStartQuery = z.object({
271
271
  * `state` is the one required field because it is the one Fleetless minted: it
272
272
  * resolves the `oidc_interactions` row that holds the app's `redirect_uri`,
273
273
  * and without it there is nowhere to send any answer, success or failure. That
274
- * is the single case where the cloud renders a page of its own (D2).
274
+ * is the single case where the cloud renders a page of its own.
275
275
  *
276
276
  * It exists as a schema rather than as four parameters read by hand because
277
277
  * the manifest forbids the second: a documented route whose prose names a
@@ -306,7 +306,7 @@ export const clientOidcExchangeRequest = z
306
306
  /**
307
307
  * **Why a federated sign-in ended without a session, in a code the app can
308
308
  * branch on** — carried back to the app's own `redirect_uri` as `error`, not
309
- * rendered by Fleetless (D2). The only Fleetless-rendered page in this flow is
309
+ * rendered by Fleetless. The only Fleetless-rendered page in this flow is
310
310
  * the one for a state that can no longer be resolved to a redirect URI, because
311
311
  * then there is nowhere to send the answer.
312
312
  *
@@ -355,7 +355,7 @@ export const clientOidcErrorCode = z.enum([
355
355
  /* ------------------------------------------------ MCP, delegated login -- */
356
356
  /**
357
357
  * **A pending MCP authorization, as the app's own consent screen reads it**
358
- * (D7). Fleetless renders no page here either: `authorize` redirects to the
358
+ * Fleetless renders no page here either: `authorize` redirects to the
359
359
  * app's `mcp_login_url` with an interaction id, the app authenticates the user
360
360
  * with its normal UI, shows this, and approves or denies through the API.
361
361
  *
@@ -456,7 +456,7 @@ export const mcpConsentGrantListResponse = z.object({
456
456
  * quietest way for a cut like this to go wrong.
457
457
  *
458
458
  * **`act` is gone.** It named the org admin behind an impersonation (the RFC
459
- * 8693 pattern). Impersonation is deleted with no successor (D1), so a field
459
+ * 8693 pattern). Impersonation is deleted with no successor, so a field
460
460
  * that could still arrive would describe a delegation nothing can mint — and a
461
461
  * client rendering "you are acting as …" from it would be showing a state the
462
462
  * platform cannot enter.
package/dist/common.d.ts CHANGED
@@ -123,8 +123,8 @@ export declare const wireTimestampMs: z.ZodPipe<z.ZodPipe<z.ZodUnion<readonly [z
123
123
  *
124
124
  * Lives here, not in `protocol.ts`, for the same reason `slug` and friends
125
125
  * do: `config.ts`'s `configState.applied_errors` is the REST shape the
126
- * console reads this same error through (spec `2026-08-21-exposure-and-revoke-design`
127
- * D4), and `protocol.ts` already imports from `config.ts`
126
+ * console reads this same error through, and `protocol.ts` already imports
127
+ * from `config.ts`
128
128
  * (`credentialRef`, `robotConfigDoc`) — so `config.ts` importing back from
129
129
  * `protocol.ts` would be a cycle. One definition, reachable from both
130
130
  * without either importing the other.
package/dist/common.js CHANGED
@@ -149,8 +149,8 @@ export const wireTimestampMs = z
149
149
  *
150
150
  * Lives here, not in `protocol.ts`, for the same reason `slug` and friends
151
151
  * do: `config.ts`'s `configState.applied_errors` is the REST shape the
152
- * console reads this same error through (spec `2026-08-21-exposure-and-revoke-design`
153
- * D4), and `protocol.ts` already imports from `config.ts`
152
+ * console reads this same error through, and `protocol.ts` already imports
153
+ * from `config.ts`
154
154
  * (`credentialRef`, `robotConfigDoc`) — so `config.ts` importing back from
155
155
  * `protocol.ts` would be a cycle. One definition, reachable from both
156
156
  * without either importing the other.
@@ -4,17 +4,15 @@ import type { ValidationIssue } from './config.js';
4
4
  /**
5
5
  * What is wrong with a configuration document, in one account.
6
6
  *
7
- * Everything here used to live in `cloud/src/validation.ts`. It is in
8
- * contracts because the console has to say **exactly** what the server says
9
- * about a document — same codes, same sentences, same paths — and the only
10
- * way that is true is if it is the same code. A console that reimplemented
11
- * this and then disagreed with the server about what is wrong would be worse
12
- * than a console that said nothing (spec D3).
13
- *
14
- * **The second door D3 forbids existed for one wave, and closed in wave 2
15
- * task 8** (cloud `a307e18`, 2026-09-03). The cloud cannot import a specifier
16
- * it has not pinned, so its own copy of `schemaIssues`, `refusal`, `slugOf`,
17
- * `formatPath` and `valueAt` stood from wave 1, when this module landed here,
7
+ * It lives in contracts because an editor has to say **exactly** what the
8
+ * server says about a document — same codes, same sentences, same paths — and
9
+ * the only way that is true is if it is the same code. An editor that
10
+ * reimplemented this and then disagreed with the server about what is wrong
11
+ * would be worse than one that said nothing.
12
+ *
13
+ * **A second implementation is the thing this module exists to prevent.** A
14
+ * server that cannot import this package keeps its own copy of `schemaIssues`,
15
+ * `refusal`, `slugOf`, `formatPath` and `valueAt`,
18
16
  * until that re-pin deleted them and imported these. The window is recorded
19
17
  * rather than dropped because it cost a live bug while it was open: the
20
18
  * cloud's own `formatPath` wrote a blank path segment as the empty string,
@@ -42,10 +40,9 @@ export declare const DOCUMENT_ROOT_PATH = "(document)";
42
40
  * `messages:` is deliberately not among them: its names are their own
43
41
  * namespace.
44
42
  *
45
- * `cloud/src/config-sections.ts` re-exports this constant and drives the
46
- * cloud's iteration over sections from it; the console reads it directly
47
- * (`useConfigRepairs.ts`). It was spelled out separately in all three until
48
- * wave 2 task 8 (cloud `a307e18`, 2026-09-03) — this is the only spelling
43
+ * The cloud re-exports this constant and drives its iteration over sections
44
+ * from it; an editor reads it directly. Spelling it out separately in each
45
+ * would be three copies — this is the only spelling
49
46
  * since.
50
47
  */
51
48
  export declare const EXPOSURE_SECTIONS: readonly ["datapoints", "actions", "services", "publishers", "cameras"];
@@ -90,10 +87,10 @@ export declare function schemaIssues(value: unknown, issues: readonly SchemaIssu
90
87
  * width. Rendered bare, such a key produced a path a reader cannot act
91
88
  * on — and at the root it produced the empty string, which
92
89
  * `validationIssue.path` (`z.string().min(1)`) refuses. That was the cloud
93
- * publishing a finding that fails the cloud's own contract for findings, and
94
- * after D2 stored the draft it cost the whole `configDraftResponse`, not one
95
- * issue: the console's `safeParse` dropped the response and handed the editor
96
- * nothing, for two characters typed.
90
+ * publishing a finding that fails its own contract for findings, and because a
91
+ * draft that does not parse is stored rather than refused it costs the whole
92
+ * `configDraftResponse`, not one issue: a client's `safeParse` drops the
93
+ * response and hands the editor nothing, for two characters typed.
97
94
  *
98
95
  * The quoted spelling is the segment's JSON string literal, and that is the
99
96
  * whole of the reason for choosing it: JSON's string syntax is a subset of
@@ -126,9 +123,9 @@ export declare function formatPath(path: readonly PropertyKey[]): string;
126
123
  *
127
124
  * Escaping on the way out was the alternative and was rejected: `path` is a
128
125
  * wire field (`validationIssue.path`), it is rendered to developers as-is,
129
- * and every recorded expectation in this repo and the cloud's spells it
130
- * unescaped. Changing what the server says about every document to make one
131
- * console lookup total is the larger of the two costs.
126
+ * and every recorded expectation on both sides spells it unescaped. Changing
127
+ * what the server says about every document to make one client-side lookup
128
+ * total is the larger of the two costs.
132
129
  *
133
130
  * So the property this has, and the one its test asserts, is the narrow one:
134
131
  * **a path round-trips when no string segment contains `.` or `[`, and the
@@ -7,10 +7,9 @@ export const DOCUMENT_ROOT_PATH = '(document)';
7
7
  * `messages:` is deliberately not among them: its names are their own
8
8
  * namespace.
9
9
  *
10
- * `cloud/src/config-sections.ts` re-exports this constant and drives the
11
- * cloud's iteration over sections from it; the console reads it directly
12
- * (`useConfigRepairs.ts`). It was spelled out separately in all three until
13
- * wave 2 task 8 (cloud `a307e18`, 2026-09-03) — this is the only spelling
10
+ * The cloud re-exports this constant and drives its iteration over sections
11
+ * from it; an editor reads it directly. Spelling it out separately in each
12
+ * would be three copies — this is the only spelling
14
13
  * since.
15
14
  */
16
15
  export const EXPOSURE_SECTIONS = ['datapoints', 'actions', 'services', 'publishers', 'cameras'];
@@ -107,10 +106,10 @@ function slugOf(path) {
107
106
  * width. Rendered bare, such a key produced a path a reader cannot act
108
107
  * on — and at the root it produced the empty string, which
109
108
  * `validationIssue.path` (`z.string().min(1)`) refuses. That was the cloud
110
- * publishing a finding that fails the cloud's own contract for findings, and
111
- * after D2 stored the draft it cost the whole `configDraftResponse`, not one
112
- * issue: the console's `safeParse` dropped the response and handed the editor
113
- * nothing, for two characters typed.
109
+ * publishing a finding that fails its own contract for findings, and because a
110
+ * draft that does not parse is stored rather than refused it costs the whole
111
+ * `configDraftResponse`, not one issue: a client's `safeParse` drops the
112
+ * response and hands the editor nothing, for two characters typed.
114
113
  *
115
114
  * The quoted spelling is the segment's JSON string literal, and that is the
116
115
  * whole of the reason for choosing it: JSON's string syntax is a subset of
@@ -199,9 +198,9 @@ function isBlank(segment) {
199
198
  *
200
199
  * Escaping on the way out was the alternative and was rejected: `path` is a
201
200
  * wire field (`validationIssue.path`), it is rendered to developers as-is,
202
- * and every recorded expectation in this repo and the cloud's spells it
203
- * unescaped. Changing what the server says about every document to make one
204
- * console lookup total is the larger of the two costs.
201
+ * and every recorded expectation on both sides spells it unescaped. Changing
202
+ * what the server says about every document to make one client-side lookup
203
+ * total is the larger of the two costs.
205
204
  *
206
205
  * So the property this has, and the one its test asserts, is the narrow one:
207
206
  * **a path round-trips when no string segment contains `.` or `[`, and the
package/dist/config.d.ts CHANGED
@@ -362,10 +362,9 @@ export declare const messageBody: z.ZodUnknown;
362
362
  * contract.** This runs inside `publisherConfig`'s failsafe refinement, so a
363
363
  * `RangeError: Maximum call stack size exceeded` did not stay here: it
364
364
  * propagated out of `safeParse`, which is specified to return a result and
365
- * not to throw. Measured on the recursive version — fine at 8 000 levels of
366
- * nesting, throwing at 20 000 — and a flow-style YAML one-liner reaches that
367
- * in about 120 KB of input. A draft PUT would have answered 500 where it
368
- * meant 400.
365
+ * not to throw. A recursive walk survives a few thousand levels of nesting and
366
+ * throws somewhere above that, which a flow-style YAML one-liner reaches in
367
+ * about 120 KB of input — so a draft PUT would answer 500 where it meant 400.
369
368
  *
370
369
  * `seen` is not an optimisation. YAML anchors can express a cycle
371
370
  * (`&a { b: *a }`), and the parser resolves an alias to the same object, so
@@ -560,9 +559,9 @@ export type CameraSource = z.infer<typeof cameraSource>;
560
559
  *
561
560
  * The bound lives here once, and `rest.ts`'s `cameraDescriptor` reuses it —
562
561
  * the same treatment `rateThrottleHz` got, and for the same reason: the
563
- * descriptor used to say `snapshot_interval_ms` while the document said
564
- * seconds, so the cloud converted on one descriptor and not its sibling, with
565
- * nothing in either file saying so.
562
+ * two spellings of one interval — milliseconds in a descriptor, seconds in the
563
+ * document — mean a server converting on one and not the other, with nothing in
564
+ * either file saying so.
566
565
  */
567
566
  export declare const snapshotIntervalSeconds: z.ZodNumber;
568
567
  /**