@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.
Files changed (49) hide show
  1. package/CHANGELOG.md +97 -2
  2. package/CODE_OF_CONDUCT.md +83 -0
  3. package/CONTRIBUTING.md +136 -0
  4. package/README.md +49 -13
  5. package/SECURITY.md +55 -0
  6. package/artifacts/openapi.json +11 -11
  7. package/artifacts/routes.json +12 -12
  8. package/artifacts/schema/create-app-oidc-provider-request.schema.json +1 -1
  9. package/dist/alerts.d.ts +23 -28
  10. package/dist/alerts.js +23 -29
  11. package/dist/app-users.d.ts +18 -19
  12. package/dist/app-users.js +18 -20
  13. package/dist/apps.d.ts +21 -25
  14. package/dist/apps.js +42 -52
  15. package/dist/assets.d.ts +70 -132
  16. package/dist/assets.js +130 -223
  17. package/dist/audit.d.ts +14 -15
  18. package/dist/audit.js +28 -55
  19. package/dist/client-auth.d.ts +9 -9
  20. package/dist/client-auth.js +8 -9
  21. package/dist/common.d.ts +29 -37
  22. package/dist/common.js +28 -37
  23. package/dist/config-issues.d.ts +23 -25
  24. package/dist/config-issues.js +17 -17
  25. package/dist/config.d.ts +37 -44
  26. package/dist/config.js +145 -187
  27. package/dist/errors.d.ts +4 -3
  28. package/dist/errors.js +83 -116
  29. package/dist/identity.d.ts +24 -27
  30. package/dist/identity.js +23 -27
  31. package/dist/index.d.ts +4 -4
  32. package/dist/index.js +14 -15
  33. package/dist/introspection.d.ts +7 -6
  34. package/dist/introspection.js +6 -6
  35. package/dist/jobs.d.ts +16 -16
  36. package/dist/jobs.js +24 -29
  37. package/dist/mcp.d.ts +14 -15
  38. package/dist/mcp.js +12 -14
  39. package/dist/oauth.d.ts +21 -27
  40. package/dist/oauth.js +33 -43
  41. package/dist/protocol.d.ts +51 -62
  42. package/dist/protocol.js +107 -139
  43. package/dist/realtime.d.ts +53 -68
  44. package/dist/realtime.js +78 -104
  45. package/dist/rest.d.ts +183 -244
  46. package/dist/rest.js +305 -399
  47. package/dist/routes.d.ts +4 -3
  48. package/dist/routes.js +33 -32
  49. package/package.json +12 -7
package/dist/routes.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ // SPDX-License-Identifier: Apache-2.0
1
2
  /**
2
3
  * **The route manifest: every HTTP route the cloud registers, declared once.**
3
4
  *
@@ -62,9 +63,9 @@ export interface RouteEntry {
62
63
  * It exists because the OpenAPI render marked every request body `required`,
63
64
  * which documented a refusal `POST /api/robots/:id/jobs/:slug/cancel` does not
64
65
  * make: it parses `request.body ?? {}`, and a bodyless `POST` was every
65
- * caller's shape before `job_id` existed. Getting that wrong in the other
66
- * direction is what W5's worst bug was — a bodyless `POST` with a JSON
67
- * content type rejected outright — so this is a fact worth carrying rather
66
+ * caller's shape before `job_id` existed. Getting it wrong in the other
67
+ * direction — a bodyless `POST` with a JSON content type rejected outright —
68
+ * makes whole routes unreachable, so this is a fact worth carrying rather
68
69
  * than a default worth assuming.
69
70
  */
70
71
  readonly requestOptional?: true;
package/dist/routes.js CHANGED
@@ -1,3 +1,4 @@
1
+ // SPDX-License-Identifier: Apache-2.0
1
2
  import { appListResponse, createAppRequest, createServerKeyResponse, app as appSchema, role, roleListResponse, rolePermissions, serverKeyListResponse, updateAppRequest, } from './apps.js';
2
3
  import { alertListResponse, orgAlertsQuery, orgFiringAlertsResponse } from './alerts.js';
3
4
  import { asset, assetListResponse, assetSyncRequest, assetSyncResponse, assetSyncStatus, missingAssetQuery } from './assets.js';
@@ -62,16 +63,16 @@ export const IN_HANDLER_ROUTES = [
62
63
  * **The three refusals every `auth: 'developer'` route inherits from its guard**,
63
64
  * spelled once rather than retyped eighty times.
64
65
  *
65
- * They are the arms of `cloud/src/auth.ts`'s `requireDeveloper`: no bearer or an
66
- * unverifiable one is `401 unauthorized`, an expired one `401 token_expired`, a
66
+ * They are the arms of the developer guard: no bearer or an unverifiable one is
67
+ * `401 unauthorized`, an expired one `401 token_expired`, a
67
68
  * vanished account or a bumped `token_version` `401 token_revoked`.
68
69
  *
69
70
  * **Three, not four: `403 forbidden` went with the Org Admins group.** It stood
70
71
  * for "an account that is no longer in the org's Org Admins group", and the
71
- * two-space cut leaves a Fleetless user who IS the team — `TokenRefusalReason`
72
- * in `cloud/src/auth.ts` is `'expired' | 'revoked' | 'invalid'`, and
73
- * `createRequireDeveloper` answers 401 codes only. A removed team member now
74
- * gets `401 token_revoked`; a reader of the API reference who branched on
72
+ * two identity spaces leave a Fleetless user who IS the team, and the guard's
73
+ * refusal reasons are `'expired' | 'revoked' | 'invalid'` — 401 codes only. A
74
+ * removed team member gets `401 token_revoked`; a reader of the API reference
75
+ * who branched on
75
76
  * `forbidden` to render "you lost console access" was branching on an answer no
76
77
  * developer-guarded route can send. The `forbidden` producers that remain
77
78
  * (`history.ts`, `commands.ts`, `cameras.ts`, `robots.ts`, `mcp.ts`) all sit on
@@ -92,15 +93,15 @@ const DEVELOPER_GUARD = ['unauthorized', 'token_expired', 'token_revoked'];
92
93
  * **The same four codes, not five.** `sendTokenRefusal` has a fifth arm,
93
94
  * `account_blocked`, and this list carried it for exactly one commit. Nothing
94
95
  * reaches it: `TokenRefusalReason` admits `'blocked'`, but no site in
95
- * `cloud/src` constructs one — the only reasons ever returned are `'invalid'`,
96
- * `'revoked'` and `'forbidden'`. `auth.ts` says why on the line where the check
97
- * used to be: D2 replaced "block the account" with "remove the assignment", so
98
- * an app user who loses access loses it because no assignment resolves, and
99
- * there is no blocked state left to re-check.
96
+ * the cloud constructs one — the only reasons ever returned are `'invalid'`,
97
+ * `'revoked'` and `'forbidden'`. Access is withdrawn by removing an assignment
98
+ * rather than by blocking an account, so an app user who loses access loses it
99
+ * because no assignment resolves, and there is no blocked state left to
100
+ * re-check.
100
101
  *
101
102
  * Listing it would have documented a refusal no caller can receive — the same
102
- * mistake as the `invalid_token` above, found by review rather than by any test
103
- * here, because a code in `ERROR_CODES` satisfies every check this file has.
103
+ * mistake as the `invalid_token` above. No check here catches it, because a
104
+ * code in `ERROR_CODES` satisfies every one of them.
104
105
  *
105
106
  * It keeps `forbidden`, which `DEVELOPER_GUARD` no longer carries: this guard's
106
107
  * routes have live 403 producers — a slug the role does not grant
@@ -419,7 +420,7 @@ export const ROUTES = [
419
420
  'listing beside it. `POST /api/client/register` answers `202` to the same fact, because there the caller is a stranger. `404 not_found` ' +
420
421
  'is the app, or a `role_id` that is not a role of it — a role of another app is refused rather than stored, since a user holding one ' +
421
422
  'would carry rights nothing in this app can resolve. **The password policy answers `400 validation_error`**, not a code of its own: the ' +
422
- 'twelve-character minimum is the `password` field\'s schema rule, and every route in this repository that takes a password refuses a ' +
423
+ 'twelve-character minimum is the `password` field\'s schema rule, and every route that takes a password refuses a ' +
423
424
  'short one exactly the way it refuses any other malformed field. An account created here is `active` immediately: a developer entering ' +
424
425
  'somebody by hand has made the decision the verification mail automates, and its address counts as proven. `409 target_state_conflict` ' +
425
426
  'names `default_role_id` when `role_id` is absent and the app has no default role, or its default names a role that no longer resolves ' +
@@ -589,7 +590,7 @@ export const ROUTES = [
589
590
  '`POST /api/client/invitations/accept`, the same answer one that expired or never existed gets — the developer withdrew it deliberately, ' +
590
591
  'and an answer saying so would tell whoever still holds the link that it was once real.',
591
592
  },
592
- /* --------------------------------------- the app's OIDC providers (D4) */
593
+ /* ------------------------------------------- the app's OIDC providers */
593
594
  {
594
595
  method: 'GET', path: '/api/apps/:id/oidc-providers', section: 'apps',
595
596
  summary: "Lists every OIDC provider configured on the app, enabled or not.",
@@ -974,7 +975,7 @@ export const ROUTES = [
974
975
  audience: 'internal', auth: 'none', rateLimited: true, ownerTier: false, status: 200,
975
976
  params: [], query: null, request: null, response: null,
976
977
  errors: ['rate_limited', 'validation_error', 'token_spent'], transport: 'http',
977
- notes: 'The identifier-first step, with nothing left to identify: Fleetless users are password-only (design D1/D7), so **this step does not ' +
978
+ notes: 'The identifier-first step, with nothing left to identify: Fleetless users are password-only, so **this step does not ' +
978
979
  'read the address at all** — it renders the password card for a known address, an unknown one and an empty one alike, and the login ' +
979
980
  'step below answers the same `401` for all three. That is a property of the shape rather than of two branches agreeing: there is no ' +
980
981
  'lookup here whose result could differ. A browser form post gets the password card; a JSON caller gets `{ "next" }`, which has no ' +
@@ -1132,10 +1133,10 @@ export const ROUTES = [
1132
1133
  'a token belongs to its own app\'s endpoint. The code is `forbidden` rather than `mcp_disabled` because nothing is switched off — the ' +
1133
1134
  'caller is at the wrong server — and per-app sign-in mints exactly such tokens, so the two states must not share a word. ' +
1134
1135
  '`mcp_access_denied` is gone with the per-user override and the ' +
1135
- 'group flag it read: every Fleetless user has MCP access here (D1). Stateless: a fresh transport per request, no session id, nothing ' +
1136
+ 'group flag it read: every Fleetless user has MCP access here. Stateless: a fresh transport per request, no session id, nothing ' +
1136
1137
  'survives the call.',
1137
1138
  },
1138
- /* ----------------------------------------- mcp (one app's own server, D7) */
1139
+ /* --------------------------------------------- mcp (one app's own server) */
1139
1140
  {
1140
1141
  method: 'POST', path: MCP_APP.endpoint, section: 'mcp',
1141
1142
  summary: "One app's MCP endpoint: the same stateless Streamable HTTP transport, carrying that app's robots.",
@@ -1171,13 +1172,13 @@ export const ROUTES = [
1171
1172
  params: [APP_IDENTIFIER], query: null, request: null, response: null,
1172
1173
  errors: ['not_found', 'unauthorized', 'forbidden'], transport: 'http',
1173
1174
  notes: 'MCP\'s Streamable HTTP gives this path three verbs: `POST` carries JSON-RPC, `GET` opens the server-initiated SSE stream, and `DELETE` ' +
1174
- 'ends a session. This server has no sessions — the argument is in `MCP_PROTOCOL_VERSION`\'s own note, and W8\'s second cloud instance is ' +
1175
- 'where a per-process session map would break — so `GET` and `DELETE` answer `405`, which is what a client is built to fall back from. ' +
1176
- '\n\n**The `405` is this cloud\'s own answer, not the SDK\'s**, and the difference was measured: MCP SDK 1.30.0 opens an SSE stream on ' +
1175
+ 'ends a session. This server has no sessions — the argument is in `MCP_PROTOCOL_VERSION`\'s own note, and a per-process session map is ' +
1176
+ 'what breaks at the second cloud instance — so `GET` and `DELETE` answer `405`, which is what a client is built to fall back from. ' +
1177
+ '\n\n**The `405` is this cloud\'s own answer, not the SDK\'s**, and the two differ: MCP SDK 1.30.0 opens an SSE stream on ' +
1177
1178
  '`GET` (`handleGetRequest`) and answers `200` on `DELETE` (`handleDeleteRequest`), neither of which a stateless server has any ' +
1178
1179
  'business doing, so the cloud writes the `405` itself in the transport\'s own JSON-RPC error shape with `Allow: POST`. \n\n**The row exists so that the `405` is not a `404`.** An unregistered verb answers ' +
1179
1180
  '`404`, and at a path whose last segment is an app identifier a `404` already means *no such app* — one answer for two states, which is ' +
1180
- 'the failure this project keeps paying for. Registering the verb lets the endpoint say "this app\'s server is here; this verb is not ' +
1181
+ 'one answer for two states. Registering the verb lets the endpoint say "this app\'s server is here; this verb is not ' +
1181
1182
  'part of it". The central `/mcp` registers neither verb and does not need to: its path takes no parameter, so nothing can misread its ' +
1182
1183
  '`404`. \n\n**The `405` body is the transport\'s JSON-RPC error object, not the `apiError` envelope.** The three codes above are the ' +
1183
1184
  'refusals that come *first* — the app, its switch, then the bearer, in the order `POST` describes — and they are `apiError` because ' +
@@ -1227,7 +1228,7 @@ export const ROUTES = [
1227
1228
  'canonical public base, never from the friendly `mcp.fleetless.dev` alias or the request\'s `Host`**, because a client checks a minted ' +
1228
1229
  'token\'s `iss` and `aud` against these exact strings. \n\n**Unlike the central document, `authorization_endpoint` does not move to an ' +
1229
1230
  'auth-portal origin**, and there is nothing here for one to serve: this authorization step renders no Fleetless page at all. It ' +
1230
- 'redirects to the app\'s own `mcp_login_url` (D7), which is on the developer\'s origin already.',
1231
+ 'redirects to the app\'s own `mcp_login_url`, which is on the developer\'s origin already.',
1231
1232
  },
1232
1233
  {
1233
1234
  method: 'POST', path: MCP_APP.register, section: 'mcp',
@@ -1235,7 +1236,7 @@ export const ROUTES = [
1235
1236
  audience: 'client', auth: 'none', rateLimited: true, ownerTier: false, status: 201,
1236
1237
  params: [APP_IDENTIFIER], query: null, request: dynamicClientRegistrationRequest, response: dynamicClientRegistrationResponse,
1237
1238
  errors: ['rate_limited', 'not_found'], transport: 'http',
1238
- notes: 'RFC 7591, the same wire and the same handler as `POST /mcp/oauth/register` — `registerMcpDynamicClient`, one implementation, because a ' +
1239
+ notes: 'RFC 7591, the same wire and the same handler as `POST /mcp/oauth/register` — one implementation, because a ' +
1239
1240
  'second answer to "is this redirect URI acceptable" would agree with the first only by luck. The request schema is what the endpoint ' +
1240
1241
  'accepts rather than what it parses, for the reason that row gives: §3.2.2 needs two distinguishable refusals and one `safeParse` ' +
1241
1242
  'failure offers one. `client_name` and `redirect_uris` are read; `grant_types`, `response_types` and `scope` are accepted and ignored, ' +
@@ -1255,7 +1256,7 @@ export const ROUTES = [
1255
1256
  params: [APP_IDENTIFIER], query: oauthAuthorizeQuery, request: null, response: null,
1256
1257
  errors: ['not_found', 'target_state_conflict'], transport: 'http',
1257
1258
  notes: 'The same query as `GET /mcp/oauth/authorize`, read the same way — parameter by parameter, because the answers differ and one parse ' +
1258
- 'would collapse them. \n\n**Fleetless renders no page here, and that is the whole of D7.** The route writes an interaction — ten minutes, as the OIDC ones live ' +
1259
+ 'would collapse them. \n\n**Fleetless renders no page here**, and that is the whole of it. The route writes an interaction — ten minutes, as the OIDC ones live ' +
1259
1260
  '— and redirects to `appAuthConfig.mcp_login_url` with `{interaction}` filled in. The app then authenticates the person with its own ' +
1260
1261
  'UI, reads `GET /api/client/mcp/interactions/:id` to show the client\'s claimed name and the scopes it asked for, and calls approve or ' +
1261
1262
  'deny. \n\nClient and `redirect_uri` are validated first and a failure there never redirects — the open-redirect discipline `GET ' +
@@ -1272,7 +1273,7 @@ export const ROUTES = [
1272
1273
  'under `/api/client/mcp/interactions/:id`. `409 ' +
1273
1274
  'target_state_conflict` names `mcp_login_url` with rule `not_set`: MCP is enabled and no page is configured to send the person to. It ' +
1274
1275
  'is the same code and the same shape `send_mail` answers for an unconfigured `invite_url`, and the refusal is the honest one — ' +
1275
- 'Fleetless has nowhere to redirect, and rendering a page of its own instead would contradict D2.',
1276
+ 'Fleetless has nowhere to redirect, and rendering a page of its own would contradict the rule that Fleetless shows an app user no page.',
1276
1277
  },
1277
1278
  {
1278
1279
  method: 'POST', path: MCP_APP.token, section: 'mcp',
@@ -1515,7 +1516,7 @@ export const ROUTES = [
1515
1516
  '\n\n**It lists `rate_limited` and no other code, because every sign-in outcome it has is a redirect.** Success and failure alike are ' +
1516
1517
  'a `302` to the app\'s own ' +
1517
1518
  '`redirect_uri`: `?code=…&state=…` when a session was resolved, `?error=<clientOidcErrorCode>&state=…` when it was not, so the app ' +
1518
- 'renders its own message and can bind either answer to the request it started. Fleetless shows an app user no page (D2). \n\n**The one ' +
1519
+ 'renders its own message and can bind either answer to the request it started. Fleetless shows an app user no page. \n\n**The one ' +
1519
1520
  'exception is a `state` that resolves to no interaction** — unknown, hand-edited, or past its ten minutes. Then there is no confirmed ' +
1520
1521
  'redirect target to carry the answer to, and bouncing a browser to an unvalidated one is the hole the whole flow is arranged to avoid, ' +
1521
1522
  'so the cloud renders an HTML problem page at `400`. That is the only Fleetless-rendered surface an app user can reach. It is HTML ' +
@@ -1538,7 +1539,7 @@ export const ROUTES = [
1538
1539
  'is, not how vague the answer is: a mailed one-time code is a credential, an interaction id names a pending request, and the two ' +
1539
1540
  'deserve different advice on the app\'s own page.',
1540
1541
  },
1541
- /* --------------------------- the app's own MCP consent screen (D7) */
1542
+ /* ------------------------------- the app's own MCP consent screen */
1542
1543
  {
1543
1544
  method: 'GET', path: '/api/client/mcp/interactions/:id', section: 'client-auth',
1544
1545
  summary: 'Reads a pending MCP authorization so the app can draw its own consent screen.',
@@ -1573,7 +1574,7 @@ export const ROUTES = [
1573
1574
  query: null, request: null, response: clientMcpInteractionDecisionResponse,
1574
1575
  errors: [...CLIENT_GUARD, 'rate_limited', 'interaction_expired', 'mcp_disabled'], transport: 'http',
1575
1576
  notes: 'The person is already signed in **at the app**, by whatever means that app uses, and this is the app telling Fleetless what they ' +
1576
- 'decided. Fleetless never sees that sign-in, which is D7 in one sentence. \n\nThe guard admits all three caller kinds and the handler ' +
1577
+ 'decided. Fleetless never sees that sign-in. \n\nThe guard admits all three caller kinds and the handler ' +
1577
1578
  'takes one: a developer bearer or a server key reaching this is `401 unauthorized`, because a consent is a person\'s and a server key ' +
1578
1579
  'is not a person — the same shape `POST /api/client/password/change` has. `403 mcp_disabled` is the app\'s switch, re-read here as it is ' +
1579
1580
  'on every request — and it is the one refusal on this surface that names the switch, because reaching it needs an app-user session ' +
@@ -1614,7 +1615,7 @@ export const ROUTES = [
1614
1615
  params: [], query: null, request: null, response: mcpConsentGrantListResponse,
1615
1616
  errors: [...CLIENT_GUARD], transport: 'http',
1616
1617
  notes: '**So the developer\'s app can offer a "connected apps" screen of its own**, which is the only place an end user could ever be shown ' +
1617
- 'this: Fleetless renders no page for an app\'s users (D2), and the console is the developer\'s tool rather than their customers\'. ' +
1618
+ 'this: Fleetless renders no page for an app\'s users, and the console is the developer\'s tool rather than their customers\'. ' +
1618
1619
  '\n\nThe answer is about the bearer\'s own account and takes no user id — there is no id to pass and therefore nothing to pass the ' +
1619
1620
  'wrong one. The guard admits all three caller kinds because it is shared, and the handler takes one: a developer bearer or a server ' +
1620
1621
  'key is `401 unauthorized`, the shape `POST /api/client/password/change` has, because a consent is a person\'s and a server key is not ' +
@@ -1936,7 +1937,7 @@ export const ROUTES = [
1936
1937
  errors: [...CLIENT_GUARD, 'invalid_uuid', 'not_found', 'capability_required', 'validation_error'], transport: 'http',
1937
1938
  notes: 'Needs the `action_history` capability, and **this route is what makes that switch mean something** — it was unkeepable while nothing ' +
1938
1939
  'durable recorded what had run. Two residuals worth stating rather than implying away. `history` is a syntactically valid slug and ' +
1939
- 'Fastify matches a static segment first, so a robot with a service literally slugged `history` can no longer be **read** through ' +
1940
+ 'The router matches a static segment first, so a robot with a service literally slugged `history` can no longer be **read** through ' +
1940
1941
  '`GET /api/robots/:id/jobs/:slug`; invoking, cancelling and the listing are unaffected. And a run row names its actor by email address, ' +
1941
1942
  'so an end user holding this capability learns which other people have been driving the machine. `robot_id` in the query is shared with ' +
1942
1943
  'the org-wide read; a *different* one here is refused rather than quietly answered about the robot in the path.',
@@ -2277,7 +2278,7 @@ export const ROUTES = [
2277
2278
  'upload token minted by `POST /api/robots/:id/assets/sync`, verified in a `preParsing` hook so a refusal precedes the work rather than ' +
2278
2279
  'following it: a `preHandler` would already have buffered the whole file. The announced size is refused there too, before a single byte ' +
2279
2280
  'is read — it is an announcement and not a proof, so it only ever rejects early and never accepts early, and a body that lies small is ' +
2280
- 'still caught by the real length check. Past both, Fastify\'s own body limit answers a bare `413 bad_request` with neither ceiling nor ' +
2281
+ 'still caught by the real length check. Past both, the server\'s own body limit answers a bare `413 bad_request` with neither ceiling nor ' +
2281
2282
  'size in it. Rate limited per robot inside that same hook, which is why `rateLimited` is `false`: there is no rate-limiting preHandler ' +
2282
2283
  'registered on this route.',
2283
2284
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fleetless/contracts",
3
- "version": "1.0.0",
3
+ "version": "1.0.3",
4
4
  "description": "Fleetless wire contracts: the bridge-cloud protocol, the REST API schemas and the error codes, as zod schemas with generated JSON Schema and OpenAPI artifacts.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Dehne Robotik GmbH",
@@ -10,7 +10,8 @@
10
10
  "url": "https://github.com/fleetless/contracts"
11
11
  },
12
12
  "bugs": {
13
- "url": "https://github.com/fleetless/contracts/issues"
13
+ "url": "https://github.com/fleetless/contracts/issues",
14
+ "email": "security@fleetless.dev"
14
15
  },
15
16
  "keywords": [
16
17
  "fleetless",
@@ -36,21 +37,25 @@
36
37
  "LICENSE",
37
38
  "NOTICE",
38
39
  "README.md",
39
- "CHANGELOG.md"
40
+ "CHANGELOG.md",
41
+ "SECURITY.md",
42
+ "CONTRIBUTING.md",
43
+ "CODE_OF_CONDUCT.md"
40
44
  ],
41
45
  "engines": {
42
46
  "node": ">=20"
43
47
  },
44
48
  "scripts": {
45
49
  "typecheck": "tsc --noEmit && tsc --noEmit -p test/tsconfig.json",
46
- "build": "tsc",
47
- "prepare": "tsc",
50
+ "build": "tsc && node scripts/stamp-dist.mjs",
51
+ "prepare": "tsc && node scripts/stamp-dist.mjs",
48
52
  "test": "vitest run",
49
53
  "artifacts": "tsx scripts/export-schemas.ts",
50
- "test:pack": "node scripts/verify-pack.mjs"
54
+ "test:pack": "node scripts/verify-pack.mjs",
55
+ "prepublishOnly": "node scripts/refuse-manual-publish.mjs"
51
56
  },
52
57
  "dependencies": {
53
- "zod": "^4.0.0"
58
+ "zod": "^4.4.3"
54
59
  },
55
60
  "devDependencies": {
56
61
  "@types/node": "^26.2.0",