@granular-software/sdk 0.3.4 → 0.4.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/README.md +20 -0
- package/dist/adapters/anthropic.d.mts +1 -1
- package/dist/adapters/anthropic.d.ts +1 -1
- package/dist/adapters/langchain.d.mts +1 -1
- package/dist/adapters/langchain.d.ts +1 -1
- package/dist/adapters/mastra.d.mts +1 -1
- package/dist/adapters/mastra.d.ts +1 -1
- package/dist/adapters/openai.d.mts +1 -1
- package/dist/adapters/openai.d.ts +1 -1
- package/dist/cli/index.js +80 -6
- package/dist/index.d.mts +19 -2
- package/dist/index.d.ts +19 -2
- package/dist/index.js +215 -5
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +215 -5
- package/dist/index.mjs.map +1 -1
- package/dist/{types-D5B8WlF4.d.mts → types-BOPsFZYi.d.mts} +8 -1
- package/dist/{types-D5B8WlF4.d.ts → types-BOPsFZYi.d.ts} +8 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -3917,6 +3917,9 @@ if (typeof globalThis !== "undefined" && globalThis.WebSocket) {
|
|
|
3917
3917
|
GlobalWebSocket = globalThis.WebSocket;
|
|
3918
3918
|
}
|
|
3919
3919
|
var READY_STATE_OPEN = 1;
|
|
3920
|
+
var TOKEN_REFRESH_LEEWAY_MS = 2 * 60 * 1e3;
|
|
3921
|
+
var TOKEN_REFRESH_RETRY_MS = 30 * 1e3;
|
|
3922
|
+
var MAX_TIMER_DELAY_MS = 2147483647;
|
|
3920
3923
|
var WSClient = class {
|
|
3921
3924
|
ws = null;
|
|
3922
3925
|
url;
|
|
@@ -3930,6 +3933,7 @@ var WSClient = class {
|
|
|
3930
3933
|
doc = Automerge.init();
|
|
3931
3934
|
syncState = Automerge.initSyncState();
|
|
3932
3935
|
reconnectTimer = null;
|
|
3936
|
+
tokenRefreshTimer = null;
|
|
3933
3937
|
isExplicitlyDisconnected = false;
|
|
3934
3938
|
options;
|
|
3935
3939
|
constructor(options) {
|
|
@@ -3938,12 +3942,109 @@ var WSClient = class {
|
|
|
3938
3942
|
this.sessionId = options.sessionId;
|
|
3939
3943
|
this.token = options.token;
|
|
3940
3944
|
}
|
|
3945
|
+
clearTokenRefreshTimer() {
|
|
3946
|
+
if (this.tokenRefreshTimer) {
|
|
3947
|
+
clearTimeout(this.tokenRefreshTimer);
|
|
3948
|
+
this.tokenRefreshTimer = null;
|
|
3949
|
+
}
|
|
3950
|
+
}
|
|
3951
|
+
decodeBase64Url(payload) {
|
|
3952
|
+
const base64 = payload.replace(/-/g, "+").replace(/_/g, "/");
|
|
3953
|
+
const padded = base64 + "=".repeat((4 - (base64.length % 4 || 4)) % 4);
|
|
3954
|
+
if (typeof atob === "function") {
|
|
3955
|
+
return atob(padded);
|
|
3956
|
+
}
|
|
3957
|
+
const maybeBuffer = globalThis.Buffer;
|
|
3958
|
+
if (maybeBuffer) {
|
|
3959
|
+
return maybeBuffer.from(padded, "base64").toString("utf8");
|
|
3960
|
+
}
|
|
3961
|
+
throw new Error("No base64 decoder available");
|
|
3962
|
+
}
|
|
3963
|
+
getTokenExpiryMs(token) {
|
|
3964
|
+
const parts = token.split(".");
|
|
3965
|
+
if (parts.length < 2) {
|
|
3966
|
+
return null;
|
|
3967
|
+
}
|
|
3968
|
+
try {
|
|
3969
|
+
const payloadRaw = this.decodeBase64Url(parts[1]);
|
|
3970
|
+
const payload = JSON.parse(payloadRaw);
|
|
3971
|
+
if (typeof payload.exp !== "number" || !Number.isFinite(payload.exp)) {
|
|
3972
|
+
return null;
|
|
3973
|
+
}
|
|
3974
|
+
return payload.exp * 1e3;
|
|
3975
|
+
} catch {
|
|
3976
|
+
return null;
|
|
3977
|
+
}
|
|
3978
|
+
}
|
|
3979
|
+
scheduleTokenRefresh() {
|
|
3980
|
+
this.clearTokenRefreshTimer();
|
|
3981
|
+
if (!this.options.tokenProvider || this.isExplicitlyDisconnected) {
|
|
3982
|
+
return;
|
|
3983
|
+
}
|
|
3984
|
+
const expiresAt = this.getTokenExpiryMs(this.token);
|
|
3985
|
+
if (!expiresAt) {
|
|
3986
|
+
return;
|
|
3987
|
+
}
|
|
3988
|
+
const refreshInMs = Math.max(1e3, expiresAt - Date.now() - TOKEN_REFRESH_LEEWAY_MS);
|
|
3989
|
+
const delay = Math.min(refreshInMs, MAX_TIMER_DELAY_MS);
|
|
3990
|
+
this.tokenRefreshTimer = setTimeout(() => {
|
|
3991
|
+
void this.refreshTokenInBackground();
|
|
3992
|
+
}, delay);
|
|
3993
|
+
}
|
|
3994
|
+
async refreshTokenInBackground() {
|
|
3995
|
+
if (!this.options.tokenProvider || this.isExplicitlyDisconnected) {
|
|
3996
|
+
return;
|
|
3997
|
+
}
|
|
3998
|
+
try {
|
|
3999
|
+
const refreshedToken = await this.options.tokenProvider();
|
|
4000
|
+
if (typeof refreshedToken !== "string" || refreshedToken.length === 0) {
|
|
4001
|
+
throw new Error("Token provider returned no token");
|
|
4002
|
+
}
|
|
4003
|
+
this.token = refreshedToken;
|
|
4004
|
+
this.scheduleTokenRefresh();
|
|
4005
|
+
} catch (error) {
|
|
4006
|
+
if (this.isExplicitlyDisconnected) {
|
|
4007
|
+
return;
|
|
4008
|
+
}
|
|
4009
|
+
console.warn("[Granular] Token refresh failed, retrying soon:", error);
|
|
4010
|
+
this.clearTokenRefreshTimer();
|
|
4011
|
+
this.tokenRefreshTimer = setTimeout(() => {
|
|
4012
|
+
void this.refreshTokenInBackground();
|
|
4013
|
+
}, TOKEN_REFRESH_RETRY_MS);
|
|
4014
|
+
}
|
|
4015
|
+
}
|
|
4016
|
+
async resolveTokenForConnect() {
|
|
4017
|
+
if (!this.options.tokenProvider) {
|
|
4018
|
+
return this.token;
|
|
4019
|
+
}
|
|
4020
|
+
const expiresAt = this.getTokenExpiryMs(this.token);
|
|
4021
|
+
const shouldRefresh = expiresAt !== null && expiresAt - Date.now() <= TOKEN_REFRESH_LEEWAY_MS;
|
|
4022
|
+
if (!shouldRefresh) {
|
|
4023
|
+
return this.token;
|
|
4024
|
+
}
|
|
4025
|
+
try {
|
|
4026
|
+
const refreshedToken = await this.options.tokenProvider();
|
|
4027
|
+
if (typeof refreshedToken !== "string" || refreshedToken.length === 0) {
|
|
4028
|
+
throw new Error("Token provider returned no token");
|
|
4029
|
+
}
|
|
4030
|
+
this.token = refreshedToken;
|
|
4031
|
+
return refreshedToken;
|
|
4032
|
+
} catch (error) {
|
|
4033
|
+
if (expiresAt > Date.now()) {
|
|
4034
|
+
console.warn("[Granular] Token refresh failed, using current token:", error);
|
|
4035
|
+
return this.token;
|
|
4036
|
+
}
|
|
4037
|
+
throw error;
|
|
4038
|
+
}
|
|
4039
|
+
}
|
|
3941
4040
|
/**
|
|
3942
4041
|
* Connect to the WebSocket server
|
|
3943
4042
|
* @returns {Promise<void>} Resolves when connection is open
|
|
3944
4043
|
*/
|
|
3945
4044
|
async connect() {
|
|
4045
|
+
const token = await this.resolveTokenForConnect();
|
|
3946
4046
|
this.isExplicitlyDisconnected = false;
|
|
4047
|
+
this.scheduleTokenRefresh();
|
|
3947
4048
|
if (this.reconnectTimer) {
|
|
3948
4049
|
clearTimeout(this.reconnectTimer);
|
|
3949
4050
|
this.reconnectTimer = null;
|
|
@@ -3963,7 +4064,7 @@ var WSClient = class {
|
|
|
3963
4064
|
try {
|
|
3964
4065
|
const wsUrl = new URL(this.url);
|
|
3965
4066
|
wsUrl.searchParams.set("sessionId", this.sessionId);
|
|
3966
|
-
wsUrl.searchParams.set("token",
|
|
4067
|
+
wsUrl.searchParams.set("token", token);
|
|
3967
4068
|
this.ws = new WebSocketClass(wsUrl.toString());
|
|
3968
4069
|
if (!this.ws) throw new Error("Failed to create WebSocket");
|
|
3969
4070
|
const socket = this.ws;
|
|
@@ -4168,10 +4269,10 @@ var WSClient = class {
|
|
|
4168
4269
|
const snapshotMessage = message;
|
|
4169
4270
|
try {
|
|
4170
4271
|
const bytes = new Uint8Array(snapshotMessage.data);
|
|
4171
|
-
console.log("[Granular DEBUG] Loading snapshot bytes:", bytes.length);
|
|
4272
|
+
console.log("[Granular DEBUG] Loading Automerge session snapshot bytes:", bytes.length);
|
|
4172
4273
|
this.doc = Automerge.load(bytes);
|
|
4173
4274
|
this.emit("sync", this.doc);
|
|
4174
|
-
console.log("[Granular DEBUG]
|
|
4275
|
+
console.log("[Granular DEBUG] Automerge session snapshot loaded. Doc:", JSON.stringify(Automerge.toJS(this.doc)));
|
|
4175
4276
|
} catch (e) {
|
|
4176
4277
|
console.warn("[Granular] Failed to load snapshot message", e);
|
|
4177
4278
|
}
|
|
@@ -4334,6 +4435,7 @@ var WSClient = class {
|
|
|
4334
4435
|
clearTimeout(this.reconnectTimer);
|
|
4335
4436
|
this.reconnectTimer = null;
|
|
4336
4437
|
}
|
|
4438
|
+
this.clearTokenRefreshTimer();
|
|
4337
4439
|
if (this.ws) {
|
|
4338
4440
|
this.ws.close(1e3, "Client disconnect");
|
|
4339
4441
|
this.ws = null;
|
|
@@ -4820,6 +4922,13 @@ import { ${allImports} } from "./sandbox-tools";
|
|
|
4820
4922
|
* Close the session and disconnect from the sandbox
|
|
4821
4923
|
*/
|
|
4822
4924
|
async disconnect() {
|
|
4925
|
+
try {
|
|
4926
|
+
await this.client.call("client.goodbye", {
|
|
4927
|
+
clientId: this.clientId,
|
|
4928
|
+
timestamp: Date.now()
|
|
4929
|
+
});
|
|
4930
|
+
} catch (error) {
|
|
4931
|
+
}
|
|
4823
4932
|
this.client.disconnect();
|
|
4824
4933
|
}
|
|
4825
4934
|
// --- Event Handling ---
|
|
@@ -5030,6 +5139,50 @@ var JobImplementation = class {
|
|
|
5030
5139
|
}
|
|
5031
5140
|
};
|
|
5032
5141
|
|
|
5142
|
+
// src/endpoints.ts
|
|
5143
|
+
var LOCAL_API_URL = "ws://localhost:8787/granular";
|
|
5144
|
+
var PRODUCTION_API_URL = "wss://api.granular.dev/v2/ws";
|
|
5145
|
+
function readEnv(name) {
|
|
5146
|
+
if (typeof process === "undefined" || !process.env) return void 0;
|
|
5147
|
+
return process.env[name];
|
|
5148
|
+
}
|
|
5149
|
+
function normalizeMode(value) {
|
|
5150
|
+
if (!value) return void 0;
|
|
5151
|
+
const normalized = value.trim().toLowerCase();
|
|
5152
|
+
if (normalized === "local") return "local";
|
|
5153
|
+
if (normalized === "prod" || normalized === "production") return "production";
|
|
5154
|
+
if (normalized === "auto") return "auto";
|
|
5155
|
+
return void 0;
|
|
5156
|
+
}
|
|
5157
|
+
function isTruthy(value) {
|
|
5158
|
+
if (!value) return false;
|
|
5159
|
+
const normalized = value.trim().toLowerCase();
|
|
5160
|
+
return normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on";
|
|
5161
|
+
}
|
|
5162
|
+
function resolveEndpointMode(explicitMode) {
|
|
5163
|
+
const explicit = normalizeMode(explicitMode);
|
|
5164
|
+
if (explicit === "local" || explicit === "production") {
|
|
5165
|
+
return explicit;
|
|
5166
|
+
}
|
|
5167
|
+
const envMode = normalizeMode(readEnv("GRANULAR_ENDPOINT_MODE") || readEnv("GRANULAR_ENV"));
|
|
5168
|
+
if (envMode === "local" || envMode === "production") {
|
|
5169
|
+
return envMode;
|
|
5170
|
+
}
|
|
5171
|
+
if (isTruthy(readEnv("GRANULAR_USE_LOCAL_ENDPOINTS")) || isTruthy(readEnv("GRANULAR_LOCAL"))) {
|
|
5172
|
+
return "local";
|
|
5173
|
+
}
|
|
5174
|
+
if (isTruthy(readEnv("GRANULAR_USE_PRODUCTION_ENDPOINTS")) || isTruthy(readEnv("GRANULAR_PROD"))) {
|
|
5175
|
+
return "production";
|
|
5176
|
+
}
|
|
5177
|
+
return readEnv("NODE_ENV") === "development" ? "local" : "production";
|
|
5178
|
+
}
|
|
5179
|
+
function resolveApiUrl(explicitApiUrl, mode) {
|
|
5180
|
+
if (explicitApiUrl) {
|
|
5181
|
+
return explicitApiUrl;
|
|
5182
|
+
}
|
|
5183
|
+
return resolveEndpointMode(mode) === "local" ? LOCAL_API_URL : PRODUCTION_API_URL;
|
|
5184
|
+
}
|
|
5185
|
+
|
|
5033
5186
|
// src/client.ts
|
|
5034
5187
|
var STANDARD_MODULES_OPERATIONS = [
|
|
5035
5188
|
{ create: "entity", has: { id: { value: "auto-generated" }, createdAt: { value: void 0 } } },
|
|
@@ -5074,6 +5227,60 @@ var Environment = class _Environment extends Session {
|
|
|
5074
5227
|
get apiEndpoint() {
|
|
5075
5228
|
return this._apiEndpoint;
|
|
5076
5229
|
}
|
|
5230
|
+
getRuntimeBaseUrl() {
|
|
5231
|
+
try {
|
|
5232
|
+
const endpoint = new URL(this._apiEndpoint);
|
|
5233
|
+
const graphqlSuffix = "/orchestrator/graphql";
|
|
5234
|
+
if (endpoint.pathname.endsWith(graphqlSuffix)) {
|
|
5235
|
+
endpoint.pathname = endpoint.pathname.slice(0, -graphqlSuffix.length);
|
|
5236
|
+
} else if (endpoint.pathname.endsWith("/graphql")) {
|
|
5237
|
+
endpoint.pathname = endpoint.pathname.slice(0, -"/graphql".length);
|
|
5238
|
+
}
|
|
5239
|
+
endpoint.search = "";
|
|
5240
|
+
endpoint.hash = "";
|
|
5241
|
+
return endpoint.toString().replace(/\/$/, "");
|
|
5242
|
+
} catch {
|
|
5243
|
+
return this._apiEndpoint.replace(/\/orchestrator\/graphql$/, "").replace(/\/$/, "");
|
|
5244
|
+
}
|
|
5245
|
+
}
|
|
5246
|
+
/**
|
|
5247
|
+
* Close the session and disconnect from the sandbox.
|
|
5248
|
+
*
|
|
5249
|
+
* Sends `client.goodbye` over WebSocket first, then issues an HTTP fallback
|
|
5250
|
+
* to the runtime goodbye endpoint if no definitive WS-side runtime notify
|
|
5251
|
+
* acknowledgement was observed.
|
|
5252
|
+
*/
|
|
5253
|
+
async disconnect() {
|
|
5254
|
+
let wsNotifiedRuntime = false;
|
|
5255
|
+
try {
|
|
5256
|
+
const goodbye = await this.rpc("client.goodbye", {
|
|
5257
|
+
timestamp: Date.now()
|
|
5258
|
+
});
|
|
5259
|
+
wsNotifiedRuntime = Boolean(goodbye?.ok && goodbye?.via);
|
|
5260
|
+
} catch {
|
|
5261
|
+
wsNotifiedRuntime = false;
|
|
5262
|
+
}
|
|
5263
|
+
if (!wsNotifiedRuntime) {
|
|
5264
|
+
try {
|
|
5265
|
+
const runtimeBase = this.getRuntimeBaseUrl();
|
|
5266
|
+
await fetch(
|
|
5267
|
+
`${runtimeBase}/orchestrator/runtime/environments/${this.environmentId}/session-goodbye`,
|
|
5268
|
+
{
|
|
5269
|
+
method: "POST",
|
|
5270
|
+
headers: {
|
|
5271
|
+
"Content-Type": "application/json",
|
|
5272
|
+
"Authorization": `Bearer ${this._apiKey}`
|
|
5273
|
+
},
|
|
5274
|
+
body: JSON.stringify({
|
|
5275
|
+
reason: "sdk_disconnect_http_fallback"
|
|
5276
|
+
})
|
|
5277
|
+
}
|
|
5278
|
+
);
|
|
5279
|
+
} catch {
|
|
5280
|
+
}
|
|
5281
|
+
}
|
|
5282
|
+
this.client.disconnect();
|
|
5283
|
+
}
|
|
5077
5284
|
// ==================== GRAPH CONTAINER READINESS ====================
|
|
5078
5285
|
/** The last known graph container status, updated by checkReadiness() or on heartbeat */
|
|
5079
5286
|
graphContainerStatus = null;
|
|
@@ -5809,6 +6016,7 @@ var Granular = class {
|
|
|
5809
6016
|
apiKey;
|
|
5810
6017
|
apiUrl;
|
|
5811
6018
|
httpUrl;
|
|
6019
|
+
tokenProvider;
|
|
5812
6020
|
WebSocketCtor;
|
|
5813
6021
|
onUnexpectedClose;
|
|
5814
6022
|
onReconnectError;
|
|
@@ -5826,11 +6034,12 @@ var Granular = class {
|
|
|
5826
6034
|
throw new Error("Granular client requires either apiKey or token. Set GRANULAR_API_KEY or GRANULAR_TOKEN, or pass one in options.");
|
|
5827
6035
|
}
|
|
5828
6036
|
this.apiKey = auth;
|
|
5829
|
-
this.apiUrl = options.apiUrl
|
|
6037
|
+
this.apiUrl = resolveApiUrl(options.apiUrl, options.endpointMode);
|
|
6038
|
+
this.tokenProvider = options.tokenProvider;
|
|
5830
6039
|
this.WebSocketCtor = options.WebSocketCtor;
|
|
5831
6040
|
this.onUnexpectedClose = options.onUnexpectedClose;
|
|
5832
6041
|
this.onReconnectError = options.onReconnectError;
|
|
5833
|
-
this.httpUrl = this.apiUrl.replace("
|
|
6042
|
+
this.httpUrl = this.apiUrl.replace(/^wss:\/\//, "https://").replace(/^ws:\/\//, "http://").replace(/\/ws$/, "");
|
|
5834
6043
|
}
|
|
5835
6044
|
/**
|
|
5836
6045
|
* Records/upserts a user and prepares them for sandbox connections
|
|
@@ -5914,6 +6123,7 @@ var Granular = class {
|
|
|
5914
6123
|
url: this.apiUrl,
|
|
5915
6124
|
sessionId: envData.environmentId,
|
|
5916
6125
|
token: this.apiKey,
|
|
6126
|
+
tokenProvider: this.tokenProvider,
|
|
5917
6127
|
WebSocketCtor: this.WebSocketCtor,
|
|
5918
6128
|
onUnexpectedClose: this.onUnexpectedClose,
|
|
5919
6129
|
onReconnectError: this.onReconnectError
|