@relayfile/sdk 0.10.59 → 0.10.61
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/cli/setup.js +3 -9
- package/dist/client.d.ts +23 -2
- package/dist/client.js +28 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -0
- package/dist/package-version.d.ts +1 -1
- package/dist/package-version.js +1 -1
- package/dist/relayauth-token-provider.d.ts +22 -0
- package/dist/relayauth-token-provider.js +250 -0
- package/dist/setup.d.ts +10 -0
- package/dist/setup.js +34 -7
- package/package.json +6 -6
package/dist/cli/setup.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import { createRelayfileCloudAccessTokenProvider } from "../cloud-token-provider.js";
|
|
2
1
|
import { runRelayfileCloudLogin } from "../cloud-login.js";
|
|
3
2
|
import { defaultMountLauncher, readMountedWorkspaceStatus } from "../mount-launcher.js";
|
|
4
|
-
import { RelayfileSetup as BaseRelayfileSetup } from "../setup.js";
|
|
3
|
+
import { RelayfileSetup as BaseRelayfileSetup, resolveCloudTokensAccessToken } from "../setup.js";
|
|
5
4
|
const DEFAULT_CLOUD_API_URL = "https://agentrelay.com/cloud";
|
|
6
5
|
export class RelayfileSetup extends BaseRelayfileSetup {
|
|
7
6
|
static async login(options = {}) {
|
|
@@ -21,13 +20,8 @@ export class RelayfileSetup extends BaseRelayfileSetup {
|
|
|
21
20
|
return new RelayfileSetup({
|
|
22
21
|
...options,
|
|
23
22
|
cloudApiUrl,
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
apiUrl: tokens.apiUrl ?? cloudApiUrl
|
|
27
|
-
}, {
|
|
28
|
-
...options,
|
|
29
|
-
cloudApiUrl
|
|
30
|
-
})
|
|
23
|
+
// Shared routing: relay_pa pairs rotate at RelayAuth, cloud pairs at Cloud.
|
|
24
|
+
accessToken: resolveCloudTokensAccessToken(tokens, options, cloudApiUrl)
|
|
31
25
|
});
|
|
32
26
|
}
|
|
33
27
|
getDefaultMountLauncher() {
|
package/dist/client.d.ts
CHANGED
|
@@ -7,6 +7,24 @@ import type { ForkHandle } from "@relayfile/core";
|
|
|
7
7
|
* `{ workspace_id: "ws_123", agent_name: "review-bot", aud: ["relayfile"] }`
|
|
8
8
|
*/
|
|
9
9
|
export type AccessTokenProvider = string | (() => string | Promise<string>);
|
|
10
|
+
/**
|
|
11
|
+
* An access+refresh token pair (e.g. `RELAYFILE_ACCESS_TOKEN` +
|
|
12
|
+
* `RELAYFILE_REFRESH_TOKEN`). Pass this as `token` and the client auto-wraps it
|
|
13
|
+
* in the correct rotating provider — a RelayAuth `relay_pa` pair refreshes at
|
|
14
|
+
* RelayAuth, a Cloud device-auth pair at Cloud — so the short access token is
|
|
15
|
+
* rotated automatically with no extra wiring.
|
|
16
|
+
*/
|
|
17
|
+
export interface RelayFileTokenPair {
|
|
18
|
+
accessToken: string;
|
|
19
|
+
refreshToken: string;
|
|
20
|
+
/** ISO expiry of the access token; derived from its `exp` claim when omitted. */
|
|
21
|
+
accessTokenExpiresAt?: string;
|
|
22
|
+
refreshTokenExpiresAt?: string;
|
|
23
|
+
/** RelayAuth base URL; derived from the refresh token's `iss` when omitted. */
|
|
24
|
+
relayauthUrl?: string;
|
|
25
|
+
/** Cloud API URL for a Cloud device-auth pair. */
|
|
26
|
+
apiUrl?: string;
|
|
27
|
+
}
|
|
10
28
|
export interface RelayFileRetryOptions {
|
|
11
29
|
maxRetries?: number;
|
|
12
30
|
baseDelayMs?: number;
|
|
@@ -31,12 +49,15 @@ export interface RelayFileClientOptions {
|
|
|
31
49
|
/** API base URL. Defaults to https://api.relayfile.dev */
|
|
32
50
|
baseUrl?: string;
|
|
33
51
|
/**
|
|
34
|
-
* Bearer token
|
|
52
|
+
* Bearer token, token factory, or an access+refresh {@link RelayFileTokenPair}
|
|
53
|
+
* for SDK requests. A pair is auto-wrapped in a rotating provider (RelayAuth
|
|
54
|
+
* `relay_pa` pairs refresh at RelayAuth, Cloud pairs at Cloud), so the short
|
|
55
|
+
* access token is renewed automatically.
|
|
35
56
|
*
|
|
36
57
|
* Relayfile-authenticated JWTs should include `workspace_id`, `agent_name`,
|
|
37
58
|
* and `aud` containing `relayfile`.
|
|
38
59
|
*/
|
|
39
|
-
token: AccessTokenProvider;
|
|
60
|
+
token: AccessTokenProvider | RelayFileTokenPair;
|
|
40
61
|
fetchImpl?: typeof fetch;
|
|
41
62
|
userAgent?: string;
|
|
42
63
|
retry?: RelayFileRetryOptions;
|
package/dist/client.js
CHANGED
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
import { RelayFileSync, normalizeFilesystemEvent } from "./sync.js";
|
|
2
|
+
import { createRelayfileCloudAccessTokenProvider } from "./cloud-token-provider.js";
|
|
3
|
+
import { createRelayauthPathTokenAccessTokenProvider, isRelayauthRefreshToken } from "./relayauth-token-provider.js";
|
|
2
4
|
import { InvalidStateError, MergeConflictError, ParentMovedError, PayloadTooLargeError, QueueFullError, RelayFileApiError, RevisionConflictError } from "./errors.js";
|
|
5
|
+
// Resolve the `token` option into a concrete AccessTokenProvider. A string or
|
|
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) {
|
|
9
|
+
if (typeof token === "string" || typeof token === "function") {
|
|
10
|
+
return token;
|
|
11
|
+
}
|
|
12
|
+
if (isRelayauthRefreshToken(token.refreshToken)) {
|
|
13
|
+
return createRelayauthPathTokenAccessTokenProvider({
|
|
14
|
+
accessToken: token.accessToken,
|
|
15
|
+
refreshToken: token.refreshToken,
|
|
16
|
+
accessTokenExpiresAt: token.accessTokenExpiresAt,
|
|
17
|
+
refreshTokenExpiresAt: token.refreshTokenExpiresAt,
|
|
18
|
+
relayauthUrl: token.relayauthUrl
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
return createRelayfileCloudAccessTokenProvider({
|
|
22
|
+
apiUrl: token.apiUrl,
|
|
23
|
+
accessToken: token.accessToken,
|
|
24
|
+
refreshToken: token.refreshToken,
|
|
25
|
+
// The cloud provider treats an unparseable expiry as "refresh now".
|
|
26
|
+
accessTokenExpiresAt: token.accessTokenExpiresAt ?? "",
|
|
27
|
+
refreshTokenExpiresAt: token.refreshTokenExpiresAt
|
|
28
|
+
});
|
|
29
|
+
}
|
|
3
30
|
/** Default base URL for the hosted Relayfile API */
|
|
4
31
|
export const DEFAULT_RELAYFILE_BASE_URL = "https://api.relayfile.dev";
|
|
5
32
|
const DEFAULT_RETRY_OPTIONS = {
|
|
@@ -1222,7 +1249,7 @@ export class RelayFileClient {
|
|
|
1222
1249
|
retryOptions;
|
|
1223
1250
|
constructor(options) {
|
|
1224
1251
|
this.baseUrl = (options.baseUrl ?? DEFAULT_RELAYFILE_BASE_URL).replace(/\/+$/, "");
|
|
1225
|
-
this.tokenProvider = options.token;
|
|
1252
|
+
this.tokenProvider = resolveTokenOption(options.token);
|
|
1226
1253
|
this.fetchImpl = options.fetchImpl ?? fetch.bind(globalThis);
|
|
1227
1254
|
this.userAgent = options.userAgent;
|
|
1228
1255
|
this.retryOptions = normalizeRetryOptions(options.retry);
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
export { RelayFileClient, DEFAULT_RELAYFILE_BASE_URL, type AccessTokenProvider, 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 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";
|
|
5
|
+
export { createRelayauthPathTokenAccessTokenProvider, isRelayauthRefreshToken, type RelayauthPathTokenSet, type RelayauthPathTokenSetupOptions } from "./relayauth-token-provider.js";
|
|
5
6
|
export { CloudAbortError, CloudApiError, CloudTimeoutError, InvalidLocalDirError, InvalidMountModeError, InvalidRemotePathError, IntegrationConnectionTimeoutError, MalformedCloudResponseError, MissingConnectionIdError, MountModeUnavailableError, MountMultiPathUnsupportedError, MountReadyTimeoutError, MountSessionInputError, ProviderNotConnectedError, ProviderNotReadyError, RelayfileSetupError, UnknownProviderError } from "./setup-errors.js";
|
|
6
7
|
export { type EnsureMountedWorkspaceInput, WORKSPACE_INTEGRATION_PROVIDERS, type AgentWorkspaceInvite, type AgentWorkspaceInviteOptions, type AgentWorkspaceScopedInviteOptions, type ConnectIntegrationOptions, type ConnectIntegrationResult, type CheckpointAndSealInput, type CreateWorkspaceOptions, type JoinWorkspaceOptions, type MountLauncher, type MountLauncherEvent, type MountLauncherInstance, type MountLauncherStart, type MountLocalLayout, type MountMode, type MountSessionRequest, type MountSessionResponse, type MountSessionResult, type MountSyncMode, type MountSupervisorEvent, type MountedWorkspaceHandle, type MountedWorkspaceStatus, type MountWorkspaceInput, type ReadMountedWorkspaceStatusInput, type RelayfileSetupOptions, type RelayfileSetupRetryOptions, type WaitForConnectionOptions, type WorkspaceInfo, type WorkspaceIntegrationProvider, type WorkspaceMountEnv, type WorkspaceMountEnvOptions, type WorkspacePermissions } from "./setup-types.js";
|
|
7
8
|
export { RelayFileSync, type RelayFileSyncOptions, type RelayFileSyncPong, type RelayFileSyncReconnectOptions, type RelayFileSyncSocket, type RelayFileSyncStart, type RelayFileSyncState, type RelayFileSyncTokenProvider } from "./sync.js";
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { RelayFileClient, DEFAULT_RELAYFILE_BASE_URL } from "./client.js";
|
|
2
2
|
export { RelayfileSetup, RELAYFILE_SDK_VERSION, WorkspaceHandle } from "./setup.js";
|
|
3
|
+
export { createRelayauthPathTokenAccessTokenProvider, isRelayauthRefreshToken } from "./relayauth-token-provider.js";
|
|
3
4
|
export { CloudAbortError, CloudApiError, CloudTimeoutError, InvalidLocalDirError, InvalidMountModeError, InvalidRemotePathError, IntegrationConnectionTimeoutError, MalformedCloudResponseError, MissingConnectionIdError, MountModeUnavailableError, MountMultiPathUnsupportedError, MountReadyTimeoutError, MountSessionInputError, ProviderNotConnectedError, ProviderNotReadyError, RelayfileSetupError, UnknownProviderError } from "./setup-errors.js";
|
|
4
5
|
export { WORKSPACE_INTEGRATION_PROVIDERS } from "./setup-types.js";
|
|
5
6
|
export { RelayFileSync } from "./sync.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const RELAYFILE_VERSION = "0.10.
|
|
1
|
+
export declare const RELAYFILE_VERSION = "0.10.61";
|
package/dist/package-version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Generated by scripts/sync-package-version.mjs during every SDK build.
|
|
2
|
-
export const RELAYFILE_VERSION = "0.10.
|
|
2
|
+
export const RELAYFILE_VERSION = "0.10.61";
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { AccessTokenProvider } from "./client.js";
|
|
2
|
+
export interface RelayauthPathTokenSet {
|
|
3
|
+
accessToken: string;
|
|
4
|
+
refreshToken: string;
|
|
5
|
+
/** Optional; when omitted it is derived from the access token's `exp` claim. */
|
|
6
|
+
accessTokenExpiresAt?: string;
|
|
7
|
+
refreshTokenExpiresAt?: string;
|
|
8
|
+
/**
|
|
9
|
+
* RelayAuth base URL (e.g. `https://api.relayauth.dev`). When omitted it is
|
|
10
|
+
* derived from the refresh token's `iss` claim, mapping the issuer host to its
|
|
11
|
+
* API host (`relayauth.dev` -> `api.relayauth.dev`).
|
|
12
|
+
*/
|
|
13
|
+
relayauthUrl?: string;
|
|
14
|
+
}
|
|
15
|
+
export interface RelayauthPathTokenSetupOptions {
|
|
16
|
+
requestTimeoutMs?: number;
|
|
17
|
+
refreshWindowMs?: number;
|
|
18
|
+
onTokens?: (tokens: RelayauthPathTokenSet) => void | Promise<void>;
|
|
19
|
+
}
|
|
20
|
+
/** True when `refreshToken` is a RelayAuth-issued refresh token (relay_pa pair). */
|
|
21
|
+
export declare function isRelayauthRefreshToken(refreshToken: string): boolean;
|
|
22
|
+
export declare function createRelayauthPathTokenAccessTokenProvider(initialTokens: RelayauthPathTokenSet, options?: RelayauthPathTokenSetupOptions): AccessTokenProvider;
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { CloudApiError, CloudTimeoutError, MalformedCloudResponseError, RelayfileSetupError } from "./setup-errors.js";
|
|
2
|
+
import { RELAYFILE_SDK_VERSION } from "./version.js";
|
|
3
|
+
// A `relay_pa` (RelayAuth path-token) access+refresh pair is issued and rotated
|
|
4
|
+
// by RelayAuth, NOT by the Cloud device-auth flow. Its refresh token carries
|
|
5
|
+
// `aud: ["relayauth"]` and scope `relayauth:token:refresh`, and it is exchanged
|
|
6
|
+
// at `<relayauth>/v1/tokens/refresh` — a DIFFERENT endpoint from Cloud's
|
|
7
|
+
// `api/v1/auth/token/refresh`. Sending such a refresh token to the Cloud endpoint
|
|
8
|
+
// returns `invalid_grant`, which is the failure mode this provider fixes:
|
|
9
|
+
// `createRelayfileCloudAccessTokenProvider` unconditionally targets Cloud, so a
|
|
10
|
+
// relay_pa credential (e.g. `RELAYFILE_ACCESS_TOKEN` + `RELAYFILE_REFRESH_TOKEN`
|
|
11
|
+
// in an agent .env) never rotates through it. Conveniently, RelayAuth's refresh
|
|
12
|
+
// response has the SAME shape Cloud returns
|
|
13
|
+
// (`accessToken`/`refreshToken`/`accessTokenExpiresAt`/`refreshTokenExpiresAt`),
|
|
14
|
+
// so only the endpoint differs.
|
|
15
|
+
const DEFAULT_REQUEST_TIMEOUT_MS = 30_000;
|
|
16
|
+
const DEFAULT_REFRESH_WINDOW_MS = 60_000;
|
|
17
|
+
/** True when `refreshToken` is a RelayAuth-issued refresh token (relay_pa pair). */
|
|
18
|
+
export function isRelayauthRefreshToken(refreshToken) {
|
|
19
|
+
const claims = decodeJwtClaims(refreshToken);
|
|
20
|
+
if (!claims)
|
|
21
|
+
return false;
|
|
22
|
+
const aud = claims.aud;
|
|
23
|
+
const audiences = Array.isArray(aud) ? aud : typeof aud === "string" ? [aud] : [];
|
|
24
|
+
if (audiences.includes("relayauth"))
|
|
25
|
+
return true;
|
|
26
|
+
const scopes = Array.isArray(claims.scopes) ? claims.scopes : [];
|
|
27
|
+
return scopes.includes("relayauth:token:refresh");
|
|
28
|
+
}
|
|
29
|
+
export function createRelayauthPathTokenAccessTokenProvider(initialTokens, options = {}) {
|
|
30
|
+
const requestTimeoutMs = Math.max(1, Math.floor(options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS));
|
|
31
|
+
const refreshWindowMs = Math.max(0, Math.floor(options.refreshWindowMs ?? DEFAULT_REFRESH_WINDOW_MS));
|
|
32
|
+
const refreshUrl = buildRefreshUrl(initialTokens);
|
|
33
|
+
let tokens = withDerivedAccessExpiry(initialTokens);
|
|
34
|
+
let refreshPromise;
|
|
35
|
+
// A rotated token set whose `onTokens` persistence callback has not yet
|
|
36
|
+
// succeeded. Until it does, a consumed refresh token may still live in the
|
|
37
|
+
// caller's persisted copy (→ `invalid_grant` after restart), so we retry the
|
|
38
|
+
// callback on every subsequent use rather than treating rotation as complete.
|
|
39
|
+
let pendingPersist;
|
|
40
|
+
return async () => {
|
|
41
|
+
if (shouldRefresh(tokens, refreshWindowMs)) {
|
|
42
|
+
if (!refreshPromise) {
|
|
43
|
+
refreshPromise = refresh();
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
await refreshPromise;
|
|
47
|
+
}
|
|
48
|
+
finally {
|
|
49
|
+
refreshPromise = undefined;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
else if (pendingPersist) {
|
|
53
|
+
// No refresh needed, but a prior rotation still owes a successful persist.
|
|
54
|
+
await persistPendingTokens();
|
|
55
|
+
}
|
|
56
|
+
return tokens.accessToken;
|
|
57
|
+
};
|
|
58
|
+
async function refresh() {
|
|
59
|
+
const { response, payload } = await fetchJsonWithTimeout(refreshUrl, {
|
|
60
|
+
method: "POST",
|
|
61
|
+
headers: {
|
|
62
|
+
"Content-Type": "application/json",
|
|
63
|
+
"X-Relayfile-SDK-Version": RELAYFILE_SDK_VERSION
|
|
64
|
+
},
|
|
65
|
+
body: JSON.stringify({ refreshToken: tokens.refreshToken })
|
|
66
|
+
}, requestTimeoutMs);
|
|
67
|
+
if (!response.ok) {
|
|
68
|
+
throw new CloudApiError(response.status, payload);
|
|
69
|
+
}
|
|
70
|
+
tokens = withDerivedAccessExpiry({
|
|
71
|
+
relayauthUrl: tokens.relayauthUrl,
|
|
72
|
+
accessToken: requireStringField(payload, "accessToken"),
|
|
73
|
+
refreshToken: requireStringField(payload, "refreshToken"),
|
|
74
|
+
accessTokenExpiresAt: readOptionalStringField(payload, "accessTokenExpiresAt"),
|
|
75
|
+
refreshTokenExpiresAt: readOptionalStringField(payload, "refreshTokenExpiresAt")
|
|
76
|
+
});
|
|
77
|
+
pendingPersist = { ...tokens };
|
|
78
|
+
await persistPendingTokens();
|
|
79
|
+
}
|
|
80
|
+
// Best-effort, retried persistence. A callback failure never rejects the
|
|
81
|
+
// provider — the in-memory access token is valid and usable — but the set
|
|
82
|
+
// stays pending so the next provider() call re-attempts, so a durable copy
|
|
83
|
+
// eventually catches up with the rotated refresh token.
|
|
84
|
+
async function persistPendingTokens() {
|
|
85
|
+
if (!pendingPersist) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (!options.onTokens) {
|
|
89
|
+
pendingPersist = undefined;
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const toPersist = pendingPersist;
|
|
93
|
+
try {
|
|
94
|
+
await options.onTokens({ ...toPersist });
|
|
95
|
+
// Only clear if nothing rotated again while the callback was in flight.
|
|
96
|
+
if (pendingPersist === toPersist) {
|
|
97
|
+
pendingPersist = undefined;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
// Keep pending; retried on the next use.
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function buildRefreshUrl(tokens) {
|
|
106
|
+
const explicit = normalizeNonEmptyString(tokens.relayauthUrl);
|
|
107
|
+
const base = explicit ?? deriveRelayauthApiBase(tokens.refreshToken);
|
|
108
|
+
if (!base) {
|
|
109
|
+
// NEVER put the refresh token in the error — it is a live bearer credential.
|
|
110
|
+
throw new RelayfileSetupError("Cannot determine the RelayAuth refresh endpoint: pass `relayauthUrl`, or use a refresh token that carries an `iss` claim.", "relayauth_url_unresolved");
|
|
111
|
+
}
|
|
112
|
+
let url;
|
|
113
|
+
try {
|
|
114
|
+
url = new URL(base);
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
throw new RelayfileSetupError(`Invalid RelayAuth URL: ${base}`, "relayauth_url_invalid");
|
|
118
|
+
}
|
|
119
|
+
// The refresh token is transmitted in the request body, so the endpoint must
|
|
120
|
+
// be HTTPS. A loopback host is allowed for local self-host development.
|
|
121
|
+
if (url.protocol !== "https:" && !isLoopbackHost(url.hostname)) {
|
|
122
|
+
throw new RelayfileSetupError(`RelayAuth refresh endpoint must use HTTPS (got ${url.protocol}//${url.host}).`, "relayauth_url_insecure");
|
|
123
|
+
}
|
|
124
|
+
if (!url.pathname.endsWith("/")) {
|
|
125
|
+
url.pathname = `${url.pathname}/`;
|
|
126
|
+
}
|
|
127
|
+
return new URL("v1/tokens/refresh", url).toString();
|
|
128
|
+
}
|
|
129
|
+
function isLoopbackHost(hostname) {
|
|
130
|
+
return (hostname === "localhost" ||
|
|
131
|
+
hostname === "127.0.0.1" ||
|
|
132
|
+
hostname === "::1" ||
|
|
133
|
+
hostname === "[::1]");
|
|
134
|
+
}
|
|
135
|
+
// Derive the RelayAuth API base from the refresh token's `iss`. The issuer is the
|
|
136
|
+
// public host (`https://relayauth.dev`); token exchange lives on the API host
|
|
137
|
+
// (`https://api.relayauth.dev`). A host that already begins with `api.` is left
|
|
138
|
+
// as-is so self-hosted issuers work unchanged.
|
|
139
|
+
function deriveRelayauthApiBase(refreshToken) {
|
|
140
|
+
const claims = decodeJwtClaims(refreshToken);
|
|
141
|
+
const iss = typeof claims?.iss === "string" ? claims.iss : undefined;
|
|
142
|
+
if (!iss)
|
|
143
|
+
return undefined;
|
|
144
|
+
try {
|
|
145
|
+
const url = new URL(iss);
|
|
146
|
+
if (!url.hostname.startsWith("api.")) {
|
|
147
|
+
url.hostname = `api.${url.hostname}`;
|
|
148
|
+
}
|
|
149
|
+
// `url.host` preserves a non-default port; `url.hostname` would drop it.
|
|
150
|
+
return `${url.protocol}//${url.host}`;
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
return undefined;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
function withDerivedAccessExpiry(tokens) {
|
|
157
|
+
const explicit = normalizeNonEmptyString(tokens.accessTokenExpiresAt);
|
|
158
|
+
if (explicit && !Number.isNaN(Date.parse(explicit))) {
|
|
159
|
+
return { ...tokens, accessTokenExpiresAt: explicit };
|
|
160
|
+
}
|
|
161
|
+
const exp = decodeJwtClaims(tokens.accessToken)?.exp;
|
|
162
|
+
const iso = typeof exp === "number" && Number.isFinite(exp)
|
|
163
|
+
? new Date(exp * 1000).toISOString()
|
|
164
|
+
: new Date(0).toISOString(); // unknown -> force refresh on first use
|
|
165
|
+
return { ...tokens, accessTokenExpiresAt: iso };
|
|
166
|
+
}
|
|
167
|
+
function shouldRefresh(tokens, refreshWindowMs) {
|
|
168
|
+
const expiresAt = Date.parse(tokens.accessTokenExpiresAt);
|
|
169
|
+
if (Number.isNaN(expiresAt)) {
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
return expiresAt - Date.now() <= refreshWindowMs;
|
|
173
|
+
}
|
|
174
|
+
function decodeJwtClaims(token) {
|
|
175
|
+
try {
|
|
176
|
+
const compact = token.replace(/^relay_[a-z]+_/, "");
|
|
177
|
+
const payload = compact.split(".")[1];
|
|
178
|
+
if (!payload)
|
|
179
|
+
return undefined;
|
|
180
|
+
const json = Buffer.from(payload, "base64url").toString("utf8");
|
|
181
|
+
return JSON.parse(json);
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
return undefined;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
// Runs the fetch AND the response-body read under one timeout, so a server that
|
|
188
|
+
// sends headers then stalls the body still trips `requestTimeoutMs` instead of
|
|
189
|
+
// hanging the shared refresh (and every caller awaiting it) indefinitely.
|
|
190
|
+
async function fetchJsonWithTimeout(url, init, timeoutMs) {
|
|
191
|
+
const controller = new AbortController();
|
|
192
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
193
|
+
try {
|
|
194
|
+
const response = await fetch(url, { ...init, signal: controller.signal });
|
|
195
|
+
const payload = await readResponseBody(response);
|
|
196
|
+
return { response, payload };
|
|
197
|
+
}
|
|
198
|
+
catch (error) {
|
|
199
|
+
if (controller.signal.aborted) {
|
|
200
|
+
throw new CloudTimeoutError("refreshRelayauthAccessToken", timeoutMs);
|
|
201
|
+
}
|
|
202
|
+
throw error;
|
|
203
|
+
}
|
|
204
|
+
finally {
|
|
205
|
+
clearTimeout(timer);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
async function readResponseBody(response) {
|
|
209
|
+
const text = await response.text();
|
|
210
|
+
if (text === "") {
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
214
|
+
if (contentType.includes("application/json")) {
|
|
215
|
+
try {
|
|
216
|
+
return JSON.parse(text);
|
|
217
|
+
}
|
|
218
|
+
catch {
|
|
219
|
+
return text;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return text;
|
|
223
|
+
}
|
|
224
|
+
function requireStringField(payload, field) {
|
|
225
|
+
const value = readField(payload, field);
|
|
226
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
227
|
+
throw new MalformedCloudResponseError(field, payload);
|
|
228
|
+
}
|
|
229
|
+
return value;
|
|
230
|
+
}
|
|
231
|
+
function readOptionalStringField(payload, field) {
|
|
232
|
+
const value = readField(payload, field);
|
|
233
|
+
if (value === undefined) {
|
|
234
|
+
return undefined;
|
|
235
|
+
}
|
|
236
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
237
|
+
throw new MalformedCloudResponseError(field, payload);
|
|
238
|
+
}
|
|
239
|
+
return value;
|
|
240
|
+
}
|
|
241
|
+
function readField(payload, field) {
|
|
242
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
|
|
243
|
+
return undefined;
|
|
244
|
+
}
|
|
245
|
+
return payload[field];
|
|
246
|
+
}
|
|
247
|
+
function normalizeNonEmptyString(value) {
|
|
248
|
+
const normalized = value?.trim();
|
|
249
|
+
return normalized ? normalized : undefined;
|
|
250
|
+
}
|
package/dist/setup.d.ts
CHANGED
|
@@ -37,6 +37,16 @@ interface WorkspaceHandleOptions {
|
|
|
37
37
|
token: string;
|
|
38
38
|
joinOptions: NormalizedJoinWorkspaceOptions;
|
|
39
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* Build the rotating access-token provider for a cloud token set, auto-routing
|
|
42
|
+
* by the refresh token's issuer: a RelayAuth `relay_pa` pair (e.g.
|
|
43
|
+
* RELAYFILE_ACCESS_TOKEN + RELAYFILE_REFRESH_TOKEN) rotates at RelayAuth, while a
|
|
44
|
+
* Cloud device-auth pair rotates at Cloud. Routing a relay_pa token through the
|
|
45
|
+
* Cloud endpoint returns `invalid_grant`, so this picks the right provider once
|
|
46
|
+
* and is shared by every `fromCloudTokens` (base and CLI subclass) so no entry
|
|
47
|
+
* point can silently skip the routing.
|
|
48
|
+
*/
|
|
49
|
+
export declare function resolveCloudTokensAccessToken(tokens: RelayfileCloudTokenSet, options: RelayfileCloudTokenSetupOptions, cloudApiUrl: string): AccessTokenProvider;
|
|
40
50
|
export declare class RelayfileSetup {
|
|
41
51
|
private readonly cloudApiUrl;
|
|
42
52
|
private readonly accessToken?;
|
package/dist/setup.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { RelayFileClient } from "./client.js";
|
|
2
2
|
import { createRelayfileCloudAccessTokenProvider } from "./cloud-token-provider.js";
|
|
3
|
+
import { createRelayauthPathTokenAccessTokenProvider, isRelayauthRefreshToken } from "./relayauth-token-provider.js";
|
|
3
4
|
import { CloudAbortError, CloudApiError, CloudTimeoutError, InvalidLocalDirError, InvalidMountModeError, InvalidRemotePathError, IntegrationConnectionTimeoutError, MalformedCloudResponseError, MissingConnectionIdError, MountSessionInputError, ProviderNotConnectedError, ProviderNotReadyError, RelayfileSetupError, UnknownProviderError } from "./setup-errors.js";
|
|
4
5
|
import { WORKSPACE_INTEGRATION_PROVIDERS } from "./setup-types.js";
|
|
5
6
|
import { RELAYFILE_SDK_VERSION } from "./version.js";
|
|
@@ -32,6 +33,38 @@ const nodeOnlyMountLauncher = {
|
|
|
32
33
|
throw new RelayfileSetupError("The default relayfile-mount launcher is only available from @relayfile/sdk/cli. Import RelayfileSetup from @relayfile/sdk/cli or pass a custom launcher to mountWorkspace().", "node_only_sdk_feature");
|
|
33
34
|
}
|
|
34
35
|
};
|
|
36
|
+
/**
|
|
37
|
+
* Build the rotating access-token provider for a cloud token set, auto-routing
|
|
38
|
+
* by the refresh token's issuer: a RelayAuth `relay_pa` pair (e.g.
|
|
39
|
+
* RELAYFILE_ACCESS_TOKEN + RELAYFILE_REFRESH_TOKEN) rotates at RelayAuth, while a
|
|
40
|
+
* Cloud device-auth pair rotates at Cloud. Routing a relay_pa token through the
|
|
41
|
+
* Cloud endpoint returns `invalid_grant`, so this picks the right provider once
|
|
42
|
+
* and is shared by every `fromCloudTokens` (base and CLI subclass) so no entry
|
|
43
|
+
* point can silently skip the routing.
|
|
44
|
+
*/
|
|
45
|
+
export function resolveCloudTokensAccessToken(tokens, options, cloudApiUrl) {
|
|
46
|
+
if (!isRelayauthRefreshToken(tokens.refreshToken)) {
|
|
47
|
+
return createRelayfileCloudAccessTokenProvider({ ...tokens, apiUrl: tokens.apiUrl ?? cloudApiUrl }, { ...options, cloudApiUrl });
|
|
48
|
+
}
|
|
49
|
+
return createRelayauthPathTokenAccessTokenProvider({
|
|
50
|
+
accessToken: tokens.accessToken,
|
|
51
|
+
refreshToken: tokens.refreshToken,
|
|
52
|
+
accessTokenExpiresAt: tokens.accessTokenExpiresAt,
|
|
53
|
+
refreshTokenExpiresAt: tokens.refreshTokenExpiresAt
|
|
54
|
+
}, {
|
|
55
|
+
requestTimeoutMs: options.requestTimeoutMs,
|
|
56
|
+
refreshWindowMs: options.refreshWindowMs,
|
|
57
|
+
onTokens: options.onTokens
|
|
58
|
+
? (rotated) => options.onTokens({
|
|
59
|
+
apiUrl: tokens.apiUrl ?? cloudApiUrl,
|
|
60
|
+
accessToken: rotated.accessToken,
|
|
61
|
+
refreshToken: rotated.refreshToken,
|
|
62
|
+
accessTokenExpiresAt: rotated.accessTokenExpiresAt ?? "",
|
|
63
|
+
refreshTokenExpiresAt: rotated.refreshTokenExpiresAt
|
|
64
|
+
})
|
|
65
|
+
: undefined
|
|
66
|
+
});
|
|
67
|
+
}
|
|
35
68
|
export class RelayfileSetup {
|
|
36
69
|
cloudApiUrl;
|
|
37
70
|
accessToken;
|
|
@@ -47,13 +80,7 @@ export class RelayfileSetup {
|
|
|
47
80
|
return new RelayfileSetup({
|
|
48
81
|
...options,
|
|
49
82
|
cloudApiUrl,
|
|
50
|
-
accessToken:
|
|
51
|
-
...tokens,
|
|
52
|
-
apiUrl: tokens.apiUrl ?? cloudApiUrl
|
|
53
|
-
}, {
|
|
54
|
-
...options,
|
|
55
|
-
cloudApiUrl
|
|
56
|
-
})
|
|
83
|
+
accessToken: resolveCloudTokensAccessToken(tokens, options, cloudApiUrl)
|
|
57
84
|
});
|
|
58
85
|
}
|
|
59
86
|
constructor(options = {}) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@relayfile/sdk",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.61",
|
|
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.
|
|
63
|
+
"@relayfile/core": "0.10.61",
|
|
64
64
|
"ignore": "^7.0.5",
|
|
65
65
|
"tar": "^7.5.10"
|
|
66
66
|
},
|
|
67
67
|
"optionalDependencies": {
|
|
68
|
-
"@relayfile/mount-darwin-arm64": "0.10.
|
|
69
|
-
"@relayfile/mount-darwin-x64": "0.10.
|
|
70
|
-
"@relayfile/mount-linux-arm64": "0.10.
|
|
71
|
-
"@relayfile/mount-linux-x64": "0.10.
|
|
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"
|
|
72
72
|
},
|
|
73
73
|
"devDependencies": {
|
|
74
74
|
"typescript": "^5.7.3",
|