@fleetless/contracts 2.0.0 → 4.0.0-next.1

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/index.d.ts CHANGED
@@ -3,7 +3,7 @@ export { SLUG_RULE, ROS_NAME_RULE, ROS_TYPE_NAME_RULE, FIELD_PATH_RULE, slug, ro
3
3
  export type { ApplyErrorKind, ApplyError } from './common.js';
4
4
  export { MCP_PROTOCOL_VERSION, MCP_ENDPOINT_PATH, mcpAppEndpointPath, MCP_APP_PATHS, MCP_TOOL_NAME_MAX, MCP_ASSET_LINK_PATH, MCP_ASSET_LINK_TTL_MS, mcpToolNamePattern, mcpToolKind, mcpExposure, mcpCapabilities, mcpRobotDatasheet, mcpRolePreviewResponse, } from './mcp.js';
5
5
  export type { McpAppPaths, McpToolKind, McpExposure, McpCapabilities, McpRobotDatasheet, McpRolePreviewResponse, } from './mcp.js';
6
- export { PROTOCOL_VERSION, PROTOCOL_VERSIONS, PROTOCOL_SUNSET_DAYS, LATEST_BRIDGE_VERSION, protocolStatus, minimumProtocolVersion, sunsetOf, statusFromTable, bridgeHello, cloudHelloOk, cloudHelloError, cloudPing, bridgePong, bridgeLinkMode, datapointFrame, bridgeState, cloudConfig, bridgeConfigApplied, cloudIntrospectRequest, bridgeIntrospect, cloudTypeRequest, bridgeTypeDefinitions, cloudInvoke, cloudCancel, cloudPublish, bridgeJobUpdate, bridgeJobLost, snapshotHeader, cloudCameraStart, cloudCameraStop, bridgeCameraState, SNAPSHOT_MAX_BYTES, CLOSE_ROBOT_DELETED, CLOSE_TOKEN_ROTATED, bridgeAssetsAvailable, cloudAssetRequest, bridgeAssetProgress, activeJob, DEFAULT_PATIENCE_MS, MAX_PATIENCE_MS, MIN_PATIENCE_MS, } from './protocol.js';
6
+ export { PROTOCOL_VERSION, PROTOCOL_VERSIONS, PROTOCOL_SUNSET_DAYS, LATEST_BRIDGE_VERSION, protocolStatus, minimumProtocolVersion, sunsetOf, statusFromTable, bridgeHello, cloudHelloOk, cloudHelloError, cloudPing, bridgePong, bridgeLinkMode, datapointFrame, bridgeState, cloudConfig, bridgeConfigApplied, cloudIntrospectRequest, bridgeIntrospect, cloudTypeRequest, bridgeTypeDefinitions, cloudInvoke, cloudCancel, cloudPublish, bridgeJobUpdate, bridgeJobLost, snapshotHeader, cloudCameraStart, cloudCameraStop, bridgeCameraState, SNAPSHOT_MAX_BYTES, CLOSE_ROBOT_DELETED, CLOSE_TOKEN_ROTATED, bridgeAssetsAvailable, cloudAssetRequest, bridgeAssetProgress, activeJob, DEFAULT_PATIENCE_MS, MAX_PATIENCE_MS, MIN_PATIENCE_MS, JOB_HEARTBEAT_INTERVAL_MS, JOB_HEARTBEAT_TIMEOUT_MS, JOB_OFFLINE_GRACE_MS, } from './protocol.js';
7
7
  export type { ProtocolVersionEntry, ProtocolStatus, BridgeHello, CloudHelloOk, CloudHelloError, CloudPing, BridgePong, BridgeLinkMode, DatapointFrame, BridgeState, CloudConfig, BridgeConfigApplied, CloudIntrospectRequest, BridgeIntrospect, CloudTypeRequest, BridgeTypeDefinitions, CloudInvoke, CloudCancel, CloudPublish, BridgeJobUpdate, BridgeJobLost, SnapshotHeader, CloudCameraStart, CloudCameraStop, BridgeCameraState, ActiveJob, BridgeAssetsAvailable, CloudAssetRequest, BridgeAssetProgress, } from './protocol.js';
8
8
  export { jobState, job, jobEvent, busyDetails, publisherBusyDetails, jobQueueFullDetails } from './jobs.js';
9
9
  export type { JobState, Job, JobEvent, BusyDetails, PublisherBusyDetails, JobQueueFullDetails, } from './jobs.js';
@@ -31,14 +31,14 @@ export { clientAuth, authOk, authError, clientInvoke, clientCancel, clientPublis
31
31
  export type { ClientAuth, AuthOk, AuthError, ClientInvoke, ClientCancel, ClientPublish, CommandResult, ErrorFrame, ClientSubscribe, ClientUnsubscribe, SubscribeError, DatapointEvent, ResourceHealthEvent, ResourceHealthCleared, LiveSessionEndReason, LiveSessionEvent, OrgEventKind, OrgEventSeverity, OrgEvent, OrgEventSubscribe, OrgEventUnsubscribe, OrgEventReplay, OrgEventDropped, } from './realtime.js';
32
32
  export { password, org, patchOrgResponse, sessionTokens, refreshRequest, signUpRequest, signUpResponse, waitlistRequest, developerLoginRequest, USER_DISPLAY_NAME_MAX, orgAdminTier, fleetlessUser, fleetlessUserListResponse, createTeamInviteRequest, teamInvite, pendingTeamInvite, pendingTeamInviteListResponse, acceptTeamInviteRequest, patchFleetlessUserRequest, tierChangeRequest, mailStatus, tierRequiredDetails, passwordChangeRequest, passwordResetRequest, passwordResetConfirm, idpIssuer, authMeResponse, patchOrgRequest, patchAuthMeRequest, } from './identity.js';
33
33
  export type { Org, PatchOrgResponse, SessionTokens, RefreshRequest, SignUpRequest, SignUpResponse, WaitlistRequest, DeveloperLoginRequest, OrgAdminTier, FleetlessUser, FleetlessUserListResponse, CreateTeamInviteRequest, TeamInvite, PendingTeamInvite, PendingTeamInviteListResponse, AcceptTeamInviteRequest, PatchFleetlessUserRequest, TierChangeRequest, MailStatus, TierRequiredDetails, PasswordChangeRequest, PasswordResetRequest, PasswordResetConfirm, IdpIssuer, AuthMeResponse, PatchOrgRequest, PatchAuthMeRequest, } from './identity.js';
34
- export { appIdentifier, app, appListResponse, createAppRequest, updateAppRequest, serverKeyToken, serverKey, serverKeyListResponse, createServerKeyResponse, role, roleListResponse, rolePermissions, } from './apps.js';
35
- export type { App, AppListResponse, CreateAppRequest, UpdateAppRequest, ServerKey, ServerKeyListResponse, CreateServerKeyResponse, Role, RoleListResponse, RolePermissions, } from './apps.js';
34
+ export { appIdentifier, app, appListResponse, appDeletionSummary, createAppRequest, updateAppRequest, serverKeyToken, serverKey, serverKeyListResponse, createServerKeyResponse, role, roleListResponse, rolePermissions, } from './apps.js';
35
+ export type { App, AppListResponse, AppDeletionSummary, CreateAppRequest, UpdateAppRequest, ServerKey, ServerKeyListResponse, CreateServerKeyResponse, Role, RoleListResponse, RolePermissions, } from './apps.js';
36
36
  export { clientLoginRequest, clientRefreshRequest, clientLogoutRequest, clientRegisterRequest, clientVerifyEmailRequest, clientResendVerificationRequest, clientPasswordResetRequest, clientPasswordResetConfirmRequest, clientAcceptInvitationRequest, CLIENT_OIDC_CALLBACK_PATH, clientProviderListQuery, clientProviderListResponse, clientOidcStartQuery, clientOidcCallbackQuery, clientOidcExchangeRequest, clientOidcErrorCode, clientMcpInteraction, clientMcpInteractionDecisionResponse, mcpConsentGrant, mcpConsentGrantListResponse, clientIdentity, } from './client-auth.js';
37
37
  export type { ClientLoginRequest, ClientRefreshRequest, ClientLogoutRequest, ClientRegisterRequest, ClientVerifyEmailRequest, ClientResendVerificationRequest, ClientPasswordResetRequest, ClientPasswordResetConfirmRequest, ClientAcceptInvitationRequest, ClientProviderListQuery, ClientProviderListResponse, ClientOidcStartQuery, ClientOidcCallbackQuery, ClientOidcExchangeRequest, ClientOidcErrorCode, ClientMcpInteraction, ClientMcpInteractionDecisionResponse, McpConsentGrant, McpConsentGrantListResponse, ClientIdentity, } from './client-auth.js';
38
38
  export { clientRobotListItem, clientRobotListResponse } from './client-robots.js';
39
39
  export type { ClientRobotListItem, ClientRobotListResponse } from './client-robots.js';
40
- export { APP_USER_DISPLAY_NAME_MAX, APP_URL_PLACEHOLDERS, MAIL_TEMPLATE_VARIABLES, DEFAULT_MAIL_TEMPLATES, providerSlug, appUserStatus, appUser, appUserListResponse, createAppUserRequest, patchAppUserRequest, createAppInvitationRequest, appInvitation, pendingAppInvitation, appInvitationListResponse, appOidcProvider, appOidcProviderListResponse, createAppOidcProviderRequest, patchAppOidcProviderRequest, appUrlTemplate, allowedOrigin, emailDomain, appAuthConfig, putAppAuthConfigRequest, mailTemplateKind, appMailTemplate, appMailTemplateListResponse, putAppMailTemplateRequest, mailTemplatePreviewRequest, mailTemplatePreviewResponse, mailTemplateProblemDetails, mailOutcome, } from './app-users.js';
41
- export type { AppUserStatus, AppUser, AppUserListResponse, CreateAppUserRequest, PatchAppUserRequest, CreateAppInvitationRequest, AppInvitation, PendingAppInvitation, AppInvitationListResponse, AppOidcProvider, AppOidcProviderListResponse, CreateAppOidcProviderRequest, PatchAppOidcProviderRequest, AppAuthConfig, PutAppAuthConfigRequest, MailTemplateKind, AppMailTemplate, AppMailTemplateListResponse, PutAppMailTemplateRequest, MailTemplatePreviewRequest, MailTemplatePreviewResponse, MailTemplateProblemDetails, MailOutcome, } from './app-users.js';
40
+ export { APP_USER_DISPLAY_NAME_MAX, APP_URL_PLACEHOLDERS, MAIL_TEMPLATE_VARIABLES, DEFAULT_MAIL_TEMPLATES, providerSlug, appUserStatus, appUser, appUserListResponse, createAppUserRequest, patchAppUserRequest, createAppInvitationRequest, appInvitation, pendingAppInvitation, appInvitationListResponse, appOidcProvider, appOidcProviderListResponse, createAppOidcProviderRequest, patchAppOidcProviderRequest, appUrlTemplate, allowedOrigin, emailDomain, appAuthConfig, putAppAuthRegistrationRequest, putAppAuthUrlsRequest, putAppAuthMcpRequest, mailTemplateKind, appMailTemplate, appMailTemplateListResponse, putAppMailTemplateRequest, mailTemplatePreviewRequest, mailTemplatePreviewResponse, mailTemplateProblemDetails, mailOutcome, } from './app-users.js';
41
+ export type { AppUserStatus, AppUser, AppUserListResponse, CreateAppUserRequest, PatchAppUserRequest, CreateAppInvitationRequest, AppInvitation, PendingAppInvitation, AppInvitationListResponse, AppOidcProvider, AppOidcProviderListResponse, CreateAppOidcProviderRequest, PatchAppOidcProviderRequest, AppAuthConfig, PutAppAuthRegistrationRequest, PutAppAuthUrlsRequest, PutAppAuthMcpRequest, MailTemplateKind, AppMailTemplate, AppMailTemplateListResponse, PutAppMailTemplateRequest, MailTemplatePreviewRequest, MailTemplatePreviewResponse, MailTemplateProblemDetails, MailOutcome, } from './app-users.js';
42
42
  export { assetKind, URDF_ASSET_NAME, asset, urdfCompleteness, assetListResponse, assetsClearResponse, missingAssetQuery, assetSyncRequest, assetSyncResponse, assetSyncState, assetSyncStatus, assetFailure, assetFailureKind, assetStoreRefusedDetails, assetSyncBusyDetails, ROBOT_ASSET_STORE_BYTES, } from './assets.js';
43
43
  export type { AssetKind, Asset, UrdfCompleteness, AssetListResponse, AssetsClearResponse, MissingAssetQuery, AssetSyncRequest, AssetSyncResponse, AssetSyncState, AssetSyncStatus, AssetFailure, AssetFailureKind, AssetStoreRefusedDetails, AssetSyncBusyDetails, } from './assets.js';
44
44
  export { auditActor, auditEvent, auditQuery, auditListResponse, AUDIT_CSV_COLUMNS, AUDIT_RETENTION_DAYS } from './audit.js';
package/dist/index.js CHANGED
@@ -5,7 +5,7 @@ export { PROTOCOL_VERSION, PROTOCOL_VERSIONS, PROTOCOL_SUNSET_DAYS, LATEST_BRIDG
5
5
  // Assets.
6
6
  bridgeAssetsAvailable, cloudAssetRequest, bridgeAssetProgress,
7
7
  // Addressing.
8
- activeJob, DEFAULT_PATIENCE_MS, MAX_PATIENCE_MS, MIN_PATIENCE_MS, } from './protocol.js';
8
+ activeJob, DEFAULT_PATIENCE_MS, MAX_PATIENCE_MS, MIN_PATIENCE_MS, JOB_HEARTBEAT_INTERVAL_MS, JOB_HEARTBEAT_TIMEOUT_MS, JOB_OFFLINE_GRACE_MS, } from './protocol.js';
9
9
  export { jobState, job, jobEvent, busyDetails, publisherBusyDetails, jobQueueFullDetails } from './jobs.js';
10
10
  export { JOB_RUN_PAGE_MAX, JOB_RUN_RETENTION_DAYS, jobActor, jobRunKind, jobRun, jobRunQuery, jobRunListResponse, jobRunSummaryQuery, jobRunSummary, } from './jobs.js';
11
11
  export { FLEETLESS_FORMAT_VERSION, RESERVED_SLUGS, parameterType, parameterSpec, parameterMap, serviceDescription, parameterDescription, messageTemplate, messageRef, messageBody, messageMap, PLACEHOLDER_RE, placeholderNames, actionConfig, serviceConfig, publisherConfig, cameraConfig, alertCondition, datapointAlert, rateThrottleHz, datapointNumeric, datapointRetention, datapointChart, datapointConfig, lowBandwidthSection, LOW_BANDWIDTH_DEFAULTS, robotConfigDoc, validationIssue, configState, snapshotIntervalSeconds,
@@ -38,11 +38,11 @@ USER_DISPLAY_NAME_MAX, orgAdminTier, fleetlessUser, fleetlessUserListResponse, c
38
38
  mailStatus, tierRequiredDetails, passwordChangeRequest, passwordResetRequest, passwordResetConfirm, idpIssuer,
39
39
  // auth/me, org and member patches.
40
40
  authMeResponse, patchOrgRequest, patchAuthMeRequest, } from './identity.js';
41
- export { appIdentifier, app, appListResponse, createAppRequest, updateAppRequest, serverKeyToken, serverKey, serverKeyListResponse, createServerKeyResponse, role, roleListResponse, rolePermissions, } from './apps.js';
41
+ export { appIdentifier, app, appListResponse, appDeletionSummary, createAppRequest, updateAppRequest, serverKeyToken, serverKey, serverKeyListResponse, createServerKeyResponse, role, roleListResponse, rolePermissions, } from './apps.js';
42
42
  export { clientLoginRequest, clientRefreshRequest, clientLogoutRequest, clientRegisterRequest, clientVerifyEmailRequest, clientResendVerificationRequest, clientPasswordResetRequest, clientPasswordResetConfirmRequest, clientAcceptInvitationRequest, CLIENT_OIDC_CALLBACK_PATH, clientProviderListQuery, clientProviderListResponse, clientOidcStartQuery, clientOidcCallbackQuery, clientOidcExchangeRequest, clientOidcErrorCode, clientMcpInteraction, clientMcpInteractionDecisionResponse, mcpConsentGrant, mcpConsentGrantListResponse, clientIdentity, } from './client-auth.js';
43
43
  export { clientRobotListItem, clientRobotListResponse } from './client-robots.js';
44
44
  // The per-app identity space.
45
- export { APP_USER_DISPLAY_NAME_MAX, APP_URL_PLACEHOLDERS, MAIL_TEMPLATE_VARIABLES, DEFAULT_MAIL_TEMPLATES, providerSlug, appUserStatus, appUser, appUserListResponse, createAppUserRequest, patchAppUserRequest, createAppInvitationRequest, appInvitation, pendingAppInvitation, appInvitationListResponse, appOidcProvider, appOidcProviderListResponse, createAppOidcProviderRequest, patchAppOidcProviderRequest, appUrlTemplate, allowedOrigin, emailDomain, appAuthConfig, putAppAuthConfigRequest, mailTemplateKind, appMailTemplate, appMailTemplateListResponse, putAppMailTemplateRequest, mailTemplatePreviewRequest, mailTemplatePreviewResponse, mailTemplateProblemDetails, mailOutcome, } from './app-users.js';
45
+ export { APP_USER_DISPLAY_NAME_MAX, APP_URL_PLACEHOLDERS, MAIL_TEMPLATE_VARIABLES, DEFAULT_MAIL_TEMPLATES, providerSlug, appUserStatus, appUser, appUserListResponse, createAppUserRequest, patchAppUserRequest, createAppInvitationRequest, appInvitation, pendingAppInvitation, appInvitationListResponse, appOidcProvider, appOidcProviderListResponse, createAppOidcProviderRequest, patchAppOidcProviderRequest, appUrlTemplate, allowedOrigin, emailDomain, appAuthConfig, putAppAuthRegistrationRequest, putAppAuthUrlsRequest, putAppAuthMcpRequest, mailTemplateKind, appMailTemplate, appMailTemplateListResponse, putAppMailTemplateRequest, mailTemplatePreviewRequest, mailTemplatePreviewResponse, mailTemplateProblemDetails, mailOutcome, } from './app-users.js';
46
46
  export { assetKind, URDF_ASSET_NAME, asset, urdfCompleteness, assetListResponse, assetsClearResponse, missingAssetQuery, assetSyncRequest, assetSyncResponse, assetSyncState, assetSyncStatus, assetFailure, assetFailureKind, assetStoreRefusedDetails, assetSyncBusyDetails, ROBOT_ASSET_STORE_BYTES, } from './assets.js';
47
47
  export { auditActor, auditEvent, auditQuery, auditListResponse, AUDIT_CSV_COLUMNS, AUDIT_RETENTION_DAYS } from './audit.js';
48
48
  export { alertRowCondition, alertSeverity, alertState, datapointAlertRow, alertListResponse, orgFiringAlertsResponse, orgAlertsQuery, datapointDisplay, putDatapointDisplayRequest, } from './alerts.js';
@@ -1,7 +1,7 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
2
  import { z } from 'zod';
3
3
  /**
4
- * Bridge <-> cloud protocol, version 3.
4
+ * Bridge <-> cloud protocol, version 4.
5
5
  *
6
6
  * The version is exchanged in the hello handshake. Since 2026-09 the cloud
7
7
  * serves a **window** of versions, not one: every entry of
@@ -11,6 +11,18 @@ import { z } from 'zod';
11
11
  * which names the window and reaches the robot's detail view as
12
12
  * `last_hello_error`.
13
13
  *
14
+ * **4 (2026-09-28):** the bridge sends a `job_update` heartbeat at
15
+ * `JOB_HEARTBEAT_INTERVAL_MS` for every running job, whether or not the
16
+ * action said anything new, and reports a vanished action server with
17
+ * `job_lost`'s new optional `error`, `action_server_lost`. The cloud bounds
18
+ * a protocol-4 job's silence by the heartbeat (`JOB_HEARTBEAT_TIMEOUT_MS`)
19
+ * once it has heard from the job at all; `patience_ms` still bounds
20
+ * acceptance, the same as before. Offline tolerance is
21
+ * `JOB_OFFLINE_GRACE_MS` (five minutes), up from the informal one minute a
22
+ * protocol-3 bridge got. A protocol-3 bridge sends no heartbeat and keeps
23
+ * today's behaviour exactly: `patience_ms` alone bounds the whole running
24
+ * job, silence included.
25
+ *
14
26
  * **3 (2026-09-22):** the ping carries `latency_ms` and `lag_ms`, the bridge
15
27
  * sends `link_mode`, `bridge_state` gains `low_bandwidth`, and the
16
28
  * `bridge_pressure` datapoint is gone. A protocol-2 bridge is served until
@@ -23,7 +35,7 @@ import { z } from 'zod';
23
35
  * **2 (2026-08-21):** `config_applied.errors` entries gained `kind` and `code`
24
36
  * beside `message`.
25
37
  */
26
- export declare const PROTOCOL_VERSION = 3;
38
+ export declare const PROTOCOL_VERSION = 4;
27
39
  /** Days between a version's deprecation and its sunset. */
28
40
  export declare const PROTOCOL_SUNSET_DAYS = 90;
29
41
  export interface ProtocolVersionEntry {
@@ -36,13 +48,15 @@ export interface ProtocolVersionEntry {
36
48
  /**
37
49
  * Every protocol version the cloud has served, oldest first. A test keeps
38
50
  * exactly one entry current and equal to `PROTOCOL_VERSION`; `test/changelog.test.ts`
39
- * requires the CHANGELOG's current section to name the newest `bridge_from`
40
- * and the previous entry's `sunsetOf(...)` date, and `scripts/verify-version-tag.mjs`
41
- * requires a dated heading for the tag being released.
51
+ * requires some CHANGELOG section — `[Unreleased]` or a dated one — to name
52
+ * the newest `bridge_from` together with the previous entry's `sunsetOf(...)`
53
+ * date, so the pull request that moves this window is the one that fails
54
+ * without saying so; and `scripts/verify-version-tag.mjs` requires a dated
55
+ * heading for the tag being released.
42
56
  */
43
57
  export declare const PROTOCOL_VERSIONS: readonly ProtocolVersionEntry[];
44
58
  /** The newest bridge package. The cloud mails organisations still below it. */
45
- export declare const LATEST_BRIDGE_VERSION = "4.0.0";
59
+ export declare const LATEST_BRIDGE_VERSION = "4.1.0";
46
60
  export interface ProtocolStatus {
47
61
  status: 'current' | 'deprecated' | 'unsupported';
48
62
  /** ISO date, or null for a current or unknown version. */
@@ -128,6 +142,40 @@ export declare const MAX_PATIENCE_MS = 120000;
128
142
  * this end bounds what the caller can do to the robot.
129
143
  */
130
144
  export declare const MIN_PATIENCE_MS = 1000;
145
+ /**
146
+ * How often a protocol-4 bridge sends a `job_update` heartbeat for every
147
+ * running job — the last known state, whether or not the action itself said
148
+ * anything new. One second: often enough that `JOB_HEARTBEAT_TIMEOUT_MS`
149
+ * can be a small multiple of it and still absorb a missed beat or two, rare
150
+ * enough that it costs nothing next to the datapoint traffic a busy robot
151
+ * already sends.
152
+ */
153
+ export declare const JOB_HEARTBEAT_INTERVAL_MS = 1000;
154
+ /**
155
+ * How long a protocol-4 job may go without a `job_update` — heartbeat or
156
+ * real progress, either counts — before the cloud settles it `lost` with
157
+ * `bridge_timeout`, once the bridge is connected. Five heartbeats: enough
158
+ * slack for an ordinary scheduling jitter, small next to `patience_ms`
159
+ * because it no longer has to cover the acceptance gap too. `patience_ms`
160
+ * bounds only the time from `invoke` to the *first* update on a protocol-4
161
+ * job; every rearm after that uses this constant instead. A protocol-3
162
+ * bridge sends no heartbeat, so this constant does not apply to it —
163
+ * `patience_ms` keeps bounding the whole running job there, exactly as
164
+ * before.
165
+ */
166
+ export declare const JOB_HEARTBEAT_TIMEOUT_MS = 5000;
167
+ /**
168
+ * How long a running job survives its robot going offline before the cloud
169
+ * gives up and settles it `lost` with `bridge_disconnected`. Five minutes:
170
+ * long enough that an ordinary Wi-Fi dead zone — the case this constant
171
+ * exists for — never costs a job, since a robot with no safety layer of its
172
+ * own (§ Fleetless is not a safety layer) keeps driving through one and the
173
+ * result the cloud is waiting for is often still coming. A robot connected
174
+ * the whole time never reaches this bound at all: while online, silence is
175
+ * `JOB_HEARTBEAT_TIMEOUT_MS`'s question (protocol 4) or `patience_ms`'s
176
+ * (protocol 3), never this one's.
177
+ */
178
+ export declare const JOB_OFFLINE_GRACE_MS = 300000;
131
179
  /** Re-exported so consumers keep importing wire names from one place. */
132
180
  export { slug } from './common.js';
133
181
  /**
@@ -598,10 +646,23 @@ export type BridgeJobUpdate = z.infer<typeof bridgeJobUpdate>;
598
646
  * left to enumerate, so it is `hello.active_job_ids` that closes that gap.
599
647
  * Both paths end in the same place — the cloud publishes `lost` rather than
600
648
  * leaving a job reading "running" because nobody contradicted it.
649
+ *
650
+ * **`error` (since protocol 4) is optional and, when present, applies to
651
+ * every job named in `job_ids`.** A vanished action server is discovered
652
+ * once, by the bridge's own liveness check on that one goal, so a frame
653
+ * naming several jobs at once — plausible if several goals shared the same
654
+ * server — always shares the same cause. Absent means today's behaviour:
655
+ * the cloud settles the job `lost` with no specific code, the same as a
656
+ * protocol-3 bridge's frame, which carries no `error` at all and still
657
+ * parses under this schema unchanged.
601
658
  */
602
659
  export declare const bridgeJobLost: z.ZodObject<{
603
660
  type: z.ZodLiteral<"job_lost">;
604
661
  job_ids: z.ZodArray<z.ZodUUID>;
662
+ error: z.ZodOptional<z.ZodObject<{
663
+ code: z.ZodString;
664
+ message: z.ZodString;
665
+ }, z.core.$strip>>;
605
666
  }, z.core.$strip>;
606
667
  export type BridgeJobLost = z.infer<typeof bridgeJobLost>;
607
668
  /** Cloud asks for a fresh ROS graph; `request_id` correlates the answer. */
package/dist/protocol.js CHANGED
@@ -7,7 +7,7 @@ import { rosGraph, typeDefinition } from './introspection.js';
7
7
  import { jobState } from './jobs.js';
8
8
  import { rosTypeName } from './common.js';
9
9
  /**
10
- * Bridge <-> cloud protocol, version 3.
10
+ * Bridge <-> cloud protocol, version 4.
11
11
  *
12
12
  * The version is exchanged in the hello handshake. Since 2026-09 the cloud
13
13
  * serves a **window** of versions, not one: every entry of
@@ -17,6 +17,18 @@ import { rosTypeName } from './common.js';
17
17
  * which names the window and reaches the robot's detail view as
18
18
  * `last_hello_error`.
19
19
  *
20
+ * **4 (2026-09-28):** the bridge sends a `job_update` heartbeat at
21
+ * `JOB_HEARTBEAT_INTERVAL_MS` for every running job, whether or not the
22
+ * action said anything new, and reports a vanished action server with
23
+ * `job_lost`'s new optional `error`, `action_server_lost`. The cloud bounds
24
+ * a protocol-4 job's silence by the heartbeat (`JOB_HEARTBEAT_TIMEOUT_MS`)
25
+ * once it has heard from the job at all; `patience_ms` still bounds
26
+ * acceptance, the same as before. Offline tolerance is
27
+ * `JOB_OFFLINE_GRACE_MS` (five minutes), up from the informal one minute a
28
+ * protocol-3 bridge got. A protocol-3 bridge sends no heartbeat and keeps
29
+ * today's behaviour exactly: `patience_ms` alone bounds the whole running
30
+ * job, silence included.
31
+ *
20
32
  * **3 (2026-09-22):** the ping carries `latency_ms` and `lag_ms`, the bridge
21
33
  * sends `link_mode`, `bridge_state` gains `low_bandwidth`, and the
22
34
  * `bridge_pressure` datapoint is gone. A protocol-2 bridge is served until
@@ -29,22 +41,25 @@ import { rosTypeName } from './common.js';
29
41
  * **2 (2026-08-21):** `config_applied.errors` entries gained `kind` and `code`
30
42
  * beside `message`.
31
43
  */
32
- export const PROTOCOL_VERSION = 3;
44
+ export const PROTOCOL_VERSION = 4;
33
45
  /** Days between a version's deprecation and its sunset. */
34
46
  export const PROTOCOL_SUNSET_DAYS = 90;
35
47
  /**
36
48
  * Every protocol version the cloud has served, oldest first. A test keeps
37
49
  * exactly one entry current and equal to `PROTOCOL_VERSION`; `test/changelog.test.ts`
38
- * requires the CHANGELOG's current section to name the newest `bridge_from`
39
- * and the previous entry's `sunsetOf(...)` date, and `scripts/verify-version-tag.mjs`
40
- * requires a dated heading for the tag being released.
50
+ * requires some CHANGELOG section — `[Unreleased]` or a dated one — to name
51
+ * the newest `bridge_from` together with the previous entry's `sunsetOf(...)`
52
+ * date, so the pull request that moves this window is the one that fails
53
+ * without saying so; and `scripts/verify-version-tag.mjs` requires a dated
54
+ * heading for the tag being released.
41
55
  */
42
56
  export const PROTOCOL_VERSIONS = [
43
57
  { version: 2, bridge_from: '3.0.0', deprecated_at: '2026-09-22' },
44
- { version: 3, bridge_from: '4.0.0', deprecated_at: null },
58
+ { version: 3, bridge_from: '4.0.0', deprecated_at: '2026-09-28' },
59
+ { version: 4, bridge_from: '4.1.0', deprecated_at: null },
45
60
  ];
46
61
  /** The newest bridge package. The cloud mails organisations still below it. */
47
- export const LATEST_BRIDGE_VERSION = '4.0.0';
62
+ export const LATEST_BRIDGE_VERSION = '4.1.0';
48
63
  const DAY_MS = 24 * 60 * 60 * 1000;
49
64
  function isoDate(date) {
50
65
  return date.toISOString().slice(0, 10);
@@ -146,6 +161,40 @@ export const MAX_PATIENCE_MS = 120_000;
146
161
  * this end bounds what the caller can do to the robot.
147
162
  */
148
163
  export const MIN_PATIENCE_MS = 1_000;
164
+ /**
165
+ * How often a protocol-4 bridge sends a `job_update` heartbeat for every
166
+ * running job — the last known state, whether or not the action itself said
167
+ * anything new. One second: often enough that `JOB_HEARTBEAT_TIMEOUT_MS`
168
+ * can be a small multiple of it and still absorb a missed beat or two, rare
169
+ * enough that it costs nothing next to the datapoint traffic a busy robot
170
+ * already sends.
171
+ */
172
+ export const JOB_HEARTBEAT_INTERVAL_MS = 1_000;
173
+ /**
174
+ * How long a protocol-4 job may go without a `job_update` — heartbeat or
175
+ * real progress, either counts — before the cloud settles it `lost` with
176
+ * `bridge_timeout`, once the bridge is connected. Five heartbeats: enough
177
+ * slack for an ordinary scheduling jitter, small next to `patience_ms`
178
+ * because it no longer has to cover the acceptance gap too. `patience_ms`
179
+ * bounds only the time from `invoke` to the *first* update on a protocol-4
180
+ * job; every rearm after that uses this constant instead. A protocol-3
181
+ * bridge sends no heartbeat, so this constant does not apply to it —
182
+ * `patience_ms` keeps bounding the whole running job there, exactly as
183
+ * before.
184
+ */
185
+ export const JOB_HEARTBEAT_TIMEOUT_MS = 5_000;
186
+ /**
187
+ * How long a running job survives its robot going offline before the cloud
188
+ * gives up and settles it `lost` with `bridge_disconnected`. Five minutes:
189
+ * long enough that an ordinary Wi-Fi dead zone — the case this constant
190
+ * exists for — never costs a job, since a robot with no safety layer of its
191
+ * own (§ Fleetless is not a safety layer) keeps driving through one and the
192
+ * result the cloud is waiting for is often still coming. A robot connected
193
+ * the whole time never reaches this bound at all: while online, silence is
194
+ * `JOB_HEARTBEAT_TIMEOUT_MS`'s question (protocol 4) or `patience_ms`'s
195
+ * (protocol 3), never this one's.
196
+ */
197
+ export const JOB_OFFLINE_GRACE_MS = 300_000;
149
198
  /** Re-exported so consumers keep importing wire names from one place. */
150
199
  export { slug } from './common.js';
151
200
  /**
@@ -440,10 +489,20 @@ export const bridgeJobUpdate = z.object({
440
489
  * left to enumerate, so it is `hello.active_job_ids` that closes that gap.
441
490
  * Both paths end in the same place — the cloud publishes `lost` rather than
442
491
  * leaving a job reading "running" because nobody contradicted it.
492
+ *
493
+ * **`error` (since protocol 4) is optional and, when present, applies to
494
+ * every job named in `job_ids`.** A vanished action server is discovered
495
+ * once, by the bridge's own liveness check on that one goal, so a frame
496
+ * naming several jobs at once — plausible if several goals shared the same
497
+ * server — always shares the same cause. Absent means today's behaviour:
498
+ * the cloud settles the job `lost` with no specific code, the same as a
499
+ * protocol-3 bridge's frame, which carries no `error` at all and still
500
+ * parses under this schema unchanged.
443
501
  */
444
502
  export const bridgeJobLost = z.object({
445
503
  type: z.literal('job_lost'),
446
504
  job_ids: z.array(z.uuid()),
505
+ error: z.object({ code: z.string().min(1), message: z.string().min(1) }).optional(),
447
506
  });
448
507
  /** Cloud asks for a fresh ROS graph; `request_id` correlates the answer. */
449
508
  export const cloudIntrospectRequest = z.object({
@@ -256,8 +256,8 @@ export declare const liveSessionEndReason: z.ZodEnum<{
256
256
  unknown: "unknown";
257
257
  publish_failed: "publish_failed";
258
258
  robot_offline: "robot_offline";
259
- released_by_peer: "released_by_peer";
260
259
  config_changed: "config_changed";
260
+ released_by_peer: "released_by_peer";
261
261
  revoked: "revoked";
262
262
  expired: "expired";
263
263
  robot_deleted: "robot_deleted";
@@ -287,8 +287,8 @@ export declare const liveSessionEvent: z.ZodObject<{
287
287
  unknown: "unknown";
288
288
  publish_failed: "publish_failed";
289
289
  robot_offline: "robot_offline";
290
- released_by_peer: "released_by_peer";
291
290
  config_changed: "config_changed";
291
+ released_by_peer: "released_by_peer";
292
292
  revoked: "revoked";
293
293
  expired: "expired";
294
294
  robot_deleted: "robot_deleted";
package/dist/rest.d.ts CHANGED
@@ -1440,6 +1440,8 @@ export type HistoryResponse = z.infer<typeof historyResponse>;
1440
1440
  * |---|---|---|
1441
1441
  * | `DELETE /api/robots/:id` | — | `204`. `?force=true` to proceed while a live session is open; without it, `409 robot_in_use` |
1442
1442
  * | `GET /api/robots/:id/deletion-preview` | — | `robotDeletionSummary` — the same shape the audit event carries |
1443
+ * | `DELETE /api/apps/:id` | — | `204`. No `force` parameter — an app has no open-session hazard to force past, so the preview is the guard |
1444
+ * | `GET /api/apps/:id/deletion-preview` | — | `appDeletionSummary` — the same shape the audit event carries |
1443
1445
  * | `GET /api/org/health` | — | `resourceHealthListResponse`; `?robot_id=` narrows it to one robot |
1444
1446
  *
1445
1447
  * Plus `resourceHealthEvent`, pushed on the **developer** realtime socket
package/dist/rest.js CHANGED
@@ -1228,6 +1228,8 @@ export const historyResponse = z.union([historySamplesResponse, historyBucketsRe
1228
1228
  * |---|---|---|
1229
1229
  * | `DELETE /api/robots/:id` | — | `204`. `?force=true` to proceed while a live session is open; without it, `409 robot_in_use` |
1230
1230
  * | `GET /api/robots/:id/deletion-preview` | — | `robotDeletionSummary` — the same shape the audit event carries |
1231
+ * | `DELETE /api/apps/:id` | — | `204`. No `force` parameter — an app has no open-session hazard to force past, so the preview is the guard |
1232
+ * | `GET /api/apps/:id/deletion-preview` | — | `appDeletionSummary` — the same shape the audit event carries |
1231
1233
  * | `GET /api/org/health` | — | `resourceHealthListResponse`; `?robot_id=` narrows it to one robot |
1232
1234
  *
1233
1235
  * Plus `resourceHealthEvent`, pushed on the **developer** realtime socket
package/dist/routes.js CHANGED
@@ -1,11 +1,11 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
- import { appListResponse, createAppRequest, createServerKeyResponse, app as appSchema, role, roleListResponse, rolePermissions, serverKeyListResponse, updateAppRequest, } from './apps.js';
2
+ import { appListResponse, appDeletionSummary, createAppRequest, createServerKeyResponse, app as appSchema, role, roleListResponse, rolePermissions, serverKeyListResponse, updateAppRequest, } from './apps.js';
3
3
  import { alertListResponse, orgAlertsQuery, orgFiringAlertsResponse } from './alerts.js';
4
4
  import { asset, assetListResponse, assetsClearResponse, assetSyncRequest, assetSyncResponse, assetSyncStatus, missingAssetQuery } from './assets.js';
5
5
  import { auditListResponse, auditQuery } from './audit.js';
6
6
  import { CLIENT_OIDC_CALLBACK_PATH, clientAcceptInvitationRequest, clientIdentity, clientLoginRequest, clientLogoutRequest, clientMcpInteraction, clientMcpInteractionDecisionResponse, clientOidcCallbackQuery, clientOidcExchangeRequest, clientOidcStartQuery, clientPasswordResetConfirmRequest, clientPasswordResetRequest, clientProviderListQuery, clientProviderListResponse, clientRefreshRequest, clientRegisterRequest, clientResendVerificationRequest, clientVerifyEmailRequest, mcpConsentGrantListResponse, } from './client-auth.js';
7
7
  import { clientRobotListResponse } from './client-robots.js';
8
- import { appAuthConfig, appInvitation, appInvitationListResponse, appMailTemplate, appMailTemplateListResponse, appOidcProvider, appOidcProviderListResponse, appUser, appUserListResponse, createAppInvitationRequest, createAppOidcProviderRequest, createAppUserRequest, mailOutcome, mailTemplatePreviewRequest, mailTemplatePreviewResponse, patchAppOidcProviderRequest, patchAppUserRequest, putAppAuthConfigRequest, putAppMailTemplateRequest, } from './app-users.js';
8
+ import { appAuthConfig, appInvitation, appInvitationListResponse, appMailTemplate, appMailTemplateListResponse, appOidcProvider, appOidcProviderListResponse, appUser, appUserListResponse, createAppInvitationRequest, createAppOidcProviderRequest, createAppUserRequest, mailOutcome, mailTemplatePreviewRequest, mailTemplatePreviewResponse, patchAppOidcProviderRequest, patchAppUserRequest, putAppAuthMcpRequest, putAppAuthRegistrationRequest, putAppAuthUrlsRequest, putAppMailTemplateRequest, } from './app-users.js';
9
9
  import { acceptTeamInviteRequest, authMeResponse, createTeamInviteRequest, fleetlessUser, fleetlessUserListResponse, passwordChangeRequest, passwordResetConfirm, passwordResetRequest, patchAuthMeRequest, patchFleetlessUserRequest, patchOrgRequest, patchOrgResponse, pendingTeamInviteListResponse, refreshRequest, sessionTokens, signUpRequest, signUpResponse, teamInvite, tierChangeRequest, waitlistRequest, } from './identity.js';
10
10
  import { jobRunListResponse, jobRunQuery, jobRunSummary, jobRunSummaryQuery } from './jobs.js';
11
11
  import { MCP_APP_PATHS, mcpRobotDatasheet, mcpRolePreviewResponse } from './mcp.js';
@@ -295,6 +295,32 @@ export const ROUTES = [
295
295
  notes: 'A `default_role_id` naming a role of another app is refused: it is the one cross-app authorization check this shape can carry. ' +
296
296
  'Changing the robot set closes every live subscription the app\'s users hold, since a grant may no longer name a reachable robot.',
297
297
  },
298
+ {
299
+ method: 'GET', path: '/api/apps/:id/deletion-preview', section: 'apps',
300
+ summary: 'Reports what deleting the app would destroy, without destroying it.',
301
+ audience: 'developer', auth: 'developer', rateLimited: false, ownerTier: false, status: 200,
302
+ params: [{ name: 'id', description: 'The app\'s uuid, as returned by `POST /api/apps` or listed by `GET /api/apps`.' }],
303
+ query: null, request: null, response: appDeletionSummary,
304
+ errors: [...DEVELOPER_GUARD, 'invalid_uuid', 'not_found'], transport: 'http',
305
+ notes: 'The same shape the delete\'s own audit event carries, computed by the same function on purpose: the confirmation dialog and the eventual ' +
306
+ 'receipt agree by construction, and any difference between them is real drift rather than two estimates that quietly disagree. \n\n' +
307
+ '**No `force` parameter, unlike the robot pair this is modelled on.** A robot\'s open live session is a single nameable state whose ' +
308
+ 'interruption is its own hazard, which is why that route makes the caller pass `force` explicitly. An app has no equivalent state to ' +
309
+ 'force past, and inventing one would be a guess wearing a guard\'s clothes — this preview is the guard.',
310
+ },
311
+ {
312
+ method: 'DELETE', path: '/api/apps/:id', section: 'apps',
313
+ summary: 'Deletes an app and everything it produced.',
314
+ audience: 'developer', auth: 'developer', rateLimited: false, ownerTier: true, status: 204,
315
+ params: [{ name: 'id', description: 'The app\'s uuid, as returned by `POST /api/apps` or listed by `GET /api/apps`.' }],
316
+ query: null, request: null, response: null,
317
+ errors: [...DEVELOPER_GUARD, 'tier_required', 'invalid_uuid', 'not_found'], transport: 'http',
318
+ notes: 'Owner tier, and the gate runs **after** the org-scoped lookup: a developer-tier admin therefore sees the same `404` a stranger would ' +
319
+ 'for an app outside their org, rather than a tier refusal that confirms the id exists. A full cascade — its users, roles, server keys, ' +
320
+ 'invitations, OIDC provider configuration and mail templates all go, recorded once as `app.deleted` carrying an `appDeletionSummary`. ' +
321
+ 'Its robots are untouched: they belong to the org, not to the app. \n\n**No `force` parameter** — see ' +
322
+ '`GET /api/apps/:id/deletion-preview`.',
323
+ },
298
324
  {
299
325
  method: 'POST', path: '/api/apps/:id/roles', section: 'apps',
300
326
  summary: 'Creates a custom role on the app.',
@@ -681,22 +707,55 @@ export const ROUTES = [
681
707
  errors: [...DEVELOPER_GUARD, 'invalid_uuid', 'not_found'], transport: 'http',
682
708
  notes: 'One row per app, created with the app and never absent — an app that has configured nothing reads back the defaults rather than a ' +
683
709
  '`404`. `oidc_callback_url` is in the answer and not in the request: it is minted by the cloud from its own public base URL, is the same ' +
684
- 'for every app and every provider, and is the value a developer registers at their identity provider.',
710
+ 'for every app and every provider, and is the value a developer registers at their identity provider. It stays read-only on every slice ' +
711
+ 'write below for a second reason: a writable callback URL would let a caller point the return leg of an OIDC sign-in, which carries an ' +
712
+ 'authorization code, at a host they own. `updated_at` is read-only for a duller one: the server stamps it on every write, and a ' +
713
+ 'client-supplied value would be a lie about when the row last changed.',
714
+ },
715
+ {
716
+ method: 'PUT', path: '/api/apps/:id/auth-config/registration', section: 'apps',
717
+ summary: 'Replaces who may self-register, and from where.',
718
+ audience: 'developer', auth: 'developer', rateLimited: false, ownerTier: false, status: 200,
719
+ params: [{ name: 'id', description: 'The app\'s uuid, as returned by `POST /api/apps` or listed by `GET /api/apps`.' }],
720
+ query: null, request: putAppAuthRegistrationRequest, response: appAuthConfig,
721
+ errors: [...DEVELOPER_GUARD, 'invalid_uuid', 'validation_error', 'not_found'], transport: 'http',
722
+ notes: '**A replace, not a merge, and `.strict()`**: `self_registration`, `allowed_domains` and `allowed_origins` all arrive or the write is ' +
723
+ 'refused, so a client built against an older shape cannot silently clear a setting it does not know about. `oidc_callback_url` and ' +
724
+ '`updated_at` are the server\'s, refused in this body as in every slice\'s — see `GET`\'s notes for why. ' +
725
+ '\n\n`400 validation_error` is where the two field rules land: an entry in `allowed_domains` must be lowercase, since a capitalised one ' +
726
+ 'can never match a lowercased address, and an entry in `allowed_origins` must be a bare scheme-host-port with no path, since a browser ' +
727
+ 'sends nothing longer in its `Origin` header. Each refuses at configuration time rather than failing silently later. ' +
728
+ '\n\nThe merge is server-side against the stored row, so this write never disturbs the urls or mcp slice.',
729
+ },
730
+ {
731
+ method: 'PUT', path: '/api/apps/:id/auth-config/urls', section: 'apps',
732
+ summary: "Replaces the three pages Fleetless's mails point at.",
733
+ audience: 'developer', auth: 'developer', rateLimited: false, ownerTier: false, status: 200,
734
+ params: [{ name: 'id', description: 'The app\'s uuid, as returned by `POST /api/apps` or listed by `GET /api/apps`.' }],
735
+ query: null, request: putAppAuthUrlsRequest, response: appAuthConfig,
736
+ errors: [...DEVELOPER_GUARD, 'invalid_uuid', 'validation_error', 'not_found'], transport: 'http',
737
+ notes: '**A replace, not a merge, and `.strict()`**: `invite_url`, `verify_url` and `reset_url` all arrive or the write is refused, so a ' +
738
+ 'client built against an older shape cannot silently clear a setting it does not know about. `oidc_callback_url` and `updated_at` are ' +
739
+ 'the server\'s, refused in this body as in every slice\'s — see `GET`\'s notes for why. ' +
740
+ '\n\n`400 validation_error` is where the field rule lands: a URL template must be https (or `http` on `localhost`) and carry its ' +
741
+ 'placeholder exactly once — a second occurrence leaves one literal in a mailed link, refused here rather than failing silently once ' +
742
+ 'the mail is sent. ' +
743
+ '\n\nThe merge is server-side against the stored row, so this write never disturbs the registration or mcp slice.',
685
744
  },
686
745
  {
687
- method: 'PUT', path: '/api/apps/:id/auth-config', section: 'apps',
688
- summary: "Replaces the app's auth settings in one write.",
746
+ method: 'PUT', path: '/api/apps/:id/auth-config/mcp', section: 'apps',
747
+ summary: 'Replaces the MCP switch and its login URL together.',
689
748
  audience: 'developer', auth: 'developer', rateLimited: false, ownerTier: false, status: 200,
690
749
  params: [{ name: 'id', description: 'The app\'s uuid, as returned by `POST /api/apps` or listed by `GET /api/apps`.' }],
691
- query: null, request: putAppAuthConfigRequest, response: appAuthConfig,
750
+ query: null, request: putAppAuthMcpRequest, response: appAuthConfig,
692
751
  errors: [...DEVELOPER_GUARD, 'invalid_uuid', 'validation_error', 'not_found'], transport: 'http',
693
- notes: '**A replace, not a merge, and `.strict()`**: every field arrives or the write is refused, so a client built against an older shape ' +
694
- 'cannot silently clear a setting it does not know about. `oidc_callback_url` and `updated_at` are refused in the body — a writable ' +
695
- 'callback URL would let a caller point the return leg of an OIDC sign-in, which carries an authorization code, at a host they own. ' +
696
- '\n\n`400 validation_error` is where the three field rules land: a URL template must be https (or `http` on `localhost`) and carry its ' +
697
- 'placeholder exactly once, an origin must be a bare scheme-host-port with no path, and a domain must be lowercase. Each refuses at ' +
698
- 'configuration time because each would otherwise fail silently later — a second placeholder leaves one occurrence literal in a mailed ' +
699
- 'link, an origin with a path can never equal a browser\'s `Origin` header, and a capitalised domain can never match a lowercased address.',
752
+ notes: '**A replace, not a merge, and `.strict()`**: `mcp_enabled` and `mcp_login_url` both arrive or the write is refused, so a client built ' +
753
+ 'against an older shape cannot silently clear a setting it does not know about. `oidc_callback_url` and `updated_at` are the server\'s, ' +
754
+ 'refused in this body as in every slice\'s — see `GET`\'s notes for why. ' +
755
+ '\n\n`mcp_login_url` answers to the same rule as the `urls` slice\'s three templates — https (or `http` on `localhost`), its placeholder ' +
756
+ 'exactly once — refused as `400 validation_error` rather than left to fail mid-OAuth, in a client\'s browser where no console screen ' +
757
+ 'is watching. ' +
758
+ '\n\nThe merge is server-side against the stored row, so this write never disturbs the registration or urls slice.',
700
759
  },
701
760
  {
702
761
  method: 'GET', path: '/api/apps/:id/mail-templates', section: 'apps',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fleetless/contracts",
3
- "version": "2.0.0",
3
+ "version": "4.0.0-next.1",
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",
@@ -1,93 +0,0 @@
1
- {
2
- "$schema": "https://json-schema.org/draft/2020-12/schema",
3
- "type": "object",
4
- "properties": {
5
- "self_registration": {
6
- "type": "boolean",
7
- "description": "Whether a stranger may create an account in this app. Off refuses `POST /api/client/register` with `403 registration_closed`, and refuses an unknown identity at an OIDC callback with the same reasoning — one switch for one decision, whichever door the person arrives at."
8
- },
9
- "allowed_domains": {
10
- "maxItems": 50,
11
- "type": "array",
12
- "items": {
13
- "type": "string",
14
- "minLength": 1,
15
- "maxLength": 253,
16
- "pattern": "^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\\.)+[a-z]{2,63}$"
17
- },
18
- "description": "The email domains self-registration accepts, lowercase. An empty list means no domain restriction, not \"nobody\" — the switch above is what closes the door. **An invitation always bypasses this**, by password and through a provider alike."
19
- },
20
- "allowed_origins": {
21
- "maxItems": 20,
22
- "type": "array",
23
- "items": {
24
- "type": "string",
25
- "maxLength": 200
26
- },
27
- "description": "The origins the client auth API answers CORS for, and the only origins an OIDC `redirect_uri` may name. Bare origins: scheme, host and port, with no path — a browser sends nothing longer, so an entry carrying one could never match."
28
- },
29
- "mcp_enabled": {
30
- "type": "boolean",
31
- "description": "Whether this app serves an MCP endpoint at `/mcp/<identifier>`. Off refuses the whole OAuth surface for the app, not merely the tool calls, and is re-read on every request rather than cached off a token."
32
- },
33
- "invite_url": {
34
- "anyOf": [
35
- {
36
- "type": "string",
37
- "maxLength": 500
38
- },
39
- {
40
- "type": "null"
41
- }
42
- ],
43
- "description": "The page in the developer's app that accepts an invitation, with `{token}` where the token goes. `null` when unconfigured, and then an invitation still issues but `send_mail` is refused with `409 target_state_conflict` — there would be nowhere for the link to point."
44
- },
45
- "verify_url": {
46
- "anyOf": [
47
- {
48
- "type": "string",
49
- "maxLength": 500
50
- },
51
- {
52
- "type": "null"
53
- }
54
- ],
55
- "description": "The page that confirms a new address, with `{token}` where the token goes. Self-registration needs it: without a page to send people to, a registration would leave an account nobody can activate."
56
- },
57
- "reset_url": {
58
- "anyOf": [
59
- {
60
- "type": "string",
61
- "maxLength": 500
62
- },
63
- {
64
- "type": "null"
65
- }
66
- ],
67
- "description": "The page that takes a new password, with `{token}` where the token goes."
68
- },
69
- "mcp_login_url": {
70
- "anyOf": [
71
- {
72
- "type": "string",
73
- "maxLength": 500
74
- },
75
- {
76
- "type": "null"
77
- }
78
- ],
79
- "description": "The page an MCP authorization redirects to, with `{interaction}` where the interaction id goes. Not a token: the id names a pending request the server already holds, and the app authenticates the user itself before approving it."
80
- }
81
- },
82
- "required": [
83
- "self_registration",
84
- "allowed_domains",
85
- "allowed_origins",
86
- "mcp_enabled",
87
- "invite_url",
88
- "verify_url",
89
- "reset_url",
90
- "mcp_login_url"
91
- ],
92
- "additionalProperties": false
93
- }