@fleetless/sdk 3.0.2 → 3.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -607,13 +607,13 @@ type ErrorCode = (typeof ERROR_CODES)[number];
607
607
  * later on the same socket — nobody knows — but the caller cannot be made
608
608
  * to wait forever for that.
609
609
  * - `command_outcome_unknown`: worse than a timeout, and told apart from it
610
- * on purpose — the realtime connection that carried the command was
611
- * replaced by a new one (a reconnect) before any reply arrived. A reply
612
- * can now never come: the server, if it answered at all, answered a
613
- * socket that no longer exists. The command may or may not have run.
614
- * Never retried automatically — that could run an action twice — the
615
- * caller recovers by reading the job (e.g. `actions.subscribe`), since
616
- * state is observed by slug regardless of which connection asked for it.
610
+ * on purpose — the connection that carried the command was replaced by a
611
+ * new one (a reconnect) before any reply arrived. The server, if it
612
+ * answered at all, answered a socket that no longer exists, so the
613
+ * command may or may not have run. Never retried automatically — that
614
+ * could run an action twice — the caller recovers by reading the job
615
+ * (e.g. `actions.subscribe`), since state is observed by slug regardless
616
+ * of which connection asked for it.
617
617
  * - `unexpected_response`: the server answered `ok:true` but left out
618
618
  * something the command is defined to always return (e.g. no `job` on a
619
619
  * successful `invoke`) — a contract violation the SDK noticed, not a
@@ -801,20 +801,19 @@ interface JobSubscriptionHandlers {
801
801
  interface JobSubscription {
802
802
  /**
803
803
  * Stops this subscription. The `unsubscribe` frame reaches the server only
804
- * when this was the **last** holder of the robot and slug pair, and only if
805
- * the channel is connected — subscriptions are reference-counted across
806
- * kinds, so releasing this one while a second `actions.subscribe`, a
807
- * `datapoints.subscribe` or an in-flight `services.call` still holds the
808
- * same pair leaves that one's stream running untouched. Safe to call more
809
- * than once.
804
+ * when this was the **last** holder of the robot/slug pair and the channel
805
+ * is connected — subscriptions are reference-counted across kinds, so a
806
+ * second `actions.subscribe`, `datapoints.subscribe` or in-flight
807
+ * `services.call` on the same pair keeps its stream running. Safe to call
808
+ * more than once.
810
809
  */
811
810
  unsubscribe(): void;
812
811
  }
813
812
 
814
813
  /**
815
814
  * Long-running work on a robot, reachable as `client.actions`. An action is
816
- * a ROS action the developer exposed under a slug: it is invoked, it runs
817
- * for as long as it runs, and it reports back while it does.
815
+ * a ROS action the developer exposed under a slug: it is invoked, runs as
816
+ * long as it runs, and reports back while it does.
818
817
  */
819
818
  interface ActionsApi {
820
819
  /**
@@ -822,7 +821,7 @@ interface ActionsApi {
822
821
  * — the job id is informative, not the result. Feedback, progress and the
823
822
  * eventual result arrive separately over `subscribe`. A second invoke of
824
823
  * the same slug while one is already running is refused `busy`, with
825
- * `error.details.running` naming the job that is running.
824
+ * `error.details.running` naming the running job.
826
825
  *
827
826
  * `options.patienceMs` bounds goal *acceptance* only — once a goal
828
827
  * is accepted this call has already resolved; the job then runs as long
@@ -1106,7 +1105,7 @@ interface AssetsApi {
1106
1105
  * **Do not also install `createMeshLoader` on the same manager.** The two
1107
1106
  * consume different URDF sources — this method fetches the URDF's *raw*
1108
1107
  * bytes, `createMeshLoader` is meant to pair with `urdf()`'s
1109
- * cloud-rewritten text. **It is not that every mesh gets fetched twice.**
1108
+ * cloud-rewritten text. **Not a double-fetch.**
1110
1109
  * What actually happens is asymmetric breakage,
1111
1110
  * whichever URDF text the combination ends up parsing: paired with
1112
1111
  * *this* method's raw text, `createMeshLoader` receives urdf-loader's
@@ -1236,7 +1235,7 @@ interface TokenStore {
1236
1235
  /** The default store: works out of the box, forgets the session on reload. */
1237
1236
  declare class InMemoryTokenStore implements TokenStore {
1238
1237
  #private;
1239
- /** Creates an empty store. Nothing is loaded from anywhere — a client built with it starts logged out. */
1238
+ /** Nothing is loaded from anywhere — a client built with it starts logged out. */
1240
1239
  constructor();
1241
1240
  /** Returns the session held in memory, or `null` if there is none. */
1242
1241
  load(): StoredSession | null;
@@ -1680,8 +1679,8 @@ interface CameraLiveSession {
1680
1679
  * courteous fast path; the robot actually stops publishing once every
1681
1680
  * viewer's LiveKit `Room` has disconnected, which the SFU notices on its
1682
1681
  * own with no cooperation required. **Always pair this with disconnecting
1683
- * the `Room` you connected with `url`/`token`** — see the README's
1684
- * Cameras section for the paired cleanup pattern; a `release()` that ran
1682
+ * the `Room` you connected with `url`/`token`** — see the Cameras
1683
+ * section of the SDK reference for the paired cleanup pattern; a `release()` that ran
1685
1684
  * alone while the `Room` stayed connected would stop nothing.
1686
1685
  *
1687
1686
  * Safe to call more than once (only the first call does anything) and
@@ -1692,14 +1691,12 @@ interface CameraLiveSession {
1692
1691
  * from `beforeunload`, where a call that could throw would be a liability.
1693
1692
  *
1694
1693
  * **A failed DELETE here is not observable anywhere** — not as a
1695
- * rejection, not as a realtime event, not as a field on this object. This
1696
- * is a deliberate decision, not an oversight: the only
1697
- * consumer of that information would be code deciding whether to retry,
1698
- * and the backstop this comment already describes — the cloud's own
1699
- * LiveKit-participation reconciliation — makes a retry unnecessary for
1700
- * correctness. If a future caller needs to know "did my release actually
1701
- * reach the cloud" (telemetry, say), that is a new, additive signal to
1702
- * design, not a change to this method's contract.
1694
+ * rejection, a realtime event, or a field on this object. Deliberate, not
1695
+ * an oversight: the only consumer of that information would be code
1696
+ * deciding whether to retry, and the reconciliation backstop described
1697
+ * above already makes a retry unnecessary for correctness. A future need
1698
+ * to know "did my release actually reach the cloud" (telemetry, say) is a
1699
+ * new, additive signal to design, not a change to this method's contract.
1703
1700
  */
1704
1701
  release(): Promise<void>;
1705
1702
  }
@@ -1753,12 +1750,11 @@ interface DatapointSubscriptionHandlers {
1753
1750
  interface DatapointSubscription {
1754
1751
  /**
1755
1752
  * Stops this subscription. The `unsubscribe` frame reaches the server only
1756
- * when this was the **last** holder of the robot and slug pair, and only if
1757
- * the channel is connected — subscriptions are reference-counted across
1758
- * kinds, so releasing this one while a second `datapoints.subscribe`, an
1759
- * `actions.subscribe` or an in-flight `services.call` still holds the same
1760
- * pair leaves that one's stream running untouched. Safe to call more than
1761
- * once.
1753
+ * when this was the **last** holder of the robot/slug pair and the channel
1754
+ * is connected — subscriptions are reference-counted across kinds, so
1755
+ * releasing this one while a `datapoints.subscribe`, `actions.subscribe` or
1756
+ * in-flight `services.call` still holds the same pair leaves that one's
1757
+ * stream running. Safe to call more than once.
1762
1758
  */
1763
1759
  unsubscribe(): void;
1764
1760
  }
@@ -1766,11 +1762,10 @@ interface DatapointSubscription {
1766
1762
  * Window aggregation for `datapoints.history`. `window` and `agg` always
1767
1763
  * travel together on the wire — the cloud refuses one without the other
1768
1764
  * rather than defaulting either, since a silently chosen aggregation is a
1769
- * chart that lies quietly — so they live in one object here instead of two
1770
- * independent optional fields a caller could set only one of. The same
1771
- * reasoning as `cameraSource` being a discriminated union rather than
1772
- * optional fields: make the impossible combination unrepresentable, not
1773
- * merely rejected.
1765
+ * chart that lies quietly — so they live in one object instead of two
1766
+ * optional fields a caller could set only one of. Same reasoning as
1767
+ * `cameraSource`'s discriminated union: make the impossible combination
1768
+ * unrepresentable, not merely rejected.
1774
1769
  */
1775
1770
  interface HistoryAggregation {
1776
1771
  /** Bucket width, e.g. `10s`, `1m`. */
@@ -1781,9 +1776,8 @@ interface HistoryAggregation {
1781
1776
  field?: string;
1782
1777
  }
1783
1778
  /**
1784
- * The window `datapoints.history` reads, and whether it comes back as raw
1785
- * samples or as aggregated buckets. `aggregate` is what decides which of
1786
- * the two responses you get.
1779
+ * The window `datapoints.history` reads. `aggregate` decides whether the
1780
+ * response is raw samples or aggregated buckets.
1787
1781
  */
1788
1782
  interface HistoryOptions {
1789
1783
  /**
@@ -1823,9 +1817,9 @@ interface DatapointsApi {
1823
1817
  * Reference-counted per `(robotId, slug)`: two subscriptions to the same
1824
1818
  * pair share one wire subscription. Unsubscribing one never affects the
1825
1819
  * other — the `unsubscribe` frame is sent only when the last subscriber
1826
- * on that pair goes away. This matters in practice: two widgets showing
1827
- * the same battery value, or a component mounted twice under React
1828
- * StrictMode, both subscribe to the same key. That count is shared with
1820
+ * on that pair goes away. In practice: two widgets showing the same
1821
+ * battery value, or a component mounted twice under React StrictMode,
1822
+ * subscribe to the same key. That count is shared with
1829
1823
  * `actions.subscribe` and `services.call` — a slug is one namespace across
1830
1824
  * kinds, and so is its subscription.
1831
1825
  */
@@ -1838,10 +1832,9 @@ interface DatapointsApi {
1838
1832
  * reduced by `aggregate.agg`. Leave `aggregate` out and the other overload
1839
1833
  * gives you raw samples instead.
1840
1834
  *
1841
- * Two overloads rather than one union so a caller who already knows which
1842
- * they asked for isn't forced to narrow something they determined
1843
- * themselves. `kind` still carries the same information on both, so code
1844
- * holding the result dynamically can still branch on it.
1835
+ * Two overloads rather than one union, so a caller who already knows which
1836
+ * one they asked for isn't forced to narrow it. `kind` carries the same
1837
+ * information either way, so dynamic code can still branch on it.
1845
1838
  *
1846
1839
  * **Rejects, does not silently empty out, two specific refusals** —
1847
1840
  * unlike `cameras.snapshot`'s absorption of `no_snapshot_yet` into a null
@@ -1875,28 +1868,26 @@ interface DatapointsApi {
1875
1868
  }
1876
1869
 
1877
1870
  /**
1878
- * Robot-wide job reads, reachable as `client.jobs`. Everything here is
1879
- * addressed by robot rather than by slug, which is what `actions` and
1880
- * `services` cannot do.
1871
+ * Robot-wide job reads, reachable as `client.jobs` — addressed by robot,
1872
+ * not slug, which `actions` and `services` cannot do.
1881
1873
  */
1882
1874
  interface JobsApi {
1883
1875
  /**
1884
1876
  * Every job the platform currently believes this robot has.
1885
1877
  *
1886
- * `actions.subscribe`/`services.call` and `GET /jobs/:slug` (the per-slug
1887
- * route those build on) all require already knowing the slug. That is not
1888
- * always true: a reconnecting bridge can name a job the cloud only
1889
- * *adopted*, and a configuration change can leave a job on a slug the
1890
- * published document no longer contains. Both are jobs no slug can name,
1891
- * which is exactly what this method is for — an app developer has no
1892
- * other way to reach them.
1893
- *
1894
- * At most one entry per slug: the current job there, exactly what a
1895
- * per-slug read would answer for that slug. Not a history endpoint.
1896
- * Grant-filtered same as `cameras.list`/`datapoints` — an end user or
1897
- * server key sees only jobs on slugs their role grants; a developer
1898
- * session sees every job on the robot. Never empty-vs-missing ambiguity:
1899
- * a robot doing nothing resolves `[]`.
1878
+ * `actions.subscribe`/`services.call` and `GET /jobs/:slug` — the per-slug
1879
+ * route those build on — all require knowing the slug already. Two cases
1880
+ * don't: a reconnecting bridge naming a job the cloud only *adopted*, and
1881
+ * a config change leaving a job on a slug the published document no
1882
+ * longer contains. Neither has a slug to give — this method is the only
1883
+ * way an app developer reaches them.
1884
+ *
1885
+ * At most one entry per slug: the current job there, same as a per-slug
1886
+ * read would answer. Not a history endpoint. Grant-filtered same as
1887
+ * `cameras.list`/`datapoints` — an end user or server key sees only jobs
1888
+ * on slugs their role grants, a developer session sees every job on the
1889
+ * robot. Never empty-vs-missing ambiguity: a robot doing nothing
1890
+ * resolves `[]`.
1900
1891
  *
1901
1892
  * **Ordered newest first by `started_at`, with `job.seq` as the
1902
1893
  * tiebreaker** (`started_at` alone is not a total order — two jobs minted
@@ -1920,16 +1911,15 @@ interface PublishersApi {
1920
1911
  /**
1921
1912
  * Publishes one message to a publisher.
1922
1913
  *
1923
- * This is a plain method call — there is deliberately no deadman switch,
1924
- * rate governor or "takt" helper here. The bridge's own
1925
- * `timeout_ms` failsafe is the platform's safety primitive: if messages
1926
- * stop arriving — including because this process crashed — the bridge
1927
- * publishes the configured failsafe message itself. That does **not**
1928
- * mean the SDK protects a caller who stops calling `publish` on purpose
1929
- * without stopping cleanly (e.g. no repeated call at a safe rate): the
1930
- * safety pattern for *how often* and *when* to publish belongs in the
1931
- * app, not here. See the README's "Publishers, and no teleop helpers"
1932
- * section before building a publisher-driven control loop.
1914
+ * A plain method call — deliberately no deadman switch, rate governor or
1915
+ * "takt" helper. The bridge's own `timeout_ms` failsafe is the platform's
1916
+ * safety primitive: when messages stop arriving, crash included, the
1917
+ * bridge publishes its configured failsafe message. That does **not**
1918
+ * cover a caller who stops calling `publish` on purpose without stopping
1919
+ * cleanly (e.g. no repeated call at a safe rate) — how often and when to
1920
+ * publish is the app's pattern, not the SDK's. See the Publishers
1921
+ * section of the SDK reference before building a
1922
+ * publisher-driven control loop.
1933
1923
  *
1934
1924
  * Rejects `publisher_busy` while a different user is publishing and has
1935
1925
  * not been quiet for its configured quiet timeout yet — whoever publishes
@@ -1940,16 +1930,15 @@ interface PublishersApi {
1940
1930
 
1941
1931
  /**
1942
1932
  * Request/response calls to a robot, reachable as `client.services`. A
1943
- * service answers once and is done, which is why this namespace has a
1944
- * single method and nothing to subscribe to.
1933
+ * service answers once and is done — one method, nothing to subscribe to.
1945
1934
  */
1946
1935
  interface ServicesApi {
1947
1936
  /**
1948
1937
  * Calls a service and resolves with its result. A service call is a job
1949
1938
  * underneath — the same `job_id` exchange and disconnect survival as an
1950
1939
  * action — but that is deliberately invisible here: the caller gets a
1951
- * plain `Promise<result>`, matching the REST `serviceCallResponse` shape's
1952
- * developer experience. There is nothing to subscribe to for a service —
1940
+ * plain `Promise<result>`, matching the REST `serviceCallResponse` shape.
1941
+ * There is nothing to subscribe to for a service —
1953
1942
  * no feedback, no progress, no cancel — so this call already waits for
1954
1943
  * the terminal state internally.
1955
1944
  *
@@ -1962,8 +1951,7 @@ interface ServicesApi {
1962
1951
  * call — the ack that a job was created, plus however much of the
1963
1952
  * budget is left for it to then reach a terminal state — not two
1964
1953
  * separate `timeoutMs`-length windows back to back. A caller who sets
1965
- * `timeoutMs: 5000` is bounding total latency at ~5s, not ~10s; the
1966
- * number means what it says, once, for the whole call.
1954
+ * `timeoutMs: 5000` bounds total latency at ~5s, not ~10s.
1967
1955
  *
1968
1956
  * It is also **not independent** of `patienceMs`: left unset, it
1969
1957
  * is derived from `patienceMs` so this SDK's local clock cannot fire
@@ -1986,10 +1974,9 @@ interface FleetlessClientOptions {
1986
1974
  /** The app's identifier (the slug shown in the console), sent on every login. */
1987
1975
  appIdentifier: string;
1988
1976
  /**
1989
- * Where refresh/access tokens are kept between calls. Defaults to an
1990
- * in-memory store — pass your own (localStorage, a cookie, a native
1991
- * keystore) to persist a session across reloads. The SDK never assumes a
1992
- * browser exists.
1977
+ * Where refresh/access tokens live between calls. Defaults to in-memory —
1978
+ * pass your own (localStorage, a cookie, a native keystore) to persist a
1979
+ * session across reloads. The SDK never assumes a browser exists.
1993
1980
  */
1994
1981
  tokenStore?: TokenStore;
1995
1982
  /**
@@ -2051,13 +2038,13 @@ interface FleetlessClient {
2051
2038
  /** URDF and mesh reads — list/get/urdf, plus the `urdf-loader` mesh callback. */
2052
2039
  readonly assets: AssetsApi;
2053
2040
  /**
2054
- * Closes the realtime channel and stops it from reconnecting. Safe to
2055
- * call whether or not any subscription was ever made, and safe to call
2056
- * more than once. A Node script (the exact use case `serverKey` is for)
2057
- * that never calls this after subscribing will not exit on its own — an
2058
- * open WebSocket keeps the event loop alive. `auth.logout()` calls this
2059
- * automatically; call it yourself too if the process should exit without
2060
- * logging out (e.g. a server-side caller shutting down).
2041
+ * Closes the realtime channel and stops it from reconnecting. Safe with
2042
+ * no subscription ever made, and safe to call twice. A Node script (the
2043
+ * exact use case `serverKey` is for) that never calls this after
2044
+ * subscribing will not exit on its own — an open WebSocket keeps the
2045
+ * event loop alive. `auth.logout()` calls this automatically; call it
2046
+ * yourself if the process should exit without logging out (e.g. a
2047
+ * server-side shutdown).
2061
2048
  */
2062
2049
  close(): void;
2063
2050
  }
package/dist/index.js CHANGED
@@ -304,7 +304,7 @@ function createAssetsApi(http) {
304
304
  finish(
305
305
  null,
306
306
  new Error(
307
- `createMeshLoader and prepareUrdfScene were both installed on the same LoadingManager for robot ${robotId}. Use one or the other on a given manager, not both \u2014 see the README.`
307
+ `createMeshLoader and prepareUrdfScene were both installed on the same LoadingManager for robot ${robotId}. Use one or the other on a given manager, not both \u2014 see the SDK reference.`
308
308
  )
309
309
  );
310
310
  return;
@@ -8302,21 +8302,21 @@ function assertValidJobId(jobId) {
8302
8302
  if (jobId === void 0 || jobId === null || typeof jobId === "string") return;
8303
8303
  throw new FleetlessError(
8304
8304
  "invalid_option",
8305
- `cancel()'s third argument must be a job id (string), null, or omitted \u2014 got ${typeof jobId === "object" ? "an object" : typeof jobId}. If you are passing an options object (e.g. {timeoutMs}) as the third argument, note the signature changed in this release: cancel(robotId, slug) is unchanged, but a third positional argument is now the job id to cancel and options moved to a fourth argument \u2014 cancel(robotId, slug, jobId, options). See the README's Actions section.`
8305
+ `cancel()'s third argument must be a job id (string), null, or omitted \u2014 got ${typeof jobId === "object" ? "an object" : typeof jobId}. If you are passing an options object (e.g. {timeoutMs}) as the third argument, note the signature changed in this release: cancel(robotId, slug) is unchanged, but a third positional argument is now the job id to cancel and options moved to a fourth argument \u2014 cancel(robotId, slug, jobId, options). See the Actions section of the SDK reference at https://docs.fleetless.dev/reference/sdk/`
8306
8306
  );
8307
8307
  }
8308
8308
  function createRealtimeCommandTransport(channel) {
8309
8309
  return {
8310
- // Declared `async` deliberately, unlike `cancel`/`publish` below: it is
8310
+ // Declared `async` deliberately, unlike `cancel`/`publish` below: it's
8311
8311
  // the only one of the three that can refuse *before* sending anything
8312
- // (`resolveLocalWaitMs`'s `invalid_option`), and every caller of
8313
- // this interface — starting with this file's own `sendCommand` callers
8314
- // — is entitled to assume `CommandTransport.invoke` always returns a
8315
- // promise rather than throwing synchronously. Without `async` here, a
8316
- // synchronous throw from `resolveLocalWaitMs` would escape as a thrown
8317
- // exception instead of a rejection, breaking that assumption for any
8318
- // caller that isn't itself inside an `async` function (e.g. a test
8319
- // calling this transport directly).
8312
+ // (`resolveLocalWaitMs`'s `invalid_option`), and every caller of this
8313
+ // interface — starting with this file's own `sendCommand` callers —
8314
+ // assumes `CommandTransport.invoke` always returns a promise, never
8315
+ // throws synchronously. Without `async` here, a synchronous throw from
8316
+ // `resolveLocalWaitMs` would escape as a thrown exception instead of a
8317
+ // rejection, breaking that assumption for a caller that isn't itself
8318
+ // inside an `async` function (e.g. a test calling this transport
8319
+ // directly).
8320
8320
  async invoke(robotId, slug2, params, options) {
8321
8321
  const timeoutMs = resolveLocalWaitMs(options ?? {}, DEFAULT_COMMAND_TIMEOUT_MS);
8322
8322
  const frame = {
@@ -8516,12 +8516,11 @@ var RealtimeChannel = class {
8516
8516
  }
8517
8517
  /**
8518
8518
  * Increments on every successful authentication (first connect and every
8519
- * reconnect). A command sent on one physical socket can only ever be
8520
- * answered on that socket — comparing the epoch captured at send time
8521
- * against the current one is how a caller (see `commands.ts`) tells "still
8522
- * waiting on the connection it was sent over" from "that connection is
8523
- * gone and a new one has taken its place", the moment it happens rather
8524
- * than after a timeout elapses.
8519
+ * reconnect). A command sent on one socket can only be answered on that
8520
+ * socket — comparing the epoch at send time against the current one is
8521
+ * how a caller (see `commands.ts`) tells "still waiting on the connection
8522
+ * it was sent over" from "that connection is gone and a new one has taken
8523
+ * its place", the moment it happens rather than after a timeout elapses.
8525
8524
  */
8526
8525
  get connectionEpoch() {
8527
8526
  return this.#connectionEpoch;
@@ -8822,7 +8821,7 @@ function createSlugSubscriptions(channel) {
8822
8821
  // src/token-store.ts
8823
8822
  var InMemoryTokenStore = class {
8824
8823
  #session = null;
8825
- /** Creates an empty store. Nothing is loaded from anywhere — a client built with it starts logged out. */
8824
+ /** Nothing is loaded from anywhere — a client built with it starts logged out. */
8826
8825
  constructor() {
8827
8826
  }
8828
8827
  /** Returns the session held in memory, or `null` if there is none. */
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@fleetless/sdk",
3
- "version": "3.0.2",
4
- "description": "The official TypeScript SDK for Fleetless client apps \u2014 a ROS 2 robot as a REST and realtime API, cameras, jobs, and the app's own user accounts, federated sign-in and MCP consent.",
3
+ "version": "3.0.3",
4
+ "description": "The official TypeScript SDK for Fleetless client apps — a ROS 2 robot as a REST and realtime API, cameras, jobs, and the app's own user accounts, federated sign-in and MCP consent.",
5
5
  "license": "MIT",
6
6
  "author": {
7
7
  "name": "Dehne Robotik GmbH",
@@ -9,6 +9,10 @@
9
9
  "url": "https://dehne-robotik.de"
10
10
  },
11
11
  "homepage": "https://docs.fleetless.dev/reference/sdk/",
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "https://github.com/fleetless/sdk"
15
+ },
12
16
  "bugs": {
13
17
  "email": "hello@fleetless.dev"
14
18
  },
@@ -51,8 +55,7 @@
51
55
  "CHANGELOG.md",
52
56
  "SECURITY.md",
53
57
  "CONTRIBUTING.md",
54
- "CODE_OF_CONDUCT.md",
55
- "RELEASING.md"
58
+ "CODE_OF_CONDUCT.md"
56
59
  ],
57
60
  "engines": {
58
61
  "node": ">=20"
@@ -61,6 +64,7 @@
61
64
  "scripts": {
62
65
  "build": "tsup && node scripts/stamp-dist.mjs",
63
66
  "prepare": "tsup && node scripts/stamp-dist.mjs",
67
+ "prepublishOnly": "node scripts/refuse-manual-publish.mjs",
64
68
  "typecheck": "tsc --noEmit",
65
69
  "test": "vitest run",
66
70
  "test:pack": "node scripts/verify-published-types.mjs",
package/RELEASING.md DELETED
@@ -1,195 +0,0 @@
1
- # Releasing `@fleetless/sdk`
2
-
3
- Maintainer notes: the development setup, the checks, and the release
4
- procedure. This file is not part of the published package.
5
-
6
- **CI runs on GitLab; the repository will mirror to GitHub.** The pipeline
7
- that verifies and publishes this package lives on an internal GitLab instance,
8
- and that is the only thing that publishes to npm. The public repository does not
9
- exist yet; when it does it is a mirror, where issues and pull requests arrive
10
- and no check runs. A contributor's pull request is verified by a maintainer
11
- running the same commands locally — see
12
- [CONTRIBUTING.md](CONTRIBUTING.md), which says so to the contributor as well.
13
-
14
- A mirror pushes what it is given, so **everything in a commit becomes public
15
- the moment it is pushed**, including the commit message and every file the
16
- branch touched.
17
- ## Development setup
18
-
19
- Node 22 via `nvm`, and pnpm through corepack:
20
-
21
- ```sh
22
- nvm use 22
23
- corepack enable
24
- pnpm install
25
- ```
26
-
27
- **That install needs nothing private.** `@fleetless/contracts` — the source
28
- of truth for every wire type this SDK reads or writes — is a devDependency on
29
- the public npm package, pinned to an exact version. It used to be a private
30
- `git+ssh://` URL that only somebody with GitLab group access could install;
31
- that is over. The published SDK is unaffected either way: `tsup` inlines
32
- contracts' types into `dist/`, so a consumer of `@fleetless/sdk` never
33
- resolves it.
34
-
35
- Never redefine a shape the SDK sends to or reads from the API. Import it from
36
- `@fleetless/contracts` instead.
37
-
38
- ## Checks
39
-
40
- | Command | What it does |
41
- |---|---|
42
- | `pnpm typecheck` | `tsc --noEmit` over the package. |
43
- | `pnpm test` | vitest. Most suites drive a fake `fetch` and a fake WebSocket; the three auth suites drive the SDK's own default `fetch` against a real `node:http` server (`test/local-api.ts`). No cloud needed. |
44
- | `pnpm build` | tsup into `dist/` — ESM, CJS and `.d.ts`. |
45
- | `pnpm run test:pack` | `npm pack`s the tarball, installs it into a bare project with nothing but `typescript`, and typechecks and runs real usage against it. This is what catches a `dist/index.d.ts` that still imports from `@fleetless/contracts` — a devDependency, so a consumer of the SDK never installs it. |
46
-
47
- Two further scripts check the built package against a **running** cloud
48
- (`./infra/dev.sh` in the umbrella repo). Each one runs against `dist/`, not
49
- `src/`, so run `pnpm build` first, and each fails by name on a missing
50
- variable rather than defaulting quietly.
51
-
52
- - `pnpm run verify:live` — a `busy` refusal carrying the job that is already
53
- running, `command_outcome_unknown` and its documented recovery, a
54
- `parameter_invalid` naming the flat key, job-id-addressed cancel, and
55
- per-session camera release. Needs `FLEETLESS_API_URL` (or `API`),
56
- `APP_IDENTIFIER`, `EMAIL`, `PASSWORD`, `ROBOT_ID`, `ACTION_SLUG`,
57
- `SERVICE_SLUG`, `CAMERA_SLUG`, and two optional identities:
58
- - `SECOND_EMAIL` — a second, distinct **app user** with the same role,
59
- without which the busy check only proves that a second request is refused,
60
- not that a different user's is.
61
- - `OBSERVER_EMAIL` — a **third** app user, and the one whose password check
62
- [6] rotates and restores. Leave it unset and the round trip runs on
63
- `EMAIL`, the identity every other check in the file signs in as; the
64
- script says so out loud when that happens. `infra/seed-dev.mjs --env`
65
- exports all three.
66
-
67
- Since 3.0.0 it also drives the client auth API as check [6]: `listProviders`
68
- without a session, `login`, `me` (kind, app, role, address), `logout`, the
69
- `changePassword` round trip with its restore, and the reset acknowledgement
70
- compared byte for byte across a known and an unknown address. What it does
71
- **not** drive is anything needing a mailed token — `register`,
72
- `verifyEmail`, `acceptInvitation`, `confirmPasswordReset` — or the federated
73
- and MCP-consent flows. Those are `infra/browser/app-auth-check.mjs`'s
74
- subject, which reads maildev and drives a real Keycloak.
75
- - `pnpm run verify:history` — a relative range and its absolute equivalent
76
- returning the same samples, aggregation matching arithmetic done here from
77
- the raw rows, and the `not_recorded` / `not_aggregatable` refusals. Needs
78
- `FLEETLESS_API_URL`, `APP_IDENTIFIER`, `EMAIL`, `PASSWORD`, `ROBOT_ID`,
79
- `RECORDED_NUMERIC_SLUG`, `LIVE_ONLY_SLUG`, and optionally
80
- `NON_NUMERIC_RECORDED_SLUG`.
81
- (`verify:hosted-login` is gone. The hosted login flow it drove —
82
- `beginHostedLogin` / `completeHostedLogin` and the app OAuth client behind
83
- them — was removed in 3.0.0 along with the script and its `package.json`
84
- entry.)
85
-
86
- `infra/seed-dev.mjs --env` in the umbrella repo exports most of those
87
- variables for a freshly seeded world.
88
-
89
- ## Commits
90
-
91
- [Conventional Commits](https://www.conventionalcommits.org/). English, for
92
- code, comments, commit messages and everything else that lands in the
93
- repository.
94
-
95
- ## Releasing
96
-
97
- Every version on npm is published by this repository's GitLab pipeline from a
98
- release tag. `npm publish` by hand is retired.
99
-
100
- 1. Add the version's entry to `CHANGELOG.md` and set `version` in
101
- `package.json` to the same number.
102
- 2. Commit (`chore(release): X.Y.Z`), push, and wait for the branch pipeline's
103
- `verify` job to go green.
104
- 3. `git tag vX.Y.Z && git push origin vX.Y.Z`. The tag pipeline runs `verify`
105
- again and then `publish`.
106
- 4. Check the registry yourself. The `publish` job already asserts the first
107
- line; this is the independent look, and the second line is the one that
108
- says which dist-tag moved.
109
-
110
- ```sh
111
- npm view @fleetless/sdk@X.Y.Z version # answers X.Y.Z
112
- npm view @fleetless/sdk dist-tags # latest -> X.Y.Z, or next -> X.Y.Z
113
- ```
114
-
115
- Name the version. A bare `npm view @fleetless/sdk version` resolves the
116
- `latest` dist-tag, so after a pre-release publish it answers the *previous*
117
- stable release and reads as a publish that did not happen.
118
-
119
- A pre-release tag — `vX.Y.Z-beta.1`, `vX.Y.Z-rc.2` — publishes under the npm
120
- dist-tag `next` instead of `latest`. Nothing else about it differs, and it is
121
- exactly why step 4 names the version.
122
-
123
- **A red `publish` job does not mean nothing was published.** The job runs
124
- `npm publish` and then looks the version up on the registry for about two
125
- minutes; npm answers reads from a replica that lags a publish, so the lookup
126
- can time out on a version that did land. The job says so itself, and it is
127
- worth repeating here because a red pipeline invites exactly one reaction —
128
- press retry — and that reaction cannot work: npm refuses to publish over an
129
- existing version, so the retry ends in a 403 that reads like a broken
130
- pipeline rather than like a release that already happened.
131
-
132
- > publish may have succeeded; the registry has not served the version yet; do
133
- > NOT retry this job (npm refuses to republish a version) — check
134
- > `npm view @fleetless/sdk@$VERSION` by hand
135
-
136
- If the hand check answers the version, the release is done: move the dist-tag
137
- by hand if it is wrong (`npm dist-tag add @fleetless/sdk@X.Y.Z latest`) and
138
- leave the job red. If it answers nothing after several minutes, the publish
139
- genuinely did not land and the job can be retried.
140
-
141
- **A red `publish` job that ends in `npm error code EOTP` published nothing.**
142
- npm is asking for a one-time password, which a pipeline cannot supply: the
143
- token in `NPM_TOKEN` does not bypass two-factor authentication, or the
144
- package's *Publishing access* setting on npmjs.com disallows tokens. Fix the
145
- token (a granular access token created with *Bypass two-factor
146
- authentication*) or the package setting (*Require two-factor authentication
147
- or an automation token*), then retry the job — nothing reached the registry,
148
- so a retry is safe here — measured on a pipeline that hit it.
149
-
150
- **The pipeline refuses a tag whose version disagrees with `package.json`.**
151
- `scripts/verify-version-tag.mjs` is the one place that rule lives; `verify`
152
- runs it first on a tag pipeline, so a mistyped tag fails in seconds and
153
- `publish` never starts (measured: `verify-version-tag: tag v9.9.9 names
154
- 9.9.9 but package.json says 1.0.0`, publish skipped).
155
-
156
- Removing that bad tag takes the API, not git. `v*` is a **protected** tag
157
- pattern, so a delete over git is refused by the server with nothing but
158
- `! [remote rejected] v9.9.9 (pre-receive hook declined)`:
159
-
160
- ```sh
161
- git tag -d vX.Y.Z # local
162
- glab api -X DELETE projects/37/repository/tags/vX.Y.Z # remote
163
- ```
164
-
165
- Then fix `package.json` and tag again.
166
-
167
- ### The one-time setting
168
-
169
- It is already in place; it is written down because nothing in this repository
170
- would tell you it exists if it were removed.
171
-
172
- There used to be a second one — `fleetless/fleetless-sdk` on the job-token
173
- allow-list of `fleetless/fleetless-contracts`, so the pipeline could rewrite
174
- the private `git+ssh://` contracts URL to HTTPS with `CI_JOB_TOKEN`. Contracts
175
- is a public npm package now, the rewrite is gone from `.gitlab-ci.yml`, and
176
- the allow-list entry buys this project nothing. Removing it is safe; leaving
177
- it is harmless.
178
-
179
- - **`NPM_TOKEN` is a protected, masked CI variable on this project**, holding
180
- an npm granular automation token with publish rights on `@fleetless/sdk`.
181
- Protected means an unprotected ref receives an *empty* value rather than no
182
- value, which is why the tag pattern `v*` is a protected tag and why the
183
- `publish` job's first action is to refuse an empty `NPM_TOKEN` by name.
184
-
185
- ```sh
186
- glab api projects/37/protected_tags # v*, create access: Maintainers
187
- ```
188
-
189
- The token reaches npm through an `.npmrc` written in the job's working
190
- directory holding `//registry.npmjs.org/:_authToken=${NPM_TOKEN}` **literally**
191
- — npm expands the variable when it reads the file, so the secret itself never
192
- lands on disk, and `after_script` removes the file either way. It cannot be
193
- passed as an environment assignment instead: `NPM_CONFIG_//registry…` is not a
194
- valid shell identifier, so both `bash` and `dash` parse it as a command name.
195
-