@relayfile/sdk 0.10.61 → 0.10.62

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/client.d.ts CHANGED
@@ -12,7 +12,15 @@ export type AccessTokenProvider = string | (() => string | Promise<string>);
12
12
  * `RELAYFILE_REFRESH_TOKEN`). Pass this as `token` and the client auto-wraps it
13
13
  * in the correct rotating provider — a RelayAuth `relay_pa` pair refreshes at
14
14
  * RelayAuth, a Cloud device-auth pair at Cloud — so the short access token is
15
- * rotated automatically with no extra wiring.
15
+ * rotated automatically.
16
+ *
17
+ * Rotation is in-memory: on refresh the refresh token rotates too and the old
18
+ * one is revoked, so the pair you passed here (e.g. the one in your `.env`) is
19
+ * spent after the first refresh. In a long-lived process this is invisible, but
20
+ * a process that restarts (a redeploy, a new worker) will reload the original,
21
+ * now-revoked pair and fail with `invalid_grant` / "refresh token revoked".
22
+ * Supply {@link RelayFileClientOptions.onTokens} to persist each rotated pair so
23
+ * the next process boots from a live credential.
16
24
  */
17
25
  export interface RelayFileTokenPair {
18
26
  accessToken: string;
@@ -25,6 +33,15 @@ export interface RelayFileTokenPair {
25
33
  /** Cloud API URL for a Cloud device-auth pair. */
26
34
  apiUrl?: string;
27
35
  }
36
+ /**
37
+ * Called after the client rotates the access+refresh {@link RelayFileTokenPair}.
38
+ * Persist the new pair (e.g. write it back to your secret store / `.env`) so a
39
+ * later process starts from a live credential instead of the revoked original.
40
+ * The callback is best-effort and retried on the next request until it succeeds;
41
+ * a throw is swallowed (the in-memory access token stays usable) but leaves the
42
+ * pair unpersisted, so make it durable.
43
+ */
44
+ export type RelayFileTokenPersister = (tokens: RelayFileTokenPair) => void | Promise<void>;
28
45
  export interface RelayFileRetryOptions {
29
46
  maxRetries?: number;
30
47
  baseDelayMs?: number;
@@ -58,6 +75,15 @@ export interface RelayFileClientOptions {
58
75
  * and `aud` containing `relayfile`.
59
76
  */
60
77
  token: AccessTokenProvider | RelayFileTokenPair;
78
+ /**
79
+ * Persistence hook for a rotating {@link RelayFileTokenPair}. Called with the
80
+ * new pair each time the access token is refreshed (the refresh token rotates
81
+ * too). Write it back to your secret store so a process that restarts reloads a
82
+ * live credential instead of the spent original — without this, a `token` pair
83
+ * survives only within one process lifetime. Ignored when `token` is a string
84
+ * or token factory (that provider owns its own rotation).
85
+ */
86
+ onTokens?: RelayFileTokenPersister;
61
87
  fetchImpl?: typeof fetch;
62
88
  userAgent?: string;
63
89
  retry?: RelayFileRetryOptions;
package/dist/client.js CHANGED
@@ -4,8 +4,9 @@ import { createRelayauthPathTokenAccessTokenProvider, isRelayauthRefreshToken }
4
4
  import { InvalidStateError, MergeConflictError, ParentMovedError, PayloadTooLargeError, QueueFullError, RelayFileApiError, RevisionConflictError } from "./errors.js";
5
5
  // Resolve the `token` option into a concrete AccessTokenProvider. A string or
6
6
  // function is used as-is; a token PAIR is auto-wrapped in the rotating provider
7
- // that matches the refresh token's issuer.
8
- function resolveTokenOption(token) {
7
+ // that matches the refresh token's issuer. When `onTokens` is supplied, each
8
+ // rotated pair is handed back so the caller can persist it across restarts.
9
+ function resolveTokenOption(token, onTokens) {
9
10
  if (typeof token === "string" || typeof token === "function") {
10
11
  return token;
11
12
  }
@@ -16,7 +17,17 @@ function resolveTokenOption(token) {
16
17
  accessTokenExpiresAt: token.accessTokenExpiresAt,
17
18
  refreshTokenExpiresAt: token.refreshTokenExpiresAt,
18
19
  relayauthUrl: token.relayauthUrl
19
- });
20
+ }, onTokens
21
+ ? {
22
+ onTokens: (rotated) => onTokens({
23
+ accessToken: rotated.accessToken,
24
+ refreshToken: rotated.refreshToken,
25
+ accessTokenExpiresAt: rotated.accessTokenExpiresAt,
26
+ refreshTokenExpiresAt: rotated.refreshTokenExpiresAt,
27
+ relayauthUrl: rotated.relayauthUrl
28
+ })
29
+ }
30
+ : undefined);
20
31
  }
21
32
  return createRelayfileCloudAccessTokenProvider({
22
33
  apiUrl: token.apiUrl,
@@ -25,7 +36,17 @@ function resolveTokenOption(token) {
25
36
  // The cloud provider treats an unparseable expiry as "refresh now".
26
37
  accessTokenExpiresAt: token.accessTokenExpiresAt ?? "",
27
38
  refreshTokenExpiresAt: token.refreshTokenExpiresAt
28
- });
39
+ }, onTokens
40
+ ? {
41
+ onTokens: (rotated) => onTokens({
42
+ accessToken: rotated.accessToken,
43
+ refreshToken: rotated.refreshToken,
44
+ accessTokenExpiresAt: rotated.accessTokenExpiresAt,
45
+ refreshTokenExpiresAt: rotated.refreshTokenExpiresAt,
46
+ apiUrl: rotated.apiUrl
47
+ })
48
+ }
49
+ : undefined);
29
50
  }
30
51
  /** Default base URL for the hosted Relayfile API */
31
52
  export const DEFAULT_RELAYFILE_BASE_URL = "https://api.relayfile.dev";
@@ -1249,7 +1270,7 @@ export class RelayFileClient {
1249
1270
  retryOptions;
1250
1271
  constructor(options) {
1251
1272
  this.baseUrl = (options.baseUrl ?? DEFAULT_RELAYFILE_BASE_URL).replace(/\/+$/, "");
1252
- this.tokenProvider = resolveTokenOption(options.token);
1273
+ this.tokenProvider = resolveTokenOption(options.token, options.onTokens);
1253
1274
  this.fetchImpl = options.fetchImpl ?? fetch.bind(globalThis);
1254
1275
  this.userAgent = options.userAgent;
1255
1276
  this.retryOptions = normalizeRetryOptions(options.retry);
@@ -14,6 +14,11 @@ export function createRelayfileCloudAccessTokenProvider(initialTokens, options =
14
14
  apiUrl: normalizeNonEmptyString(initialTokens.apiUrl) ?? cloudApiUrl
15
15
  };
16
16
  let refreshPromise;
17
+ // A rotated token set whose `onTokens` persistence callback has not yet
18
+ // succeeded. Until it does, a consumed refresh token may still live in the
19
+ // caller's persisted copy (→ `invalid_grant` after restart), so we retry the
20
+ // callback on every subsequent use rather than treating rotation as complete.
21
+ let pendingPersist;
17
22
  return async () => {
18
23
  if (shouldRefresh(tokens, refreshWindowMs)) {
19
24
  if (!refreshPromise) {
@@ -26,6 +31,10 @@ export function createRelayfileCloudAccessTokenProvider(initialTokens, options =
26
31
  refreshPromise = undefined;
27
32
  }
28
33
  }
34
+ else if (pendingPersist) {
35
+ // No refresh needed, but a prior rotation still owes a successful persist.
36
+ await persistPendingTokens();
37
+ }
29
38
  return tokens.accessToken;
30
39
  };
31
40
  async function refresh() {
@@ -42,7 +51,32 @@ export function createRelayfileCloudAccessTokenProvider(initialTokens, options =
42
51
  throw new CloudApiError(response.status, payload);
43
52
  }
44
53
  tokens = readTokenSetFromPayload(payload, cloudApiUrl);
45
- await options.onTokens?.({ ...tokens });
54
+ pendingPersist = { ...tokens };
55
+ await persistPendingTokens();
56
+ }
57
+ // Best-effort, retried persistence. A callback failure never rejects the
58
+ // provider — the in-memory access token is valid and usable — but the set
59
+ // stays pending so the next provider() call re-attempts, so a durable copy
60
+ // eventually catches up with the rotated refresh token.
61
+ async function persistPendingTokens() {
62
+ if (!pendingPersist) {
63
+ return;
64
+ }
65
+ if (!options.onTokens) {
66
+ pendingPersist = undefined;
67
+ return;
68
+ }
69
+ const toPersist = pendingPersist;
70
+ try {
71
+ await options.onTokens({ ...toPersist });
72
+ // Only clear if nothing rotated again while the callback was in flight.
73
+ if (pendingPersist === toPersist) {
74
+ pendingPersist = undefined;
75
+ }
76
+ }
77
+ catch {
78
+ // Keep pending; retried on the next use.
79
+ }
46
80
  }
47
81
  }
48
82
  function shouldRefresh(tokens, refreshWindowMs) {
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { RelayFileClient, DEFAULT_RELAYFILE_BASE_URL, type AccessTokenProvider, type RelayFileTokenPair, type RelayFileChangeLogOptions, type ConnectWebSocketOptions, type RelayFileClientOptions, type RelayFileRetryOptions, type WebSocketConnection } from "./client.js";
1
+ export { RelayFileClient, DEFAULT_RELAYFILE_BASE_URL, type AccessTokenProvider, type RelayFileTokenPair, type RelayFileTokenPersister, type RelayFileChangeLogOptions, type ConnectWebSocketOptions, type RelayFileClientOptions, type RelayFileRetryOptions, type WebSocketConnection } from "./client.js";
2
2
  export type { RelayFileReadCacheOptions } from "./types.js";
3
3
  export { RelayfileSetup, RELAYFILE_SDK_VERSION, WorkspaceHandle } from "./setup.js";
4
4
  export { type RelayfileCloudLoginOptions, type RelayfileCloudTokenSet, type RelayfileCloudTokenSetupOptions } from "./cloud-login.js";
@@ -1 +1 @@
1
- export declare const RELAYFILE_VERSION = "0.10.61";
1
+ export declare const RELAYFILE_VERSION = "0.10.62";
@@ -1,2 +1,2 @@
1
1
  // Generated by scripts/sync-package-version.mjs during every SDK build.
2
- export const RELAYFILE_VERSION = "0.10.61";
2
+ export const RELAYFILE_VERSION = "0.10.62";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@relayfile/sdk",
3
- "version": "0.10.61",
3
+ "version": "0.10.62",
4
4
  "description": "TypeScript SDK for relayfile — real-time filesystem for humans and agents",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -60,15 +60,15 @@
60
60
  "prepublishOnly": "npm run build"
61
61
  },
62
62
  "dependencies": {
63
- "@relayfile/core": "0.10.61",
63
+ "@relayfile/core": "0.10.62",
64
64
  "ignore": "^7.0.5",
65
65
  "tar": "^7.5.10"
66
66
  },
67
67
  "optionalDependencies": {
68
- "@relayfile/mount-darwin-arm64": "0.10.61",
69
- "@relayfile/mount-darwin-x64": "0.10.61",
70
- "@relayfile/mount-linux-arm64": "0.10.61",
71
- "@relayfile/mount-linux-x64": "0.10.61"
68
+ "@relayfile/mount-darwin-arm64": "0.10.62",
69
+ "@relayfile/mount-darwin-x64": "0.10.62",
70
+ "@relayfile/mount-linux-arm64": "0.10.62",
71
+ "@relayfile/mount-linux-x64": "0.10.62"
72
72
  },
73
73
  "devDependencies": {
74
74
  "typescript": "^5.7.3",