@fleetless/contracts 1.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 +26 -0
- package/LICENSE +202 -0
- package/NOTICE +17 -0
- package/README.md +88 -0
- package/artifacts/constants.json +24 -0
- package/artifacts/openapi.json +17219 -0
- package/artifacts/routes.json +4605 -0
- package/artifacts/schema/accept-team-invite-request.schema.json +22 -0
- package/artifacts/schema/action-config.schema.json +198 -0
- package/artifacts/schema/alert-list-response.schema.json +172 -0
- package/artifacts/schema/api-error.schema.json +20 -0
- package/artifacts/schema/app-auth-config.schema.json +106 -0
- package/artifacts/schema/app-invitation-list-response.schema.json +57 -0
- package/artifacts/schema/app-invitation.schema.json +69 -0
- package/artifacts/schema/app-list-response.schema.json +82 -0
- package/artifacts/schema/app-mail-template-list-response.schema.json +68 -0
- package/artifacts/schema/app-mail-template.schema.json +54 -0
- package/artifacts/schema/app-oidc-provider-list-response.schema.json +93 -0
- package/artifacts/schema/app-oidc-provider.schema.json +80 -0
- package/artifacts/schema/app-user-list-response.schema.json +111 -0
- package/artifacts/schema/app-user.schema.json +98 -0
- package/artifacts/schema/app.schema.json +69 -0
- package/artifacts/schema/apply-error.schema.json +41 -0
- package/artifacts/schema/asset-list-response.schema.json +288 -0
- package/artifacts/schema/asset-sync-request.schema.json +17 -0
- package/artifacts/schema/asset-sync-response.schema.json +16 -0
- package/artifacts/schema/asset-sync-status.schema.json +136 -0
- package/artifacts/schema/asset.schema.json +68 -0
- package/artifacts/schema/audit-actor.schema.json +32 -0
- package/artifacts/schema/audit-event.schema.json +119 -0
- package/artifacts/schema/audit-list-response.schema.json +144 -0
- package/artifacts/schema/audit-query.schema.json +79 -0
- package/artifacts/schema/auth-error.schema.json +23 -0
- package/artifacts/schema/auth-me-response.schema.json +99 -0
- package/artifacts/schema/auth-ok.schema.json +115 -0
- package/artifacts/schema/authorization-server-metadata.schema.json +80 -0
- package/artifacts/schema/bridge-asset-progress.schema.json +99 -0
- package/artifacts/schema/bridge-assets-available.schema.json +25 -0
- package/artifacts/schema/bridge-camera-state.schema.json +78 -0
- package/artifacts/schema/bridge-config-applied.schema.json +67 -0
- package/artifacts/schema/bridge-hello.schema.json +65 -0
- package/artifacts/schema/bridge-introspect.schema.json +114 -0
- package/artifacts/schema/bridge-job-lost.schema.json +22 -0
- package/artifacts/schema/bridge-job-update.schema.json +100 -0
- package/artifacts/schema/bridge-pong.schema.json +19 -0
- package/artifacts/schema/bridge-pressure.schema.json +292 -0
- package/artifacts/schema/bridge-state.schema.json +24 -0
- package/artifacts/schema/bridge-type-definitions.schema.json +169 -0
- package/artifacts/schema/busy-details.schema.json +115 -0
- package/artifacts/schema/camera-descriptor.schema.json +45 -0
- package/artifacts/schema/camera-list-response.schema.json +58 -0
- package/artifacts/schema/camera-source.schema.json +240 -0
- package/artifacts/schema/cancel-request.schema.json +20 -0
- package/artifacts/schema/client-accept-invitation-request.schema.json +35 -0
- package/artifacts/schema/client-auth.schema.json +18 -0
- package/artifacts/schema/client-cancel.schema.json +45 -0
- package/artifacts/schema/client-identity.schema.json +103 -0
- package/artifacts/schema/client-invoke.schema.json +45 -0
- package/artifacts/schema/client-login-request.schema.json +29 -0
- package/artifacts/schema/client-logout-request.schema.json +14 -0
- package/artifacts/schema/client-mcp-interaction-decision-response.schema.json +15 -0
- package/artifacts/schema/client-mcp-interaction.schema.json +59 -0
- package/artifacts/schema/client-oidc-callback-query.schema.json +28 -0
- package/artifacts/schema/client-oidc-exchange-request.schema.json +21 -0
- package/artifacts/schema/client-oidc-start-query.schema.json +36 -0
- package/artifacts/schema/client-password-reset-confirm-request.schema.json +22 -0
- package/artifacts/schema/client-password-reset-request.schema.json +24 -0
- package/artifacts/schema/client-provider-list-query.schema.json +17 -0
- package/artifacts/schema/client-provider-list-response.schema.json +34 -0
- package/artifacts/schema/client-publish.schema.json +40 -0
- package/artifacts/schema/client-refresh-request.schema.json +14 -0
- package/artifacts/schema/client-register-request.schema.json +44 -0
- package/artifacts/schema/client-resend-verification-request.schema.json +24 -0
- package/artifacts/schema/client-subscribe.schema.json +43 -0
- package/artifacts/schema/client-unsubscribe.schema.json +26 -0
- package/artifacts/schema/client-verify-email-request.schema.json +15 -0
- package/artifacts/schema/cloud-asset-request.schema.json +37 -0
- package/artifacts/schema/cloud-camera-start.schema.json +41 -0
- package/artifacts/schema/cloud-camera-stop.schema.json +26 -0
- package/artifacts/schema/cloud-cancel.schema.json +33 -0
- package/artifacts/schema/cloud-config.schema.json +1635 -0
- package/artifacts/schema/cloud-hello-error.schema.json +23 -0
- package/artifacts/schema/cloud-hello-ok.schema.json +19 -0
- package/artifacts/schema/cloud-introspect-request.schema.json +19 -0
- package/artifacts/schema/cloud-invoke.schema.json +40 -0
- package/artifacts/schema/cloud-ping.schema.json +19 -0
- package/artifacts/schema/cloud-publish.schema.json +28 -0
- package/artifacts/schema/cloud-type-request.schema.json +30 -0
- package/artifacts/schema/command-result.schema.json +175 -0
- package/artifacts/schema/config-draft-response.schema.json +1695 -0
- package/artifacts/schema/config-state.schema.json +124 -0
- package/artifacts/schema/config-version-response.schema.json +1641 -0
- package/artifacts/schema/config-versions-response.schema.json +33 -0
- package/artifacts/schema/create-app-invitation-request.schema.json +40 -0
- package/artifacts/schema/create-app-oidc-provider-request.schema.json +70 -0
- package/artifacts/schema/create-app-request.schema.json +30 -0
- package/artifacts/schema/create-app-user-request.schema.json +42 -0
- package/artifacts/schema/create-robot-request.schema.json +14 -0
- package/artifacts/schema/create-robot-response.schema.json +44 -0
- package/artifacts/schema/create-server-key-response.schema.json +65 -0
- package/artifacts/schema/create-team-invite-request.schema.json +43 -0
- package/artifacts/schema/datapoint-alert-row.schema.json +160 -0
- package/artifacts/schema/datapoint-config.schema.json +366 -0
- package/artifacts/schema/datapoint-display.schema.json +31 -0
- package/artifacts/schema/datapoint-event.schema.json +34 -0
- package/artifacts/schema/datapoint-frame.schema.json +28 -0
- package/artifacts/schema/datapoint-list-response.schema.json +61 -0
- package/artifacts/schema/datapoint-value.schema.json +28 -0
- package/artifacts/schema/developer-login-request.schema.json +19 -0
- package/artifacts/schema/dynamic-client-registration-request.schema.json +60 -0
- package/artifacts/schema/dynamic-client-registration-response.schema.json +68 -0
- package/artifacts/schema/error-frame.schema.json +23 -0
- package/artifacts/schema/exposure-counts.schema.json +39 -0
- package/artifacts/schema/exposure-list-response.schema.json +43 -0
- package/artifacts/schema/fetch-types-request.schema.json +19 -0
- package/artifacts/schema/fetch-types-response.schema.json +163 -0
- package/artifacts/schema/fleetless-user-list-response.schema.json +73 -0
- package/artifacts/schema/fleetless-user.schema.json +60 -0
- package/artifacts/schema/history-buckets-response.schema.json +79 -0
- package/artifacts/schema/history-query.schema.json +58 -0
- package/artifacts/schema/history-response.schema.json +150 -0
- package/artifacts/schema/history-samples-response.schema.json +68 -0
- package/artifacts/schema/introspection-response.schema.json +118 -0
- package/artifacts/schema/invoke-or-service-response.schema.json +141 -0
- package/artifacts/schema/invoke-request.schema.json +23 -0
- package/artifacts/schema/invoke-response.schema.json +125 -0
- package/artifacts/schema/job-actor.schema.json +34 -0
- package/artifacts/schema/job-event.schema.json +158 -0
- package/artifacts/schema/job-response.schema.json +123 -0
- package/artifacts/schema/job-run-list-response.schema.json +222 -0
- package/artifacts/schema/job-run-query.schema.json +95 -0
- package/artifacts/schema/job-run-summary-query.schema.json +23 -0
- package/artifacts/schema/job-run-summary.schema.json +33 -0
- package/artifacts/schema/job-run.schema.json +195 -0
- package/artifacts/schema/job-state.schema.json +11 -0
- package/artifacts/schema/job.schema.json +106 -0
- package/artifacts/schema/latency-bucket.schema.json +63 -0
- package/artifacts/schema/live-session-response.schema.json +41 -0
- package/artifacts/schema/mail-outcome.schema.json +20 -0
- package/artifacts/schema/mail-template-preview-request.schema.json +35 -0
- package/artifacts/schema/mail-template-preview-response.schema.json +31 -0
- package/artifacts/schema/mail-template-problem-details.schema.json +24 -0
- package/artifacts/schema/mcp-consent-grant-list-response.schema.json +52 -0
- package/artifacts/schema/mcp-consent-grant.schema.json +39 -0
- package/artifacts/schema/mcp-robot-datasheet.schema.json +115 -0
- package/artifacts/schema/mcp-role-preview-response.schema.json +134 -0
- package/artifacts/schema/missing-asset-query.schema.json +11 -0
- package/artifacts/schema/oauth-authorize-query.schema.json +47 -0
- package/artifacts/schema/oauth-redirect-response.schema.json +15 -0
- package/artifacts/schema/oauth-token-request.schema.json +47 -0
- package/artifacts/schema/oauth-token-response.schema.json +38 -0
- package/artifacts/schema/org-alerts-query.schema.json +15 -0
- package/artifacts/schema/org-event-dropped.schema.json +26 -0
- package/artifacts/schema/org-event-replay.schema.json +97 -0
- package/artifacts/schema/org-event-subscribe.schema.json +14 -0
- package/artifacts/schema/org-event-unsubscribe.schema.json +14 -0
- package/artifacts/schema/org-event.schema.json +75 -0
- package/artifacts/schema/org-firing-alerts-response.schema.json +178 -0
- package/artifacts/schema/org-health-query.schema.json +13 -0
- package/artifacts/schema/org-latency-query.schema.json +42 -0
- package/artifacts/schema/org-latency-response.schema.json +124 -0
- package/artifacts/schema/org-quota-usage-counts.schema.json +42 -0
- package/artifacts/schema/org-quota-usage.schema.json +102 -0
- package/artifacts/schema/org-quotas.schema.json +51 -0
- package/artifacts/schema/org-usage-query.schema.json +19 -0
- package/artifacts/schema/org-usage-response.schema.json +77 -0
- package/artifacts/schema/org.schema.json +30 -0
- package/artifacts/schema/parameter-invalid-details.schema.json +37 -0
- package/artifacts/schema/parameter-spec.schema.json +120 -0
- package/artifacts/schema/parameter-violation.schema.json +24 -0
- package/artifacts/schema/password-change-request.schema.json +21 -0
- package/artifacts/schema/password-reset-confirm.schema.json +19 -0
- package/artifacts/schema/password-reset-request.schema.json +14 -0
- package/artifacts/schema/patch-app-oidc-provider-request.schema.json +50 -0
- package/artifacts/schema/patch-app-user-request.schema.json +34 -0
- package/artifacts/schema/patch-auth-me-request.schema.json +22 -0
- package/artifacts/schema/patch-fleetless-user-request.schema.json +20 -0
- package/artifacts/schema/patch-org-request.schema.json +15 -0
- package/artifacts/schema/patch-org-response.schema.json +40 -0
- package/artifacts/schema/patch-robot-request.schema.json +15 -0
- package/artifacts/schema/patch-robot-response.schema.json +40 -0
- package/artifacts/schema/pending-team-invite-list-response.schema.json +52 -0
- package/artifacts/schema/pending-team-invite.schema.json +39 -0
- package/artifacts/schema/protected-resource-metadata.schema.json +41 -0
- package/artifacts/schema/publish-config-response.schema.json +21 -0
- package/artifacts/schema/publish-request.schema.json +17 -0
- package/artifacts/schema/publisher-config.schema.json +285 -0
- package/artifacts/schema/put-app-auth-config-request.schema.json +93 -0
- package/artifacts/schema/put-app-mail-template-request.schema.json +35 -0
- package/artifacts/schema/put-config-draft-request.schema.json +13 -0
- package/artifacts/schema/put-datapoint-display-request.schema.json +31 -0
- package/artifacts/schema/put-robot-details-request.schema.json +41 -0
- package/artifacts/schema/put-robot-details-response.schema.json +43 -0
- package/artifacts/schema/rate-limit-details.schema.json +15 -0
- package/artifacts/schema/refresh-request.schema.json +13 -0
- package/artifacts/schema/release-live-query.schema.json +13 -0
- package/artifacts/schema/rename-slug-request.schema.json +23 -0
- package/artifacts/schema/rename-slug-response.schema.json +24 -0
- package/artifacts/schema/resource-health-event.schema.json +72 -0
- package/artifacts/schema/resource-health-list-response.schema.json +80 -0
- package/artifacts/schema/resource-health-state.schema.json +68 -0
- package/artifacts/schema/robot-config-doc.schema.json +1616 -0
- package/artifacts/schema/robot-delete-query.schema.json +12 -0
- package/artifacts/schema/robot-deletion-summary.schema.json +63 -0
- package/artifacts/schema/robot-detail-response.schema.json +262 -0
- package/artifacts/schema/robot-details-doc.schema.json +33 -0
- package/artifacts/schema/robot-jobs-response.schema.json +119 -0
- package/artifacts/schema/robot-latency-series.schema.json +81 -0
- package/artifacts/schema/robot-list-item.schema.json +94 -0
- package/artifacts/schema/robot-list-response.schema.json +106 -0
- package/artifacts/schema/robot.schema.json +30 -0
- package/artifacts/schema/role-list-response.schema.json +48 -0
- package/artifacts/schema/role-permissions.schema.json +61 -0
- package/artifacts/schema/role.schema.json +35 -0
- package/artifacts/schema/ros-graph.schema.json +99 -0
- package/artifacts/schema/server-key-list-response.schema.json +64 -0
- package/artifacts/schema/server-key.schema.json +51 -0
- package/artifacts/schema/service-call-response.schema.json +13 -0
- package/artifacts/schema/service-config.schema.json +198 -0
- package/artifacts/schema/session-tokens.schema.json +28 -0
- package/artifacts/schema/sign-up-request.schema.json +26 -0
- package/artifacts/schema/sign-up-response.schema.json +127 -0
- package/artifacts/schema/slug-usage-response.schema.json +32 -0
- package/artifacts/schema/snapshot-header.schema.json +44 -0
- package/artifacts/schema/snapshot-meta-response.schema.json +85 -0
- package/artifacts/schema/subscribe-error.schema.json +31 -0
- package/artifacts/schema/team-invite.schema.json +57 -0
- package/artifacts/schema/tier-change-request.schema.json +17 -0
- package/artifacts/schema/type-definition.schema.json +144 -0
- package/artifacts/schema/types-response.schema.json +156 -0
- package/artifacts/schema/update-app-request.schema.json +32 -0
- package/artifacts/schema/urdf-completeness.schema.json +50 -0
- package/artifacts/schema/validation-issue.schema.json +43 -0
- package/artifacts/schema/waitlist-request.schema.json +15 -0
- package/artifacts/schema-outgoing/bridge-asset-progress.schema.json +102 -0
- package/artifacts/schema-outgoing/bridge-assets-available.schema.json +26 -0
- package/artifacts/schema-outgoing/bridge-camera-state.schema.json +80 -0
- package/artifacts/schema-outgoing/bridge-config-applied.schema.json +69 -0
- package/artifacts/schema-outgoing/bridge-hello.schema.json +68 -0
- package/artifacts/schema-outgoing/bridge-introspect.schema.json +119 -0
- package/artifacts/schema-outgoing/bridge-job-lost.schema.json +23 -0
- package/artifacts/schema-outgoing/bridge-job-update.schema.json +102 -0
- package/artifacts/schema-outgoing/bridge-pong.schema.json +20 -0
- package/artifacts/schema-outgoing/bridge-type-definitions.schema.json +174 -0
- package/artifacts/schema-outgoing/datapoint-frame.schema.json +29 -0
- package/artifacts/schema-outgoing/snapshot-header.schema.json +45 -0
- package/dist/alerts.d.ts +255 -0
- package/dist/alerts.js +193 -0
- package/dist/app-users.d.ts +606 -0
- package/dist/app-users.js +696 -0
- package/dist/apps.d.ts +175 -0
- package/dist/apps.js +267 -0
- package/dist/assets.d.ts +434 -0
- package/dist/assets.js +546 -0
- package/dist/audit.d.ts +129 -0
- package/dist/audit.js +238 -0
- package/dist/client-auth.d.ts +409 -0
- package/dist/client-auth.js +487 -0
- package/dist/common.d.ts +186 -0
- package/dist/common.js +199 -0
- package/dist/config-issues.d.ts +175 -0
- package/dist/config-issues.js +339 -0
- package/dist/config.d.ts +862 -0
- package/dist/config.js +1988 -0
- package/dist/errors.d.ts +52 -0
- package/dist/errors.js +786 -0
- package/dist/identity.d.ts +549 -0
- package/dist/identity.js +503 -0
- package/dist/index.d.ts +51 -0
- package/dist/index.js +51 -0
- package/dist/introspection.d.ts +99 -0
- package/dist/introspection.js +97 -0
- package/dist/jobs.d.ts +334 -0
- package/dist/jobs.js +345 -0
- package/dist/mcp.d.ts +239 -0
- package/dist/mcp.js +153 -0
- package/dist/oauth.d.ts +344 -0
- package/dist/oauth.js +488 -0
- package/dist/protocol.d.ts +781 -0
- package/dist/protocol.js +715 -0
- package/dist/realtime.d.ts +494 -0
- package/dist/realtime.js +512 -0
- package/dist/rest.d.ts +1989 -0
- package/dist/rest.js +1963 -0
- package/dist/routes.d.ts +94 -0
- package/dist/routes.js +2298 -0
- package/package.json +61 -0
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
/** What a path with no segments at all is called, since `path` may not be empty. */
|
|
2
|
+
export const DOCUMENT_ROOT_PATH = '(document)';
|
|
3
|
+
/**
|
|
4
|
+
* The five sections whose keys are slugs — one namespace across all of them,
|
|
5
|
+
* which is what lets a role grant say `{robot, slug}` without naming a kind.
|
|
6
|
+
* `messages:` is deliberately not among them: its names are their own
|
|
7
|
+
* namespace.
|
|
8
|
+
*
|
|
9
|
+
* `cloud/src/config-sections.ts` re-exports this constant and drives the
|
|
10
|
+
* cloud's iteration over sections from it; the console reads it directly
|
|
11
|
+
* (`useConfigRepairs.ts`). It was spelled out separately in all three until
|
|
12
|
+
* wave 2 task 8 (cloud `a307e18`, 2026-09-03) — this is the only spelling
|
|
13
|
+
* since.
|
|
14
|
+
*/
|
|
15
|
+
export const EXPOSURE_SECTIONS = ['datapoints', 'actions', 'services', 'publishers', 'cameras'];
|
|
16
|
+
/**
|
|
17
|
+
* The refusals `robotConfigDoc` already made, reported as validation issues
|
|
18
|
+
* with their FL-002 codes.
|
|
19
|
+
*
|
|
20
|
+
* **This maps; it does not re-decide.** Seven of the thirteen codes are
|
|
21
|
+
* answered by the schema before a document ever becomes a `RobotConfigDoc`,
|
|
22
|
+
* and `config.ts` attaches `params: { code }` at each site for exactly this —
|
|
23
|
+
* its header lists which codes it decides and which it defers. Reading
|
|
24
|
+
* `params.code` is also the only stable join: the prose of a message is not a
|
|
25
|
+
* contract and matching on it is a join nobody notices breaking.
|
|
26
|
+
*
|
|
27
|
+
* Two refusals carry no `params.code` and are recognised by zod's own issue
|
|
28
|
+
* code instead, which the same header says consumers should do:
|
|
29
|
+
*
|
|
30
|
+
* - `unrecognized_keys` is `unknown_key`. One issue per key, so the path
|
|
31
|
+
* names the offending key rather than its parent.
|
|
32
|
+
* - `invalid_type` **where the value at that path is `null`** is
|
|
33
|
+
* `explicit_null`. The condition is checked against the parsed value and
|
|
34
|
+
* not against the message, which says "received null" — see above. Zod 4
|
|
35
|
+
* does not carry the input on the issue, so the value is navigated to. The
|
|
36
|
+
* sentence differs at the document root, where there is no key to remove:
|
|
37
|
+
* see `EMPTY_DOCUMENT_MESSAGE`.
|
|
38
|
+
*
|
|
39
|
+
* Everything else keeps zod's own code. Those are refusals with no FL-002
|
|
40
|
+
* code — a reversed `min_value`/`max_value` pair, a section over its cap, a
|
|
41
|
+
* key that is not a slug — and inventing a fourteenth code for them would put
|
|
42
|
+
* a code on the wire that no table documents.
|
|
43
|
+
*/
|
|
44
|
+
export function schemaIssues(value, issues) {
|
|
45
|
+
return issues.flatMap((issue) => {
|
|
46
|
+
if (issue.code === 'unrecognized_keys') {
|
|
47
|
+
return (issue.keys ?? []).map((key) => refusal([...issue.path, key], 'unknown_key', `'${key}' is not a key this format defines.`));
|
|
48
|
+
}
|
|
49
|
+
// `'params' in issue` rather than `issue.code === 'custom'`: the cloud's
|
|
50
|
+
// version read `params` off any issue that carried one, and narrowing by
|
|
51
|
+
// code here would be a quieter rule than the one being moved. Zod only
|
|
52
|
+
// declares `params` on the custom issue, so the `in` check is also what
|
|
53
|
+
// types it.
|
|
54
|
+
const declared = 'params' in issue ? issue.params?.['code'] : undefined;
|
|
55
|
+
if (typeof declared === 'string')
|
|
56
|
+
return [refusal(issue.path, declared, issue.message)];
|
|
57
|
+
if (issue.code === 'invalid_type' && valueAt(value, issue.path) === null) {
|
|
58
|
+
return [refusal(issue.path, 'explicit_null', issue.path.length === 0 ? EMPTY_DOCUMENT_MESSAGE : NULL_KEY_MESSAGE)];
|
|
59
|
+
}
|
|
60
|
+
return [refusal(issue.path, issue.code, issue.message)];
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
const NULL_KEY_MESSAGE = 'This key is null. Omission is the only spelling of "not set" in this format — remove the key instead.';
|
|
64
|
+
/**
|
|
65
|
+
* The same refusal at the document root, where **there is no key**.
|
|
66
|
+
*
|
|
67
|
+
* The whole document is the null: the file is empty, holds nothing but
|
|
68
|
+
* comments, or says `null` / `~` outright. All four reach `robotConfigDoc` as
|
|
69
|
+
* a genuine `invalid_type` on `null` at the empty path, so the code is right —
|
|
70
|
+
* but the sentence for a null *key* told the developer to remove a key that
|
|
71
|
+
* does not exist, and "select all, delete" is the commonest way anybody gets
|
|
72
|
+
* here. Since FL-005 D2 stores the draft rather than refusing it, that
|
|
73
|
+
* sentence is what the FINDINGS panel shows persistently for an emptied
|
|
74
|
+
* editor, where it used to ride a one-shot 422 nobody read.
|
|
75
|
+
*
|
|
76
|
+
* It names the smallest legal document rather than only saying what is wrong,
|
|
77
|
+
* because at this path there is no line to jump to and no repair to offer —
|
|
78
|
+
* `repairsFor`'s `explicit_null` branch looks the path up in the text and
|
|
79
|
+
* finds nothing, correctly. The sentence is the entire remedy the developer
|
|
80
|
+
* gets.
|
|
81
|
+
*/
|
|
82
|
+
const EMPTY_DOCUMENT_MESSAGE = 'There is no document in this file — it is empty, holds only comments, or is an explicit null. A fleetless configuration is a mapping, and the smallest one is the single line "fleetless: 1".';
|
|
83
|
+
function refusal(path, code, message) {
|
|
84
|
+
return { path: formatPath(path), slug: slugOf(path), code, message, severity: 'error' };
|
|
85
|
+
}
|
|
86
|
+
const EXPOSURE_SECTION_NAMES = new Set(EXPOSURE_SECTIONS);
|
|
87
|
+
/**
|
|
88
|
+
* The entry a path belongs to, for the console's "jump to it" link. `null`
|
|
89
|
+
* for anything outside the five exposure sections — `messages:` most of all,
|
|
90
|
+
* whose names are their own namespace.
|
|
91
|
+
*/
|
|
92
|
+
function slugOf(path) {
|
|
93
|
+
const [section, slug] = path;
|
|
94
|
+
if (typeof section !== 'string' || !EXPOSURE_SECTION_NAMES.has(section))
|
|
95
|
+
return null;
|
|
96
|
+
return typeof slug === 'string' ? slug : null;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* `['datapoints','a','enum',0]` -> `datapoints.a.enum[0]`, the spelling every
|
|
100
|
+
* other path here uses.
|
|
101
|
+
*
|
|
102
|
+
* **A segment that would render as nothing is written quoted instead.** The
|
|
103
|
+
* last segment of an `unrecognized_keys` or `invalid_key` path is a key the
|
|
104
|
+
* *developer* wrote, and YAML lets that key be empty (`"": 3`), nothing but
|
|
105
|
+
* whitespace, or — see `isBlank` — nothing but characters that occupy no
|
|
106
|
+
* width. Rendered bare, such a key produced a path a reader cannot act
|
|
107
|
+
* on — and at the root it produced the empty string, which
|
|
108
|
+
* `validationIssue.path` (`z.string().min(1)`) refuses. That was the cloud
|
|
109
|
+
* publishing a finding that fails the cloud's own contract for findings, and
|
|
110
|
+
* after D2 stored the draft it cost the whole `configDraftResponse`, not one
|
|
111
|
+
* issue: the console's `safeParse` dropped the response and handed the editor
|
|
112
|
+
* nothing, for two characters typed.
|
|
113
|
+
*
|
|
114
|
+
* The quoted spelling is the segment's JSON string literal, and that is the
|
|
115
|
+
* whole of the reason for choosing it: JSON's string syntax is a subset of
|
|
116
|
+
* YAML's double-quoted scalar syntax, so `""`, `" "` and `"\t"` are each a
|
|
117
|
+
* valid YAML spelling of exactly the key being complained about. The path is
|
|
118
|
+
* therefore text the developer can search their own file for — which is the
|
|
119
|
+
* bar this has to clear. It is also the same move `DOCUMENT_ROOT_PATH` makes
|
|
120
|
+
* for the no-segments case, one level down: give the invisible thing a name.
|
|
121
|
+
*
|
|
122
|
+
* **Only blank segments are quoted.** A segment containing `.` or `[` is
|
|
123
|
+
* still written bare, so it still cannot be read back — see
|
|
124
|
+
* `splitFormatPath`, which documents why escaping those was rejected. That
|
|
125
|
+
* decision is unchanged here on purpose: those paths are wrong for one
|
|
126
|
+
* console lookup, these were wrong on the wire.
|
|
127
|
+
*/
|
|
128
|
+
export function formatPath(path) {
|
|
129
|
+
if (path.length === 0)
|
|
130
|
+
return DOCUMENT_ROOT_PATH;
|
|
131
|
+
return path.reduce((acc, segment, index) => {
|
|
132
|
+
if (typeof segment === 'number')
|
|
133
|
+
return `${acc}[${segment}]`;
|
|
134
|
+
const written = isBlank(segment) ? quoteBlank(segment) : String(segment);
|
|
135
|
+
// Indexed rather than `acc === ''`: "first segment" used to be detected as
|
|
136
|
+
// "nothing written yet", which is how an empty first segment came to be
|
|
137
|
+
// dropped entirely — `formatPath(['', 'a'])` was `'a'`, a path naming a
|
|
138
|
+
// key the document does not have. Quoting means no segment writes nothing
|
|
139
|
+
// any more, but a guard that holds only because of what another line
|
|
140
|
+
// happens to produce is the shape this file exists to avoid.
|
|
141
|
+
return index === 0 ? written : `${acc}.${written}`;
|
|
142
|
+
}, '');
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* The quoted spelling of a blank segment: its JSON string literal, with every
|
|
146
|
+
* zero-width character written as a `\uXXXX` escape.
|
|
147
|
+
*
|
|
148
|
+
* `JSON.stringify` escapes the C0 controls and nothing else, so a zero-width
|
|
149
|
+
* space came back as itself and `"\u200b"` rendered as two quote marks with
|
|
150
|
+
* nothing between them — visible as *a* blank key, but indistinguishable from
|
|
151
|
+
* `""`, and so not findable. The bar this function's caller set itself is that
|
|
152
|
+
* the developer can search their own file for the path, and `\uXXXX` is a JSON
|
|
153
|
+
* escape *and* a YAML double-quoted escape, so the quoted form stays a valid
|
|
154
|
+
* YAML spelling of exactly the key complained about while naming which
|
|
155
|
+
* invisible character it is. Astral format characters are left as
|
|
156
|
+
* `JSON.stringify` wrote them: `\uXXXX` cannot spell them and their surrogate
|
|
157
|
+
* pair already round-trips.
|
|
158
|
+
*/
|
|
159
|
+
function quoteBlank(segment) {
|
|
160
|
+
return JSON.stringify(segment).replace(/\p{Cf}/gu, (char) => {
|
|
161
|
+
const code = char.codePointAt(0);
|
|
162
|
+
return code > 0xffff ? char : `\\u${code.toString(16).padStart(4, '0')}`;
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* A name with nothing in it to read: empty, whitespace all the way through, or
|
|
167
|
+
* made of characters that occupy no width.
|
|
168
|
+
*
|
|
169
|
+
* `trim()` alone is not the test, and that gap was real rather than
|
|
170
|
+
* theoretical: `trim()` removes Unicode `White_Space`, and a zero-width space
|
|
171
|
+
* (`U+200B`) is not white space — it is a format character (`Cf`), as are
|
|
172
|
+
* `U+200C`–`U+200F`, the word joiner `U+2060` and a stray BOM `U+FEFF`. A key
|
|
173
|
+
* spelled with one of those rendered bare and therefore rendered as nothing,
|
|
174
|
+
* which is the exact defect quoting exists to close, one character class over.
|
|
175
|
+
* Format characters are stripped before the trim so both classes, and any
|
|
176
|
+
* mixture of them, reach the same answer.
|
|
177
|
+
*
|
|
178
|
+
* This is the **only** spelling of "blank" in this file. `unquoteBlank` asks
|
|
179
|
+
* the same question on the way back and must get the same answer, or a path
|
|
180
|
+
* `formatPath` quoted stops round-tripping.
|
|
181
|
+
*/
|
|
182
|
+
function isBlank(segment) {
|
|
183
|
+
return typeof segment === 'string' && segment.replace(/\p{Cf}/gu, '').trim() === '';
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* `formatPath` read back — `datapoints.a.enum[0]` -> `['datapoints','a','enum',0]`.
|
|
187
|
+
*
|
|
188
|
+
* It exists because two console call sites split an issue path on `.` alone
|
|
189
|
+
* while the cloud writes sequence indices in brackets, so `ranges[0]` reached
|
|
190
|
+
* a document lookup as one segment that matches no key.
|
|
191
|
+
*
|
|
192
|
+
* **It is not the inverse of `formatPath`, and must not be read as one.**
|
|
193
|
+
* `formatPath` writes `.` and `[n]` as structure and escapes nothing, so a
|
|
194
|
+
* name that contains either is indistinguishable afterwards from the
|
|
195
|
+
* structure it looks like. This is reachable, not theoretical: an
|
|
196
|
+
* `unrecognized_keys` path ends in a key the **developer** chose, and YAML
|
|
197
|
+
* lets that key be `a.b` or `ranges[0]`.
|
|
198
|
+
*
|
|
199
|
+
* Escaping on the way out was the alternative and was rejected: `path` is a
|
|
200
|
+
* wire field (`validationIssue.path`), it is rendered to developers as-is,
|
|
201
|
+
* and every recorded expectation in this repo and the cloud's spells it
|
|
202
|
+
* unescaped. Changing what the server says about every document to make one
|
|
203
|
+
* console lookup total is the larger of the two costs.
|
|
204
|
+
*
|
|
205
|
+
* So the property this has, and the one its test asserts, is the narrow one:
|
|
206
|
+
* **a path round-trips when no string segment contains `.` or `[`, and the
|
|
207
|
+
* path is not the single segment `(document)`.** Outside that, the split is a
|
|
208
|
+
* best guess. What it costs is bounded — the console uses the result to find
|
|
209
|
+
* a line to put a marker on, so a wrong split finds no line and the marker is
|
|
210
|
+
* not placed. It never makes the console assert something false about the
|
|
211
|
+
* document.
|
|
212
|
+
*
|
|
213
|
+
* A **blank** segment is inside that property rather than outside it, and
|
|
214
|
+
* that is new. `formatPath` used to drop an empty first segment entirely
|
|
215
|
+
* (`formatPath(['', 'a'])` was `'a'`, a path naming a different key) and to
|
|
216
|
+
* write a nested one as a trailing `.`; at the root it produced the empty
|
|
217
|
+
* string, which `validationIssue.path`'s `min(1)` refuses outright. It now
|
|
218
|
+
* quotes blank segments, and `unquoteBlank` reads them back, so `['']`,
|
|
219
|
+
* `[' ']` and `['datapoints', 'battery_soc', '']` all round-trip. The single
|
|
220
|
+
* new non-round-trip that buys is a key literally spelled with quote marks
|
|
221
|
+
* around whitespace.
|
|
222
|
+
*/
|
|
223
|
+
export function splitFormatPath(path) {
|
|
224
|
+
if (path === DOCUMENT_ROOT_PATH)
|
|
225
|
+
return [];
|
|
226
|
+
const segments = [];
|
|
227
|
+
for (const chunk of path.split('.')) {
|
|
228
|
+
// A blank segment left `formatPath` quoted, so read it back. Narrowed to
|
|
229
|
+
// *blank* content on purpose: it is the only content `formatPath` quotes,
|
|
230
|
+
// so this cannot misread `"x"`, and the one key it does misread — a key
|
|
231
|
+
// literally spelled with quote marks around whitespace — is the same
|
|
232
|
+
// bounded cost as the `.` and `[` cases below.
|
|
233
|
+
const unquoted = unquoteBlank(chunk);
|
|
234
|
+
if (unquoted !== null) {
|
|
235
|
+
segments.push(unquoted);
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
const match = /^([^[\]]*)((?:\[\d+\])+)$/.exec(chunk);
|
|
239
|
+
if (match === null) {
|
|
240
|
+
segments.push(chunk);
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
// A chunk is `name[0][1]` or a bare `[0]`; the name is absent only when
|
|
244
|
+
// the whole path starts with an index, which `formatPath` does write.
|
|
245
|
+
const [, name, indices] = match;
|
|
246
|
+
if (name !== '')
|
|
247
|
+
segments.push(name);
|
|
248
|
+
for (const index of indices.slice(1, -1).split(']['))
|
|
249
|
+
segments.push(Number(index));
|
|
250
|
+
}
|
|
251
|
+
return segments;
|
|
252
|
+
}
|
|
253
|
+
/** The blank string a chunk quotes, or `null` if it does not quote one. */
|
|
254
|
+
function unquoteBlank(chunk) {
|
|
255
|
+
if (chunk.length < 2 || !chunk.startsWith('"') || !chunk.endsWith('"'))
|
|
256
|
+
return null;
|
|
257
|
+
let value;
|
|
258
|
+
try {
|
|
259
|
+
value = JSON.parse(chunk);
|
|
260
|
+
}
|
|
261
|
+
catch {
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
return typeof value === 'string' && isBlank(value) ? value : null;
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* The value a zod issue's path points at in the document that was parsed.
|
|
268
|
+
*
|
|
269
|
+
* `Object.hasOwn`, not a bare index, for the reason `sectionGet` gives: the
|
|
270
|
+
* value came out of a YAML parse and carries `Object.prototype`, so a path
|
|
271
|
+
* segment like `constructor` would otherwise read a function off the
|
|
272
|
+
* prototype and answer a question about a key the document never had.
|
|
273
|
+
*/
|
|
274
|
+
function valueAt(root, path) {
|
|
275
|
+
let cursor = root;
|
|
276
|
+
for (const segment of path) {
|
|
277
|
+
if (cursor === null || typeof cursor !== 'object')
|
|
278
|
+
return undefined;
|
|
279
|
+
if (Array.isArray(cursor)) {
|
|
280
|
+
if (typeof segment !== 'number')
|
|
281
|
+
return undefined;
|
|
282
|
+
cursor = cursor[segment];
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
const key = String(segment);
|
|
286
|
+
if (!Object.hasOwn(cursor, key))
|
|
287
|
+
return undefined;
|
|
288
|
+
cursor = cursor[key];
|
|
289
|
+
}
|
|
290
|
+
return cursor;
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* A stable hash of a schema object, for asking *is the thing running the one
|
|
294
|
+
* I think it is?*
|
|
295
|
+
*
|
|
296
|
+
* Wave 5's browser sweep enumerates positions against a schema it holds and
|
|
297
|
+
* has to know that the editor is running the same one; the manifest that
|
|
298
|
+
* makes a schema-side change announce itself uses the same number as its
|
|
299
|
+
* baseline. Both are the same question, so there is one implementation of it:
|
|
300
|
+
* a second one on the sweep side would drift, and the gate would then go red
|
|
301
|
+
* for the drift rather than for the schema.
|
|
302
|
+
*
|
|
303
|
+
* Canonical JSON first — object keys sorted at every depth, so a re-ordered
|
|
304
|
+
* `meta()` block is not a change — then FNV-1a over the result, 64 bits as
|
|
305
|
+
* 16 hex characters. Sorting is done through the `JSON.stringify` replacer,
|
|
306
|
+
* which also means a cyclic input throws the engine's own "converting
|
|
307
|
+
* circular structure" TypeError rather than hanging.
|
|
308
|
+
*
|
|
309
|
+
* **Named residual: this is a change detector, not a digest.** FNV-1a is not
|
|
310
|
+
* a cryptographic hash and a collision can be constructed on purpose. It is
|
|
311
|
+
* asked *did this object change since the baseline was recorded*, by the
|
|
312
|
+
* people who wrote both; nothing here defends against someone choosing the
|
|
313
|
+
* input. `crypto.subtle` would be the answer to the other question and is
|
|
314
|
+
* async, which a `data-` attribute rendered during setup cannot be.
|
|
315
|
+
*/
|
|
316
|
+
export function configSchemaHash(schema) {
|
|
317
|
+
return fnv1a64(canonicalJson(schema));
|
|
318
|
+
}
|
|
319
|
+
function canonicalJson(value) {
|
|
320
|
+
return (JSON.stringify(value, (_key, inner) => inner !== null && typeof inner === 'object' && !Array.isArray(inner)
|
|
321
|
+
? Object.fromEntries(Object.keys(inner)
|
|
322
|
+
.sort()
|
|
323
|
+
.map((key) => [key, inner[key]]))
|
|
324
|
+
: inner) ??
|
|
325
|
+
// `JSON.stringify` answers `undefined`, not a string, for `undefined` and
|
|
326
|
+
// for a function. Hashing the word keeps this function total; a caller
|
|
327
|
+
// that passed one by accident gets a hash that matches no baseline, which
|
|
328
|
+
// is the outcome it wants anyway.
|
|
329
|
+
'undefined');
|
|
330
|
+
}
|
|
331
|
+
function fnv1a64(text) {
|
|
332
|
+
const PRIME = 0x100000001b3n;
|
|
333
|
+
const MASK = 0xffffffffffffffffn;
|
|
334
|
+
let hash = 0xcbf29ce484222325n;
|
|
335
|
+
for (let i = 0; i < text.length; i++) {
|
|
336
|
+
hash = ((hash ^ BigInt(text.charCodeAt(i))) * PRIME) & MASK;
|
|
337
|
+
}
|
|
338
|
+
return hash.toString(16).padStart(16, '0');
|
|
339
|
+
}
|