@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/README.md
CHANGED
|
@@ -27,6 +27,26 @@ bun add @granular-software/sdk
|
|
|
27
27
|
npm install @granular-software/sdk
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
+
## Endpoint Modes
|
|
31
|
+
|
|
32
|
+
By default, the SDK resolves endpoints like this:
|
|
33
|
+
|
|
34
|
+
- Local mode (`NODE_ENV=development`): `ws://localhost:8787/granular`
|
|
35
|
+
- Production mode (default): `wss://api.granular.dev/v2/ws`
|
|
36
|
+
|
|
37
|
+
Overrides:
|
|
38
|
+
|
|
39
|
+
- SDK option: `endpointMode: 'local' | 'production'`
|
|
40
|
+
- SDK option: `apiUrl: 'ws://... | wss://...'` (highest priority)
|
|
41
|
+
- Env: `GRANULAR_ENDPOINT_MODE=local|production`
|
|
42
|
+
- Env: `GRANULAR_API_URL=...` (highest priority)
|
|
43
|
+
|
|
44
|
+
CLI overrides:
|
|
45
|
+
|
|
46
|
+
- `granular --local <command>`
|
|
47
|
+
- `granular --prod <command>`
|
|
48
|
+
- `granular --env local|production <command>`
|
|
49
|
+
|
|
30
50
|
## Quick Start
|
|
31
51
|
|
|
32
52
|
```typescript
|
package/dist/cli/index.js
CHANGED
|
@@ -5384,6 +5384,57 @@ var {
|
|
|
5384
5384
|
|
|
5385
5385
|
// src/cli/config.ts
|
|
5386
5386
|
var import_dotenv = __toESM(require_main());
|
|
5387
|
+
|
|
5388
|
+
// src/endpoints.ts
|
|
5389
|
+
var LOCAL_API_URL = "ws://localhost:8787/granular";
|
|
5390
|
+
var PRODUCTION_API_URL = "wss://api.granular.dev/v2/ws";
|
|
5391
|
+
var LOCAL_AUTH_URL = "http://localhost:3000";
|
|
5392
|
+
var PRODUCTION_AUTH_URL = "https://app.granular.software";
|
|
5393
|
+
function readEnv(name) {
|
|
5394
|
+
if (typeof process === "undefined" || !process.env) return void 0;
|
|
5395
|
+
return process.env[name];
|
|
5396
|
+
}
|
|
5397
|
+
function normalizeMode(value) {
|
|
5398
|
+
if (!value) return void 0;
|
|
5399
|
+
const normalized = value.trim().toLowerCase();
|
|
5400
|
+
if (normalized === "local") return "local";
|
|
5401
|
+
if (normalized === "prod" || normalized === "production") return "production";
|
|
5402
|
+
if (normalized === "auto") return "auto";
|
|
5403
|
+
return void 0;
|
|
5404
|
+
}
|
|
5405
|
+
function isTruthy(value) {
|
|
5406
|
+
if (!value) return false;
|
|
5407
|
+
const normalized = value.trim().toLowerCase();
|
|
5408
|
+
return normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on";
|
|
5409
|
+
}
|
|
5410
|
+
function resolveEndpointMode(explicitMode) {
|
|
5411
|
+
const explicit = normalizeMode(explicitMode);
|
|
5412
|
+
if (explicit === "local" || explicit === "production") {
|
|
5413
|
+
return explicit;
|
|
5414
|
+
}
|
|
5415
|
+
const envMode = normalizeMode(readEnv("GRANULAR_ENDPOINT_MODE") || readEnv("GRANULAR_ENV"));
|
|
5416
|
+
if (envMode === "local" || envMode === "production") {
|
|
5417
|
+
return envMode;
|
|
5418
|
+
}
|
|
5419
|
+
if (isTruthy(readEnv("GRANULAR_USE_LOCAL_ENDPOINTS")) || isTruthy(readEnv("GRANULAR_LOCAL"))) {
|
|
5420
|
+
return "local";
|
|
5421
|
+
}
|
|
5422
|
+
if (isTruthy(readEnv("GRANULAR_USE_PRODUCTION_ENDPOINTS")) || isTruthy(readEnv("GRANULAR_PROD"))) {
|
|
5423
|
+
return "production";
|
|
5424
|
+
}
|
|
5425
|
+
return readEnv("NODE_ENV") === "development" ? "local" : "production";
|
|
5426
|
+
}
|
|
5427
|
+
function resolveApiUrl(explicitApiUrl, mode) {
|
|
5428
|
+
return resolveEndpointMode(mode) === "local" ? LOCAL_API_URL : PRODUCTION_API_URL;
|
|
5429
|
+
}
|
|
5430
|
+
function resolveAuthUrl(explicitAuthUrl, mode) {
|
|
5431
|
+
if (explicitAuthUrl) {
|
|
5432
|
+
return explicitAuthUrl;
|
|
5433
|
+
}
|
|
5434
|
+
return resolveEndpointMode(mode) === "local" ? LOCAL_AUTH_URL : PRODUCTION_AUTH_URL;
|
|
5435
|
+
}
|
|
5436
|
+
|
|
5437
|
+
// src/cli/config.ts
|
|
5387
5438
|
var MANIFEST_FILE = "granular.json";
|
|
5388
5439
|
var RC_FILE = ".granularrc";
|
|
5389
5440
|
var ENV_LOCAL_FILE = ".env.local";
|
|
@@ -5453,16 +5504,17 @@ function loadApiKey() {
|
|
|
5453
5504
|
return void 0;
|
|
5454
5505
|
}
|
|
5455
5506
|
function loadApiUrl() {
|
|
5507
|
+
if (process.env.GRANULAR_API_URL) return process.env.GRANULAR_API_URL;
|
|
5508
|
+
const modeOverride = process.env.GRANULAR_ENDPOINT_MODE;
|
|
5509
|
+
if (modeOverride === "local" || modeOverride === "production" || modeOverride === "prod") {
|
|
5510
|
+
return resolveApiUrl(void 0, modeOverride === "prod" ? "production" : modeOverride);
|
|
5511
|
+
}
|
|
5456
5512
|
const rc = readRcFile();
|
|
5457
5513
|
if (rc.apiUrl) return rc.apiUrl;
|
|
5458
|
-
|
|
5459
|
-
return "https://cf-api-gateway.arthur6084.workers.dev/granular";
|
|
5514
|
+
return resolveApiUrl();
|
|
5460
5515
|
}
|
|
5461
5516
|
function loadAuthUrl() {
|
|
5462
|
-
|
|
5463
|
-
return process.env.GRANULAR_AUTH_URL;
|
|
5464
|
-
}
|
|
5465
|
-
return "https://app.granular.software";
|
|
5517
|
+
return resolveAuthUrl(process.env.GRANULAR_AUTH_URL);
|
|
5466
5518
|
}
|
|
5467
5519
|
function saveApiKey(apiKey) {
|
|
5468
5520
|
const envLocalPath = getEnvLocalPath();
|
|
@@ -8623,6 +8675,28 @@ async function simulateCommand(sandboxIdArg) {
|
|
|
8623
8675
|
var VERSION = "0.2.0";
|
|
8624
8676
|
var program2 = new Command();
|
|
8625
8677
|
program2.name("granular").description("Build and deploy AI sandboxes from code").version(VERSION, "-v, --version");
|
|
8678
|
+
program2.option("--local", "Use local endpoints (localhost)").option("--prod", "Use production endpoints").option("--env <target>", "Endpoint target: local|production");
|
|
8679
|
+
program2.hook("preAction", () => {
|
|
8680
|
+
const opts = program2.opts();
|
|
8681
|
+
const normalizedEnv = opts.env?.trim().toLowerCase();
|
|
8682
|
+
if (opts.local && opts.prod) {
|
|
8683
|
+
error("Cannot use --local and --prod at the same time.");
|
|
8684
|
+
process.exit(1);
|
|
8685
|
+
}
|
|
8686
|
+
if (normalizedEnv && !["local", "production", "prod"].includes(normalizedEnv)) {
|
|
8687
|
+
error('Invalid --env value. Use "local" or "production".');
|
|
8688
|
+
process.exit(1);
|
|
8689
|
+
}
|
|
8690
|
+
let mode;
|
|
8691
|
+
if (opts.local || normalizedEnv === "local") {
|
|
8692
|
+
mode = "local";
|
|
8693
|
+
} else if (opts.prod || normalizedEnv === "production" || normalizedEnv === "prod") {
|
|
8694
|
+
mode = "production";
|
|
8695
|
+
}
|
|
8696
|
+
if (mode) {
|
|
8697
|
+
process.env.GRANULAR_ENDPOINT_MODE = mode;
|
|
8698
|
+
}
|
|
8699
|
+
});
|
|
8626
8700
|
program2.command("init [project-name]").description("Initialize a new Granular project").option("--skip-build", "Skip the initial build step").action(async (projectName, opts) => {
|
|
8627
8701
|
try {
|
|
8628
8702
|
await initCommand(projectName, { skipBuild: opts.skipBuild });
|
package/dist/index.d.mts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as Automerge from '@automerge/automerge';
|
|
2
|
-
import { W as WSClientOptions, T as ToolWithHandler, P as PublishToolsResult, J as Job, a as ToolHandler, I as InstanceToolHandler, b as ToolInfo, c as ToolsChangedEvent, D as DomainState, G as GranularOptions, R as RecordUserOptions, U as User, C as ConnectOptions, E as EnvironmentData, d as GraphQLResult, e as DefineRelationshipOptions, f as RelationshipInfo, M as ModelRef, g as ManifestContent, h as RecordObjectOptions, i as RecordObjectResult, S as SandboxListResponse, j as Sandbox, k as CreateSandboxData, l as DeleteResponse, m as PermissionProfile, n as CreatePermissionProfileData, o as CreateEnvironmentData, p as Subject, A as AssignmentListResponse } from './types-
|
|
3
|
-
export {
|
|
2
|
+
import { W as WSClientOptions, T as ToolWithHandler, P as PublishToolsResult, J as Job, a as ToolHandler, I as InstanceToolHandler, b as ToolInfo, c as ToolsChangedEvent, D as DomainState, G as GranularOptions, R as RecordUserOptions, U as User, C as ConnectOptions, E as EnvironmentData, d as GraphQLResult, e as DefineRelationshipOptions, f as RelationshipInfo, M as ModelRef, g as ManifestContent, h as RecordObjectOptions, i as RecordObjectResult, S as SandboxListResponse, j as Sandbox, k as CreateSandboxData, l as DeleteResponse, m as PermissionProfile, n as CreatePermissionProfileData, o as CreateEnvironmentData, p as Subject, A as AssignmentListResponse } from './types-BOPsFZYi.mjs';
|
|
3
|
+
export { a6 as APIError, r as AccessTokenProvider, w as Assignment, H as Build, K as BuildListResponse, B as BuildPolicy, F as BuildStatus, s as EndpointMode, x as EnvironmentListResponse, t as GranularAuth, L as JobStatus, N as JobSubmitResult, y as Manifest, a4 as ManifestImport, z as ManifestListResponse, a3 as ManifestOperation, a1 as ManifestPropertySpec, a2 as ManifestRelationshipDef, a5 as ManifestVolume, v as PermissionProfileListResponse, u as PermissionRules, O as Prompt, X as RPCRequest, _ as RPCRequestFromServer, Y as RPCResponse, Z as SyncMessage, $ as ToolInvokeParams, a0 as ToolResultParams, q as ToolSchema, Q as WSDisconnectInfo, V as WSReconnectErrorInfo } from './types-BOPsFZYi.mjs';
|
|
4
4
|
import { Doc } from '@automerge/automerge/slim';
|
|
5
5
|
|
|
6
6
|
declare class WSClient {
|
|
@@ -16,9 +16,16 @@ declare class WSClient {
|
|
|
16
16
|
doc: Automerge.Doc<Record<string, unknown>>;
|
|
17
17
|
private syncState;
|
|
18
18
|
private reconnectTimer;
|
|
19
|
+
private tokenRefreshTimer;
|
|
19
20
|
private isExplicitlyDisconnected;
|
|
20
21
|
private options;
|
|
21
22
|
constructor(options: WSClientOptions);
|
|
23
|
+
private clearTokenRefreshTimer;
|
|
24
|
+
private decodeBase64Url;
|
|
25
|
+
private getTokenExpiryMs;
|
|
26
|
+
private scheduleTokenRefresh;
|
|
27
|
+
private refreshTokenInBackground;
|
|
28
|
+
private resolveTokenForConnect;
|
|
22
29
|
/**
|
|
23
30
|
* Connect to the WebSocket server
|
|
24
31
|
* @returns {Promise<void>} Resolves when connection is open
|
|
@@ -325,6 +332,15 @@ declare class Environment extends Session {
|
|
|
325
332
|
get permissionProfileId(): string;
|
|
326
333
|
/** The GraphQL API endpoint URL */
|
|
327
334
|
get apiEndpoint(): string;
|
|
335
|
+
private getRuntimeBaseUrl;
|
|
336
|
+
/**
|
|
337
|
+
* Close the session and disconnect from the sandbox.
|
|
338
|
+
*
|
|
339
|
+
* Sends `client.goodbye` over WebSocket first, then issues an HTTP fallback
|
|
340
|
+
* to the runtime goodbye endpoint if no definitive WS-side runtime notify
|
|
341
|
+
* acknowledgement was observed.
|
|
342
|
+
*/
|
|
343
|
+
disconnect(): Promise<void>;
|
|
328
344
|
/** The last known graph container status, updated by checkReadiness() or on heartbeat */
|
|
329
345
|
graphContainerStatus: {
|
|
330
346
|
lastKeepAliveAt: number;
|
|
@@ -640,6 +656,7 @@ declare class Granular {
|
|
|
640
656
|
private apiKey;
|
|
641
657
|
private apiUrl;
|
|
642
658
|
private httpUrl;
|
|
659
|
+
private tokenProvider?;
|
|
643
660
|
private WebSocketCtor?;
|
|
644
661
|
private onUnexpectedClose?;
|
|
645
662
|
private onReconnectError?;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as Automerge from '@automerge/automerge';
|
|
2
|
-
import { W as WSClientOptions, T as ToolWithHandler, P as PublishToolsResult, J as Job, a as ToolHandler, I as InstanceToolHandler, b as ToolInfo, c as ToolsChangedEvent, D as DomainState, G as GranularOptions, R as RecordUserOptions, U as User, C as ConnectOptions, E as EnvironmentData, d as GraphQLResult, e as DefineRelationshipOptions, f as RelationshipInfo, M as ModelRef, g as ManifestContent, h as RecordObjectOptions, i as RecordObjectResult, S as SandboxListResponse, j as Sandbox, k as CreateSandboxData, l as DeleteResponse, m as PermissionProfile, n as CreatePermissionProfileData, o as CreateEnvironmentData, p as Subject, A as AssignmentListResponse } from './types-
|
|
3
|
-
export {
|
|
2
|
+
import { W as WSClientOptions, T as ToolWithHandler, P as PublishToolsResult, J as Job, a as ToolHandler, I as InstanceToolHandler, b as ToolInfo, c as ToolsChangedEvent, D as DomainState, G as GranularOptions, R as RecordUserOptions, U as User, C as ConnectOptions, E as EnvironmentData, d as GraphQLResult, e as DefineRelationshipOptions, f as RelationshipInfo, M as ModelRef, g as ManifestContent, h as RecordObjectOptions, i as RecordObjectResult, S as SandboxListResponse, j as Sandbox, k as CreateSandboxData, l as DeleteResponse, m as PermissionProfile, n as CreatePermissionProfileData, o as CreateEnvironmentData, p as Subject, A as AssignmentListResponse } from './types-BOPsFZYi.js';
|
|
3
|
+
export { a6 as APIError, r as AccessTokenProvider, w as Assignment, H as Build, K as BuildListResponse, B as BuildPolicy, F as BuildStatus, s as EndpointMode, x as EnvironmentListResponse, t as GranularAuth, L as JobStatus, N as JobSubmitResult, y as Manifest, a4 as ManifestImport, z as ManifestListResponse, a3 as ManifestOperation, a1 as ManifestPropertySpec, a2 as ManifestRelationshipDef, a5 as ManifestVolume, v as PermissionProfileListResponse, u as PermissionRules, O as Prompt, X as RPCRequest, _ as RPCRequestFromServer, Y as RPCResponse, Z as SyncMessage, $ as ToolInvokeParams, a0 as ToolResultParams, q as ToolSchema, Q as WSDisconnectInfo, V as WSReconnectErrorInfo } from './types-BOPsFZYi.js';
|
|
4
4
|
import { Doc } from '@automerge/automerge/slim';
|
|
5
5
|
|
|
6
6
|
declare class WSClient {
|
|
@@ -16,9 +16,16 @@ declare class WSClient {
|
|
|
16
16
|
doc: Automerge.Doc<Record<string, unknown>>;
|
|
17
17
|
private syncState;
|
|
18
18
|
private reconnectTimer;
|
|
19
|
+
private tokenRefreshTimer;
|
|
19
20
|
private isExplicitlyDisconnected;
|
|
20
21
|
private options;
|
|
21
22
|
constructor(options: WSClientOptions);
|
|
23
|
+
private clearTokenRefreshTimer;
|
|
24
|
+
private decodeBase64Url;
|
|
25
|
+
private getTokenExpiryMs;
|
|
26
|
+
private scheduleTokenRefresh;
|
|
27
|
+
private refreshTokenInBackground;
|
|
28
|
+
private resolveTokenForConnect;
|
|
22
29
|
/**
|
|
23
30
|
* Connect to the WebSocket server
|
|
24
31
|
* @returns {Promise<void>} Resolves when connection is open
|
|
@@ -325,6 +332,15 @@ declare class Environment extends Session {
|
|
|
325
332
|
get permissionProfileId(): string;
|
|
326
333
|
/** The GraphQL API endpoint URL */
|
|
327
334
|
get apiEndpoint(): string;
|
|
335
|
+
private getRuntimeBaseUrl;
|
|
336
|
+
/**
|
|
337
|
+
* Close the session and disconnect from the sandbox.
|
|
338
|
+
*
|
|
339
|
+
* Sends `client.goodbye` over WebSocket first, then issues an HTTP fallback
|
|
340
|
+
* to the runtime goodbye endpoint if no definitive WS-side runtime notify
|
|
341
|
+
* acknowledgement was observed.
|
|
342
|
+
*/
|
|
343
|
+
disconnect(): Promise<void>;
|
|
328
344
|
/** The last known graph container status, updated by checkReadiness() or on heartbeat */
|
|
329
345
|
graphContainerStatus: {
|
|
330
346
|
lastKeepAliveAt: number;
|
|
@@ -640,6 +656,7 @@ declare class Granular {
|
|
|
640
656
|
private apiKey;
|
|
641
657
|
private apiUrl;
|
|
642
658
|
private httpUrl;
|
|
659
|
+
private tokenProvider?;
|
|
643
660
|
private WebSocketCtor?;
|
|
644
661
|
private onUnexpectedClose?;
|
|
645
662
|
private onReconnectError?;
|
package/dist/index.js
CHANGED
|
@@ -3939,6 +3939,9 @@ if (typeof globalThis !== "undefined" && globalThis.WebSocket) {
|
|
|
3939
3939
|
GlobalWebSocket = globalThis.WebSocket;
|
|
3940
3940
|
}
|
|
3941
3941
|
var READY_STATE_OPEN = 1;
|
|
3942
|
+
var TOKEN_REFRESH_LEEWAY_MS = 2 * 60 * 1e3;
|
|
3943
|
+
var TOKEN_REFRESH_RETRY_MS = 30 * 1e3;
|
|
3944
|
+
var MAX_TIMER_DELAY_MS = 2147483647;
|
|
3942
3945
|
var WSClient = class {
|
|
3943
3946
|
ws = null;
|
|
3944
3947
|
url;
|
|
@@ -3952,6 +3955,7 @@ var WSClient = class {
|
|
|
3952
3955
|
doc = Automerge__namespace.init();
|
|
3953
3956
|
syncState = Automerge__namespace.initSyncState();
|
|
3954
3957
|
reconnectTimer = null;
|
|
3958
|
+
tokenRefreshTimer = null;
|
|
3955
3959
|
isExplicitlyDisconnected = false;
|
|
3956
3960
|
options;
|
|
3957
3961
|
constructor(options) {
|
|
@@ -3960,12 +3964,109 @@ var WSClient = class {
|
|
|
3960
3964
|
this.sessionId = options.sessionId;
|
|
3961
3965
|
this.token = options.token;
|
|
3962
3966
|
}
|
|
3967
|
+
clearTokenRefreshTimer() {
|
|
3968
|
+
if (this.tokenRefreshTimer) {
|
|
3969
|
+
clearTimeout(this.tokenRefreshTimer);
|
|
3970
|
+
this.tokenRefreshTimer = null;
|
|
3971
|
+
}
|
|
3972
|
+
}
|
|
3973
|
+
decodeBase64Url(payload) {
|
|
3974
|
+
const base64 = payload.replace(/-/g, "+").replace(/_/g, "/");
|
|
3975
|
+
const padded = base64 + "=".repeat((4 - (base64.length % 4 || 4)) % 4);
|
|
3976
|
+
if (typeof atob === "function") {
|
|
3977
|
+
return atob(padded);
|
|
3978
|
+
}
|
|
3979
|
+
const maybeBuffer = globalThis.Buffer;
|
|
3980
|
+
if (maybeBuffer) {
|
|
3981
|
+
return maybeBuffer.from(padded, "base64").toString("utf8");
|
|
3982
|
+
}
|
|
3983
|
+
throw new Error("No base64 decoder available");
|
|
3984
|
+
}
|
|
3985
|
+
getTokenExpiryMs(token) {
|
|
3986
|
+
const parts = token.split(".");
|
|
3987
|
+
if (parts.length < 2) {
|
|
3988
|
+
return null;
|
|
3989
|
+
}
|
|
3990
|
+
try {
|
|
3991
|
+
const payloadRaw = this.decodeBase64Url(parts[1]);
|
|
3992
|
+
const payload = JSON.parse(payloadRaw);
|
|
3993
|
+
if (typeof payload.exp !== "number" || !Number.isFinite(payload.exp)) {
|
|
3994
|
+
return null;
|
|
3995
|
+
}
|
|
3996
|
+
return payload.exp * 1e3;
|
|
3997
|
+
} catch {
|
|
3998
|
+
return null;
|
|
3999
|
+
}
|
|
4000
|
+
}
|
|
4001
|
+
scheduleTokenRefresh() {
|
|
4002
|
+
this.clearTokenRefreshTimer();
|
|
4003
|
+
if (!this.options.tokenProvider || this.isExplicitlyDisconnected) {
|
|
4004
|
+
return;
|
|
4005
|
+
}
|
|
4006
|
+
const expiresAt = this.getTokenExpiryMs(this.token);
|
|
4007
|
+
if (!expiresAt) {
|
|
4008
|
+
return;
|
|
4009
|
+
}
|
|
4010
|
+
const refreshInMs = Math.max(1e3, expiresAt - Date.now() - TOKEN_REFRESH_LEEWAY_MS);
|
|
4011
|
+
const delay = Math.min(refreshInMs, MAX_TIMER_DELAY_MS);
|
|
4012
|
+
this.tokenRefreshTimer = setTimeout(() => {
|
|
4013
|
+
void this.refreshTokenInBackground();
|
|
4014
|
+
}, delay);
|
|
4015
|
+
}
|
|
4016
|
+
async refreshTokenInBackground() {
|
|
4017
|
+
if (!this.options.tokenProvider || this.isExplicitlyDisconnected) {
|
|
4018
|
+
return;
|
|
4019
|
+
}
|
|
4020
|
+
try {
|
|
4021
|
+
const refreshedToken = await this.options.tokenProvider();
|
|
4022
|
+
if (typeof refreshedToken !== "string" || refreshedToken.length === 0) {
|
|
4023
|
+
throw new Error("Token provider returned no token");
|
|
4024
|
+
}
|
|
4025
|
+
this.token = refreshedToken;
|
|
4026
|
+
this.scheduleTokenRefresh();
|
|
4027
|
+
} catch (error) {
|
|
4028
|
+
if (this.isExplicitlyDisconnected) {
|
|
4029
|
+
return;
|
|
4030
|
+
}
|
|
4031
|
+
console.warn("[Granular] Token refresh failed, retrying soon:", error);
|
|
4032
|
+
this.clearTokenRefreshTimer();
|
|
4033
|
+
this.tokenRefreshTimer = setTimeout(() => {
|
|
4034
|
+
void this.refreshTokenInBackground();
|
|
4035
|
+
}, TOKEN_REFRESH_RETRY_MS);
|
|
4036
|
+
}
|
|
4037
|
+
}
|
|
4038
|
+
async resolveTokenForConnect() {
|
|
4039
|
+
if (!this.options.tokenProvider) {
|
|
4040
|
+
return this.token;
|
|
4041
|
+
}
|
|
4042
|
+
const expiresAt = this.getTokenExpiryMs(this.token);
|
|
4043
|
+
const shouldRefresh = expiresAt !== null && expiresAt - Date.now() <= TOKEN_REFRESH_LEEWAY_MS;
|
|
4044
|
+
if (!shouldRefresh) {
|
|
4045
|
+
return this.token;
|
|
4046
|
+
}
|
|
4047
|
+
try {
|
|
4048
|
+
const refreshedToken = await this.options.tokenProvider();
|
|
4049
|
+
if (typeof refreshedToken !== "string" || refreshedToken.length === 0) {
|
|
4050
|
+
throw new Error("Token provider returned no token");
|
|
4051
|
+
}
|
|
4052
|
+
this.token = refreshedToken;
|
|
4053
|
+
return refreshedToken;
|
|
4054
|
+
} catch (error) {
|
|
4055
|
+
if (expiresAt > Date.now()) {
|
|
4056
|
+
console.warn("[Granular] Token refresh failed, using current token:", error);
|
|
4057
|
+
return this.token;
|
|
4058
|
+
}
|
|
4059
|
+
throw error;
|
|
4060
|
+
}
|
|
4061
|
+
}
|
|
3963
4062
|
/**
|
|
3964
4063
|
* Connect to the WebSocket server
|
|
3965
4064
|
* @returns {Promise<void>} Resolves when connection is open
|
|
3966
4065
|
*/
|
|
3967
4066
|
async connect() {
|
|
4067
|
+
const token = await this.resolveTokenForConnect();
|
|
3968
4068
|
this.isExplicitlyDisconnected = false;
|
|
4069
|
+
this.scheduleTokenRefresh();
|
|
3969
4070
|
if (this.reconnectTimer) {
|
|
3970
4071
|
clearTimeout(this.reconnectTimer);
|
|
3971
4072
|
this.reconnectTimer = null;
|
|
@@ -3985,7 +4086,7 @@ var WSClient = class {
|
|
|
3985
4086
|
try {
|
|
3986
4087
|
const wsUrl = new URL(this.url);
|
|
3987
4088
|
wsUrl.searchParams.set("sessionId", this.sessionId);
|
|
3988
|
-
wsUrl.searchParams.set("token",
|
|
4089
|
+
wsUrl.searchParams.set("token", token);
|
|
3989
4090
|
this.ws = new WebSocketClass(wsUrl.toString());
|
|
3990
4091
|
if (!this.ws) throw new Error("Failed to create WebSocket");
|
|
3991
4092
|
const socket = this.ws;
|
|
@@ -4190,10 +4291,10 @@ var WSClient = class {
|
|
|
4190
4291
|
const snapshotMessage = message;
|
|
4191
4292
|
try {
|
|
4192
4293
|
const bytes = new Uint8Array(snapshotMessage.data);
|
|
4193
|
-
console.log("[Granular DEBUG] Loading snapshot bytes:", bytes.length);
|
|
4294
|
+
console.log("[Granular DEBUG] Loading Automerge session snapshot bytes:", bytes.length);
|
|
4194
4295
|
this.doc = Automerge__namespace.load(bytes);
|
|
4195
4296
|
this.emit("sync", this.doc);
|
|
4196
|
-
console.log("[Granular DEBUG]
|
|
4297
|
+
console.log("[Granular DEBUG] Automerge session snapshot loaded. Doc:", JSON.stringify(Automerge__namespace.toJS(this.doc)));
|
|
4197
4298
|
} catch (e) {
|
|
4198
4299
|
console.warn("[Granular] Failed to load snapshot message", e);
|
|
4199
4300
|
}
|
|
@@ -4356,6 +4457,7 @@ var WSClient = class {
|
|
|
4356
4457
|
clearTimeout(this.reconnectTimer);
|
|
4357
4458
|
this.reconnectTimer = null;
|
|
4358
4459
|
}
|
|
4460
|
+
this.clearTokenRefreshTimer();
|
|
4359
4461
|
if (this.ws) {
|
|
4360
4462
|
this.ws.close(1e3, "Client disconnect");
|
|
4361
4463
|
this.ws = null;
|
|
@@ -4842,6 +4944,13 @@ import { ${allImports} } from "./sandbox-tools";
|
|
|
4842
4944
|
* Close the session and disconnect from the sandbox
|
|
4843
4945
|
*/
|
|
4844
4946
|
async disconnect() {
|
|
4947
|
+
try {
|
|
4948
|
+
await this.client.call("client.goodbye", {
|
|
4949
|
+
clientId: this.clientId,
|
|
4950
|
+
timestamp: Date.now()
|
|
4951
|
+
});
|
|
4952
|
+
} catch (error) {
|
|
4953
|
+
}
|
|
4845
4954
|
this.client.disconnect();
|
|
4846
4955
|
}
|
|
4847
4956
|
// --- Event Handling ---
|
|
@@ -5052,6 +5161,50 @@ var JobImplementation = class {
|
|
|
5052
5161
|
}
|
|
5053
5162
|
};
|
|
5054
5163
|
|
|
5164
|
+
// src/endpoints.ts
|
|
5165
|
+
var LOCAL_API_URL = "ws://localhost:8787/granular";
|
|
5166
|
+
var PRODUCTION_API_URL = "wss://api.granular.dev/v2/ws";
|
|
5167
|
+
function readEnv(name) {
|
|
5168
|
+
if (typeof process === "undefined" || !process.env) return void 0;
|
|
5169
|
+
return process.env[name];
|
|
5170
|
+
}
|
|
5171
|
+
function normalizeMode(value) {
|
|
5172
|
+
if (!value) return void 0;
|
|
5173
|
+
const normalized = value.trim().toLowerCase();
|
|
5174
|
+
if (normalized === "local") return "local";
|
|
5175
|
+
if (normalized === "prod" || normalized === "production") return "production";
|
|
5176
|
+
if (normalized === "auto") return "auto";
|
|
5177
|
+
return void 0;
|
|
5178
|
+
}
|
|
5179
|
+
function isTruthy(value) {
|
|
5180
|
+
if (!value) return false;
|
|
5181
|
+
const normalized = value.trim().toLowerCase();
|
|
5182
|
+
return normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on";
|
|
5183
|
+
}
|
|
5184
|
+
function resolveEndpointMode(explicitMode) {
|
|
5185
|
+
const explicit = normalizeMode(explicitMode);
|
|
5186
|
+
if (explicit === "local" || explicit === "production") {
|
|
5187
|
+
return explicit;
|
|
5188
|
+
}
|
|
5189
|
+
const envMode = normalizeMode(readEnv("GRANULAR_ENDPOINT_MODE") || readEnv("GRANULAR_ENV"));
|
|
5190
|
+
if (envMode === "local" || envMode === "production") {
|
|
5191
|
+
return envMode;
|
|
5192
|
+
}
|
|
5193
|
+
if (isTruthy(readEnv("GRANULAR_USE_LOCAL_ENDPOINTS")) || isTruthy(readEnv("GRANULAR_LOCAL"))) {
|
|
5194
|
+
return "local";
|
|
5195
|
+
}
|
|
5196
|
+
if (isTruthy(readEnv("GRANULAR_USE_PRODUCTION_ENDPOINTS")) || isTruthy(readEnv("GRANULAR_PROD"))) {
|
|
5197
|
+
return "production";
|
|
5198
|
+
}
|
|
5199
|
+
return readEnv("NODE_ENV") === "development" ? "local" : "production";
|
|
5200
|
+
}
|
|
5201
|
+
function resolveApiUrl(explicitApiUrl, mode) {
|
|
5202
|
+
if (explicitApiUrl) {
|
|
5203
|
+
return explicitApiUrl;
|
|
5204
|
+
}
|
|
5205
|
+
return resolveEndpointMode(mode) === "local" ? LOCAL_API_URL : PRODUCTION_API_URL;
|
|
5206
|
+
}
|
|
5207
|
+
|
|
5055
5208
|
// src/client.ts
|
|
5056
5209
|
var STANDARD_MODULES_OPERATIONS = [
|
|
5057
5210
|
{ create: "entity", has: { id: { value: "auto-generated" }, createdAt: { value: void 0 } } },
|
|
@@ -5096,6 +5249,60 @@ var Environment = class _Environment extends Session {
|
|
|
5096
5249
|
get apiEndpoint() {
|
|
5097
5250
|
return this._apiEndpoint;
|
|
5098
5251
|
}
|
|
5252
|
+
getRuntimeBaseUrl() {
|
|
5253
|
+
try {
|
|
5254
|
+
const endpoint = new URL(this._apiEndpoint);
|
|
5255
|
+
const graphqlSuffix = "/orchestrator/graphql";
|
|
5256
|
+
if (endpoint.pathname.endsWith(graphqlSuffix)) {
|
|
5257
|
+
endpoint.pathname = endpoint.pathname.slice(0, -graphqlSuffix.length);
|
|
5258
|
+
} else if (endpoint.pathname.endsWith("/graphql")) {
|
|
5259
|
+
endpoint.pathname = endpoint.pathname.slice(0, -"/graphql".length);
|
|
5260
|
+
}
|
|
5261
|
+
endpoint.search = "";
|
|
5262
|
+
endpoint.hash = "";
|
|
5263
|
+
return endpoint.toString().replace(/\/$/, "");
|
|
5264
|
+
} catch {
|
|
5265
|
+
return this._apiEndpoint.replace(/\/orchestrator\/graphql$/, "").replace(/\/$/, "");
|
|
5266
|
+
}
|
|
5267
|
+
}
|
|
5268
|
+
/**
|
|
5269
|
+
* Close the session and disconnect from the sandbox.
|
|
5270
|
+
*
|
|
5271
|
+
* Sends `client.goodbye` over WebSocket first, then issues an HTTP fallback
|
|
5272
|
+
* to the runtime goodbye endpoint if no definitive WS-side runtime notify
|
|
5273
|
+
* acknowledgement was observed.
|
|
5274
|
+
*/
|
|
5275
|
+
async disconnect() {
|
|
5276
|
+
let wsNotifiedRuntime = false;
|
|
5277
|
+
try {
|
|
5278
|
+
const goodbye = await this.rpc("client.goodbye", {
|
|
5279
|
+
timestamp: Date.now()
|
|
5280
|
+
});
|
|
5281
|
+
wsNotifiedRuntime = Boolean(goodbye?.ok && goodbye?.via);
|
|
5282
|
+
} catch {
|
|
5283
|
+
wsNotifiedRuntime = false;
|
|
5284
|
+
}
|
|
5285
|
+
if (!wsNotifiedRuntime) {
|
|
5286
|
+
try {
|
|
5287
|
+
const runtimeBase = this.getRuntimeBaseUrl();
|
|
5288
|
+
await fetch(
|
|
5289
|
+
`${runtimeBase}/orchestrator/runtime/environments/${this.environmentId}/session-goodbye`,
|
|
5290
|
+
{
|
|
5291
|
+
method: "POST",
|
|
5292
|
+
headers: {
|
|
5293
|
+
"Content-Type": "application/json",
|
|
5294
|
+
"Authorization": `Bearer ${this._apiKey}`
|
|
5295
|
+
},
|
|
5296
|
+
body: JSON.stringify({
|
|
5297
|
+
reason: "sdk_disconnect_http_fallback"
|
|
5298
|
+
})
|
|
5299
|
+
}
|
|
5300
|
+
);
|
|
5301
|
+
} catch {
|
|
5302
|
+
}
|
|
5303
|
+
}
|
|
5304
|
+
this.client.disconnect();
|
|
5305
|
+
}
|
|
5099
5306
|
// ==================== GRAPH CONTAINER READINESS ====================
|
|
5100
5307
|
/** The last known graph container status, updated by checkReadiness() or on heartbeat */
|
|
5101
5308
|
graphContainerStatus = null;
|
|
@@ -5831,6 +6038,7 @@ var Granular = class {
|
|
|
5831
6038
|
apiKey;
|
|
5832
6039
|
apiUrl;
|
|
5833
6040
|
httpUrl;
|
|
6041
|
+
tokenProvider;
|
|
5834
6042
|
WebSocketCtor;
|
|
5835
6043
|
onUnexpectedClose;
|
|
5836
6044
|
onReconnectError;
|
|
@@ -5848,11 +6056,12 @@ var Granular = class {
|
|
|
5848
6056
|
throw new Error("Granular client requires either apiKey or token. Set GRANULAR_API_KEY or GRANULAR_TOKEN, or pass one in options.");
|
|
5849
6057
|
}
|
|
5850
6058
|
this.apiKey = auth;
|
|
5851
|
-
this.apiUrl = options.apiUrl
|
|
6059
|
+
this.apiUrl = resolveApiUrl(options.apiUrl, options.endpointMode);
|
|
6060
|
+
this.tokenProvider = options.tokenProvider;
|
|
5852
6061
|
this.WebSocketCtor = options.WebSocketCtor;
|
|
5853
6062
|
this.onUnexpectedClose = options.onUnexpectedClose;
|
|
5854
6063
|
this.onReconnectError = options.onReconnectError;
|
|
5855
|
-
this.httpUrl = this.apiUrl.replace("
|
|
6064
|
+
this.httpUrl = this.apiUrl.replace(/^wss:\/\//, "https://").replace(/^ws:\/\//, "http://").replace(/\/ws$/, "");
|
|
5856
6065
|
}
|
|
5857
6066
|
/**
|
|
5858
6067
|
* Records/upserts a user and prepares them for sandbox connections
|
|
@@ -5936,6 +6145,7 @@ var Granular = class {
|
|
|
5936
6145
|
url: this.apiUrl,
|
|
5937
6146
|
sessionId: envData.environmentId,
|
|
5938
6147
|
token: this.apiKey,
|
|
6148
|
+
tokenProvider: this.tokenProvider,
|
|
5939
6149
|
WebSocketCtor: this.WebSocketCtor,
|
|
5940
6150
|
onUnexpectedClose: this.onUnexpectedClose,
|
|
5941
6151
|
onReconnectError: this.onReconnectError
|