@sekuire/sdk 0.1.11 → 0.1.12

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/beacon.d.ts CHANGED
@@ -35,6 +35,8 @@ export interface BeaconStatus {
35
35
  deploymentUrl?: string;
36
36
  lastHeartbeat?: Date;
37
37
  failedHeartbeats: number;
38
+ /** True if bootstrap failed and agent is running without Sekuire features */
39
+ degradedMode: boolean;
38
40
  }
39
41
  export interface BootstrapResponse {
40
42
  installation_id: string;
@@ -51,9 +53,13 @@ export declare class Beacon {
51
53
  private refreshToken;
52
54
  private lastHeartbeat;
53
55
  private failedHeartbeats;
56
+ private degradedMode;
54
57
  constructor(config: BeaconConfig);
55
58
  /**
56
59
  * Start the beacon - registers with Sekuire and begins heartbeat loop
60
+ *
61
+ * If bootstrap fails, the agent continues running in degraded mode
62
+ * without Sekuire features. This prevents auth failures from crashing agents.
57
63
  */
58
64
  start(): Promise<void>;
59
65
  /**
@@ -64,6 +70,21 @@ export declare class Beacon {
64
70
  * Get current beacon status
65
71
  */
66
72
  getStatus(): BeaconStatus;
73
+ /**
74
+ * Bootstrap with retry and exponential backoff
75
+ *
76
+ * Returns true if bootstrap succeeded, false if all retries exhausted.
77
+ * Does NOT throw - allows agent to continue in degraded mode.
78
+ */
79
+ private bootstrapWithRetry;
80
+ private sleep;
81
+ /**
82
+ * Attempt recovery bootstrap using API key when install token fails
83
+ *
84
+ * Recovery bootstrap allows re-issuing runtime credentials without
85
+ * needing a new install token from the dashboard.
86
+ */
87
+ private tryRecoveryBootstrap;
67
88
  /**
68
89
  * Bootstrap registration with Sekuire
69
90
  *
package/dist/index.d.ts CHANGED
@@ -40,6 +40,8 @@ interface BeaconStatus {
40
40
  deploymentUrl?: string;
41
41
  lastHeartbeat?: Date;
42
42
  failedHeartbeats: number;
43
+ /** True if bootstrap failed and agent is running without Sekuire features */
44
+ degradedMode: boolean;
43
45
  }
44
46
  interface BootstrapResponse {
45
47
  installation_id: string;
@@ -56,9 +58,13 @@ declare class Beacon {
56
58
  private refreshToken;
57
59
  private lastHeartbeat;
58
60
  private failedHeartbeats;
61
+ private degradedMode;
59
62
  constructor(config: BeaconConfig);
60
63
  /**
61
64
  * Start the beacon - registers with Sekuire and begins heartbeat loop
65
+ *
66
+ * If bootstrap fails, the agent continues running in degraded mode
67
+ * without Sekuire features. This prevents auth failures from crashing agents.
62
68
  */
63
69
  start(): Promise<void>;
64
70
  /**
@@ -69,6 +75,21 @@ declare class Beacon {
69
75
  * Get current beacon status
70
76
  */
71
77
  getStatus(): BeaconStatus;
78
+ /**
79
+ * Bootstrap with retry and exponential backoff
80
+ *
81
+ * Returns true if bootstrap succeeded, false if all retries exhausted.
82
+ * Does NOT throw - allows agent to continue in degraded mode.
83
+ */
84
+ private bootstrapWithRetry;
85
+ private sleep;
86
+ /**
87
+ * Attempt recovery bootstrap using API key when install token fails
88
+ *
89
+ * Recovery bootstrap allows re-issuing runtime credentials without
90
+ * needing a new install token from the dashboard.
91
+ */
92
+ private tryRecoveryBootstrap;
72
93
  /**
73
94
  * Bootstrap registration with Sekuire
74
95
  *
@@ -2593,13 +2614,14 @@ interface TaskContext {
2593
2614
  type TaskHandler = (ctx: TaskContext, input: Record<string, unknown>) => Promise<unknown>;
2594
2615
  interface WorkerConfig {
2595
2616
  apiBaseUrl: string;
2596
- token: string;
2597
- agentId?: string;
2598
- apiKey?: string;
2617
+ agentId: string;
2618
+ installToken?: string;
2619
+ runtimeToken?: string;
2599
2620
  heartbeatIntervalMs?: number;
2600
2621
  reconnectDelayMs?: number;
2601
2622
  maxReconnectDelayMs?: number;
2602
2623
  deploymentUrl?: string;
2624
+ capabilities?: string[];
2603
2625
  }
2604
2626
  declare class TaskWorker {
2605
2627
  private eventSource;
@@ -2610,6 +2632,9 @@ declare class TaskWorker {
2610
2632
  private isConnected;
2611
2633
  private isPaused;
2612
2634
  private installationId;
2635
+ private runtimeToken;
2636
+ private refreshToken;
2637
+ private expiresAt;
2613
2638
  private onCommandCallback?;
2614
2639
  constructor(config: WorkerConfig);
2615
2640
  /**
@@ -2640,9 +2665,9 @@ declare class TaskWorker {
2640
2665
  */
2641
2666
  private handleCommand;
2642
2667
  /**
2643
- * Get API key from config or environment
2668
+ * Get runtime token for API calls
2644
2669
  */
2645
- private getApiKey;
2670
+ private getToken;
2646
2671
  /**
2647
2672
  * Connect to SSE stream
2648
2673
  */
@@ -2660,13 +2685,18 @@ declare class TaskWorker {
2660
2685
  */
2661
2686
  private completeTask;
2662
2687
  /**
2663
- * Register this agent installation with Sekuire
2688
+ * Register this agent installation with Sekuire using install_token
2689
+ * Returns runtime_token for subsequent API calls
2664
2690
  */
2665
2691
  private bootstrap;
2666
2692
  /**
2667
- * Start heartbeat loop
2693
+ * Start heartbeat/lease renewal loop
2668
2694
  */
2669
2695
  private startHeartbeat;
2696
+ /**
2697
+ * Refresh the runtime token using the refresh token
2698
+ */
2699
+ private refreshRuntimeToken;
2670
2700
  }
2671
2701
  declare function createWorker(config: WorkerConfig): TaskWorker;
2672
2702
 
@@ -2787,24 +2817,24 @@ interface AgentSkill {
2787
2817
  /** A2A message with role and parts */
2788
2818
  interface A2AMessage {
2789
2819
  /** Message role: "user" or "agent" */
2790
- role: "user" | "agent";
2820
+ role: 'user' | 'agent';
2791
2821
  /** Message content parts */
2792
2822
  parts: A2AMessagePart[];
2793
2823
  }
2794
2824
  /** Message part content */
2795
2825
  type A2AMessagePart = {
2796
- type: "text";
2826
+ type: 'text';
2797
2827
  text: string;
2798
2828
  } | {
2799
- type: "file";
2829
+ type: 'file';
2800
2830
  uri: string;
2801
2831
  mimeType?: string;
2802
2832
  } | {
2803
- type: "data";
2833
+ type: 'data';
2804
2834
  data: unknown;
2805
2835
  };
2806
2836
  /** A2A task states */
2807
- type A2ATaskState = "pending" | "working" | "input-required" | "completed" | "failed" | "cancelled";
2837
+ type A2ATaskState = 'pending' | 'working' | 'input-required' | 'completed' | 'failed' | 'cancelled';
2808
2838
  /** A2A task status with timestamp */
2809
2839
  interface A2ATaskStatus {
2810
2840
  state: A2ATaskState;
@@ -2856,14 +2886,14 @@ interface A2ARouteResponse {
2856
2886
  }
2857
2887
  /** JSON-RPC 2.0 Request */
2858
2888
  interface JsonRpcRequest {
2859
- jsonrpc: "2.0";
2889
+ jsonrpc: '2.0';
2860
2890
  method: string;
2861
2891
  id: string | number;
2862
2892
  params?: unknown;
2863
2893
  }
2864
2894
  /** JSON-RPC 2.0 Response */
2865
2895
  interface JsonRpcResponse<T = unknown> {
2866
- jsonrpc: "2.0";
2896
+ jsonrpc: '2.0';
2867
2897
  id: string | number;
2868
2898
  result?: T;
2869
2899
  error?: JsonRpcError;
package/dist/index.esm.js CHANGED
@@ -94,6 +94,7 @@ class Beacon {
94
94
  this.refreshToken = null;
95
95
  this.lastHeartbeat = null;
96
96
  this.failedHeartbeats = 0;
97
+ this.degradedMode = false;
97
98
  const resolvedApiKey = config.apiKey || this.getApiKey() || '';
98
99
  const resolvedInstallToken = config.installToken || this.getInstallToken();
99
100
  this.config = {
@@ -109,6 +110,9 @@ class Beacon {
109
110
  }
110
111
  /**
111
112
  * Start the beacon - registers with Sekuire and begins heartbeat loop
113
+ *
114
+ * If bootstrap fails, the agent continues running in degraded mode
115
+ * without Sekuire features. This prevents auth failures from crashing agents.
112
116
  */
113
117
  async start() {
114
118
  if (this.intervalId) {
@@ -121,16 +125,16 @@ class Beacon {
121
125
  console.log(`[Beacon] Deployment URL: ${deploymentUrl}`);
122
126
  }
123
127
  // Bootstrap registration (exchanges install token for runtime credentials)
124
- try {
125
- await this.bootstrap(deploymentUrl);
126
- console.log(`[Beacon] Bootstrap successful, installation ID: ${this.installationId}`);
127
- }
128
- catch (error) {
129
- console.error('[Beacon] Bootstrap failed:', error);
128
+ // On failure, continue in degraded mode - don't crash the agent
129
+ const bootstrapped = await this.bootstrapWithRetry(deploymentUrl);
130
+ if (!bootstrapped) {
131
+ this.degradedMode = true;
132
+ console.warn('[Beacon] Running in degraded mode - Sekuire features disabled');
133
+ console.warn('[Beacon] To re-enable: generate a new install token from the dashboard');
130
134
  this.notifyStatusChange();
131
- // Cannot continue without bootstrap - runtime token required for heartbeats
132
- throw error;
135
+ return;
133
136
  }
137
+ console.log(`[Beacon] Bootstrap successful, installation ID: ${this.installationId}`);
134
138
  // Start heartbeat loop
135
139
  const intervalMs = this.config.heartbeatIntervalSeconds * 1000;
136
140
  this.intervalId = setInterval(() => {
@@ -157,14 +161,106 @@ class Beacon {
157
161
  */
158
162
  getStatus() {
159
163
  return {
160
- isRunning: this.intervalId !== null,
164
+ isRunning: this.intervalId !== null || this.degradedMode,
161
165
  installationId: this.installationId || undefined,
162
166
  runtimeToken: this.runtimeToken ? '***' + this.runtimeToken.slice(-4) : undefined,
163
167
  deploymentUrl: this.config.deploymentUrl || detectDeploymentUrl(),
164
168
  lastHeartbeat: this.lastHeartbeat || undefined,
165
169
  failedHeartbeats: this.failedHeartbeats,
170
+ degradedMode: this.degradedMode,
166
171
  };
167
172
  }
173
+ /**
174
+ * Bootstrap with retry and exponential backoff
175
+ *
176
+ * Returns true if bootstrap succeeded, false if all retries exhausted.
177
+ * Does NOT throw - allows agent to continue in degraded mode.
178
+ */
179
+ async bootstrapWithRetry(deploymentUrl, maxRetries = 3, initialDelayMs = 1000) {
180
+ let delay = initialDelayMs;
181
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
182
+ try {
183
+ await this.bootstrap(deploymentUrl);
184
+ return true;
185
+ }
186
+ catch (error) {
187
+ const isLastAttempt = attempt === maxRetries;
188
+ const errorMsg = error instanceof Error ? error.message : String(error);
189
+ // Check if error is permanent (expired/invalid token) vs transient (network)
190
+ const isPermanentError = errorMsg.includes('401') ||
191
+ errorMsg.includes('expired') ||
192
+ errorMsg.includes('invalid') ||
193
+ errorMsg.includes('not found');
194
+ if (isPermanentError) {
195
+ console.error(`[Beacon] Bootstrap failed (permanent error): ${errorMsg}`);
196
+ // Try recovery bootstrap if we have API key
197
+ if (this.config.apiKey && this.config.workspaceId) {
198
+ console.log('[Beacon] Attempting recovery bootstrap with API key...');
199
+ const recovered = await this.tryRecoveryBootstrap(deploymentUrl);
200
+ if (recovered) {
201
+ return true;
202
+ }
203
+ }
204
+ console.error('[Beacon] Install token expired or invalid - generate a new one from the dashboard');
205
+ return false;
206
+ }
207
+ if (isLastAttempt) {
208
+ console.error(`[Beacon] Bootstrap failed after ${maxRetries} attempts: ${errorMsg}`);
209
+ return false;
210
+ }
211
+ console.warn(`[Beacon] Bootstrap attempt ${attempt}/${maxRetries} failed: ${errorMsg}`);
212
+ console.warn(`[Beacon] Retrying in ${delay}ms...`);
213
+ await this.sleep(delay);
214
+ delay *= 2; // Exponential backoff
215
+ }
216
+ }
217
+ return false;
218
+ }
219
+ sleep(ms) {
220
+ return new Promise(resolve => setTimeout(resolve, ms));
221
+ }
222
+ /**
223
+ * Attempt recovery bootstrap using API key when install token fails
224
+ *
225
+ * Recovery bootstrap allows re-issuing runtime credentials without
226
+ * needing a new install token from the dashboard.
227
+ */
228
+ async tryRecoveryBootstrap(deploymentUrl) {
229
+ if (!this.config.apiKey || !this.config.workspaceId) {
230
+ return false;
231
+ }
232
+ try {
233
+ const response = await fetch(`${this.config.apiBaseUrl}/api/v1/installations/recovery`, {
234
+ method: 'POST',
235
+ headers: {
236
+ 'Content-Type': 'application/json',
237
+ 'X-API-Key': this.config.apiKey,
238
+ },
239
+ body: JSON.stringify({
240
+ agent_id: this.config.agentId,
241
+ workspace_id: this.config.workspaceId,
242
+ upstream_url: deploymentUrl,
243
+ capabilities: this.config.capabilities ?? [],
244
+ heartbeat_interval_seconds: this.config.heartbeatIntervalSeconds,
245
+ }),
246
+ });
247
+ if (!response.ok) {
248
+ const body = await response.text();
249
+ console.error(`[Beacon] Recovery bootstrap failed: ${response.status} - ${body}`);
250
+ return false;
251
+ }
252
+ const data = await response.json();
253
+ this.installationId = data.installation_id;
254
+ this.runtimeToken = data.runtime_token;
255
+ this.refreshToken = data.refresh_token;
256
+ console.log(`[Beacon] Recovery bootstrap successful, installation ID: ${this.installationId}`);
257
+ return true;
258
+ }
259
+ catch (error) {
260
+ console.error('[Beacon] Recovery bootstrap error:', error);
261
+ return false;
262
+ }
263
+ }
168
264
  /**
169
265
  * Bootstrap registration with Sekuire
170
266
  *
@@ -223,7 +319,7 @@ class Beacon {
223
319
  }),
224
320
  });
225
321
  if (response.status === 401) {
226
- // Token expired - try to refresh
322
+ // Token expired - try to refresh using refresh token
227
323
  console.warn('[Beacon] Runtime token expired, attempting refresh...');
228
324
  await this.refreshRuntimeToken();
229
325
  // Retry heartbeat after refresh
@@ -233,6 +329,12 @@ class Beacon {
233
329
  const body = await response.text();
234
330
  throw new Error(`Heartbeat failed: ${response.status} - ${body}`);
235
331
  }
332
+ // Check if server auto-refreshed our token (sliding expiration)
333
+ const data = await response.json();
334
+ if (data.refreshed_token) {
335
+ this.runtimeToken = data.refreshed_token.runtime_token;
336
+ console.log(`[Beacon] Token auto-refreshed by server (version ${data.refreshed_token.token_version})`);
337
+ }
236
338
  this.lastHeartbeat = new Date();
237
339
  this.failedHeartbeats = 0;
238
340
  if (prevFailed > 0) {
@@ -16054,13 +16156,21 @@ class TaskWorker {
16054
16156
  this.isConnected = false;
16055
16157
  this.isPaused = false;
16056
16158
  this.installationId = null;
16159
+ this.runtimeToken = null;
16160
+ this.refreshToken = null;
16161
+ this.expiresAt = null;
16162
+ if (!config.installToken && !config.runtimeToken) {
16163
+ throw new Error("WorkerConfig requires either installToken or runtimeToken");
16164
+ }
16057
16165
  this.config = {
16058
16166
  heartbeatIntervalMs: 10000, // 10 seconds
16059
16167
  reconnectDelayMs: 1000, // 1 second initial
16060
16168
  maxReconnectDelayMs: 30000, // 30 seconds max
16169
+ capabilities: [],
16061
16170
  ...config,
16062
16171
  };
16063
16172
  this.reconnectDelay = this.config.reconnectDelayMs;
16173
+ this.runtimeToken = config.runtimeToken || null;
16064
16174
  }
16065
16175
  /**
16066
16176
  * Register a handler for a specific capability
@@ -16138,10 +16248,13 @@ class TaskWorker {
16138
16248
  }
16139
16249
  }
16140
16250
  /**
16141
- * Get API key from config or environment
16251
+ * Get runtime token for API calls
16142
16252
  */
16143
- getApiKey() {
16144
- return this.config.apiKey || (typeof process !== 'undefined' ? process.env.SEKUIRE_API_KEY : undefined);
16253
+ getToken() {
16254
+ if (!this.runtimeToken) {
16255
+ throw new Error("No runtime token available - call bootstrap first");
16256
+ }
16257
+ return this.runtimeToken;
16145
16258
  }
16146
16259
  /**
16147
16260
  * Connect to SSE stream
@@ -16151,8 +16264,7 @@ class TaskWorker {
16151
16264
  console.log(`[Worker] Connecting to SSE stream: ${url}`);
16152
16265
  this.eventSource = new EventSource$1(url, {
16153
16266
  headers: {
16154
- Authorization: `Bearer ${this.config.token}`,
16155
- ...(this.getApiKey() && { "X-API-Key": this.getApiKey() }),
16267
+ Authorization: `Bearer ${this.getToken()}`,
16156
16268
  },
16157
16269
  });
16158
16270
  this.eventSource.onopen = () => {
@@ -16244,8 +16356,7 @@ class TaskWorker {
16244
16356
  method: "POST",
16245
16357
  headers: {
16246
16358
  "Content-Type": "application/json",
16247
- Authorization: `Bearer ${this.config.token}`,
16248
- ...(this.getApiKey() && { "X-API-Key": this.getApiKey() }),
16359
+ Authorization: `Bearer ${this.getToken()}`,
16249
16360
  },
16250
16361
  body: JSON.stringify({ status, output, error }),
16251
16362
  });
@@ -16255,57 +16366,84 @@ class TaskWorker {
16255
16366
  }
16256
16367
  }
16257
16368
  /**
16258
- * Register this agent installation with Sekuire
16369
+ * Register this agent installation with Sekuire using install_token
16370
+ * Returns runtime_token for subsequent API calls
16259
16371
  */
16260
16372
  async bootstrap() {
16261
- const apiKey = this.getApiKey();
16262
- if (!apiKey || !this.config.agentId) {
16373
+ // If we already have a runtime token, skip bootstrap
16374
+ if (this.runtimeToken) {
16375
+ console.log("[Worker] Using provided runtime token, skipping bootstrap");
16263
16376
  return;
16264
16377
  }
16378
+ if (!this.config.installToken) {
16379
+ throw new Error("No install token provided for bootstrap");
16380
+ }
16265
16381
  const deploymentUrl = this.config.deploymentUrl || detectDeploymentUrl();
16266
16382
  try {
16267
16383
  const response = await fetch(`${this.config.apiBaseUrl}/api/v1/installations/bootstrap`, {
16268
16384
  method: "POST",
16269
16385
  headers: {
16270
16386
  "Content-Type": "application/json",
16271
- "X-API-Key": apiKey,
16272
16387
  },
16273
16388
  body: JSON.stringify({
16274
- agent_id: this.config.agentId,
16389
+ install_token: this.config.installToken,
16275
16390
  upstream_url: deploymentUrl,
16391
+ capabilities: this.config.capabilities || [],
16392
+ heartbeat_interval_seconds: Math.floor((this.config.heartbeatIntervalMs || 10000) / 1000),
16276
16393
  }),
16277
16394
  });
16278
- if (response.ok) {
16279
- const data = await response.json();
16280
- this.installationId = data.installation_id;
16281
- console.log(`[Worker] Registered installation: ${this.installationId}`);
16395
+ if (!response.ok) {
16396
+ const errorText = await response.text();
16397
+ throw new Error(`Bootstrap failed: ${response.status} - ${errorText}`);
16282
16398
  }
16399
+ const data = await response.json();
16400
+ this.installationId = data.installation_id;
16401
+ this.runtimeToken = data.runtime_token;
16402
+ this.refreshToken = data.refresh_token;
16403
+ this.expiresAt = data.expires_at
16404
+ ? new Date(data.expires_at).getTime()
16405
+ : null;
16406
+ console.log(`[Worker] Bootstrapped installation: ${this.installationId}`);
16283
16407
  }
16284
16408
  catch (e) {
16285
- console.warn("[Worker] Bootstrap registration failed:", e);
16409
+ console.error("[Worker] Bootstrap registration failed:", e);
16410
+ throw e;
16286
16411
  }
16287
16412
  }
16288
16413
  /**
16289
- * Start heartbeat loop
16414
+ * Start heartbeat/lease renewal loop
16290
16415
  */
16291
16416
  startHeartbeat() {
16292
16417
  this.heartbeatInterval = setInterval(async () => {
16418
+ if (!this.installationId) {
16419
+ return;
16420
+ }
16293
16421
  try {
16294
16422
  const deploymentUrl = this.config.deploymentUrl || detectDeploymentUrl();
16295
- const response = await fetch(`${this.config.apiBaseUrl}/api/v1/agents/heartbeat`, {
16423
+ const response = await fetch(`${this.config.apiBaseUrl}/api/v1/installations/${this.installationId}/lease`, {
16296
16424
  method: "POST",
16297
16425
  headers: {
16298
16426
  "Content-Type": "application/json",
16299
- Authorization: `Bearer ${this.config.token}`,
16300
- ...(this.getApiKey() && { "X-API-Key": this.getApiKey() }),
16427
+ Authorization: `Bearer ${this.getToken()}`,
16301
16428
  },
16302
16429
  body: JSON.stringify({
16303
- agent_id: this.config.agentId,
16304
- status: this.isPaused ? "paused" : "idle",
16305
- public_url: deploymentUrl,
16306
- load: 0.0,
16430
+ status: this.isPaused ? "stopped" : "running",
16431
+ upstream_url: deploymentUrl,
16432
+ heartbeat_interval_seconds: Math.floor((this.config.heartbeatIntervalMs || 10000) / 1000),
16307
16433
  }),
16308
16434
  });
16435
+ if (response.status === 401) {
16436
+ console.warn("[Worker] Runtime token expired, attempting refresh...");
16437
+ try {
16438
+ await this.refreshRuntimeToken();
16439
+ return; // Will retry on next interval
16440
+ }
16441
+ catch {
16442
+ console.error("[Worker] Token refresh failed, stopping worker");
16443
+ await this.stop();
16444
+ return;
16445
+ }
16446
+ }
16309
16447
  if (response.ok) {
16310
16448
  const data = await response.json();
16311
16449
  if (data.command) {
@@ -16314,10 +16452,34 @@ class TaskWorker {
16314
16452
  }
16315
16453
  }
16316
16454
  catch (e) {
16317
- console.warn("[Worker] Heartbeat failed:", e);
16455
+ console.warn("[Worker] Heartbeat/lease failed:", e);
16318
16456
  }
16319
16457
  }, this.config.heartbeatIntervalMs);
16320
16458
  }
16459
+ /**
16460
+ * Refresh the runtime token using the refresh token
16461
+ */
16462
+ async refreshRuntimeToken() {
16463
+ if (!this.installationId || !this.refreshToken) {
16464
+ throw new Error("Cannot refresh token - missing installation ID or refresh token");
16465
+ }
16466
+ const response = await fetch(`${this.config.apiBaseUrl}/api/v1/installations/${this.installationId}/refresh`, {
16467
+ method: "POST",
16468
+ headers: {
16469
+ "Content-Type": "application/json",
16470
+ },
16471
+ body: JSON.stringify({
16472
+ refresh_token: this.refreshToken,
16473
+ }),
16474
+ });
16475
+ if (!response.ok) {
16476
+ const body = await response.text();
16477
+ throw new Error(`Token refresh failed: ${response.status} - ${body}`);
16478
+ }
16479
+ const data = await response.json();
16480
+ this.runtimeToken = data.runtime_token;
16481
+ console.log("[Worker] Runtime token refreshed successfully");
16482
+ }
16321
16483
  }
16322
16484
  // ============================================================================
16323
16485
  // Convenience Export
package/dist/index.js CHANGED
@@ -118,6 +118,7 @@ class Beacon {
118
118
  this.refreshToken = null;
119
119
  this.lastHeartbeat = null;
120
120
  this.failedHeartbeats = 0;
121
+ this.degradedMode = false;
121
122
  const resolvedApiKey = config.apiKey || this.getApiKey() || '';
122
123
  const resolvedInstallToken = config.installToken || this.getInstallToken();
123
124
  this.config = {
@@ -133,6 +134,9 @@ class Beacon {
133
134
  }
134
135
  /**
135
136
  * Start the beacon - registers with Sekuire and begins heartbeat loop
137
+ *
138
+ * If bootstrap fails, the agent continues running in degraded mode
139
+ * without Sekuire features. This prevents auth failures from crashing agents.
136
140
  */
137
141
  async start() {
138
142
  if (this.intervalId) {
@@ -145,16 +149,16 @@ class Beacon {
145
149
  console.log(`[Beacon] Deployment URL: ${deploymentUrl}`);
146
150
  }
147
151
  // Bootstrap registration (exchanges install token for runtime credentials)
148
- try {
149
- await this.bootstrap(deploymentUrl);
150
- console.log(`[Beacon] Bootstrap successful, installation ID: ${this.installationId}`);
151
- }
152
- catch (error) {
153
- console.error('[Beacon] Bootstrap failed:', error);
152
+ // On failure, continue in degraded mode - don't crash the agent
153
+ const bootstrapped = await this.bootstrapWithRetry(deploymentUrl);
154
+ if (!bootstrapped) {
155
+ this.degradedMode = true;
156
+ console.warn('[Beacon] Running in degraded mode - Sekuire features disabled');
157
+ console.warn('[Beacon] To re-enable: generate a new install token from the dashboard');
154
158
  this.notifyStatusChange();
155
- // Cannot continue without bootstrap - runtime token required for heartbeats
156
- throw error;
159
+ return;
157
160
  }
161
+ console.log(`[Beacon] Bootstrap successful, installation ID: ${this.installationId}`);
158
162
  // Start heartbeat loop
159
163
  const intervalMs = this.config.heartbeatIntervalSeconds * 1000;
160
164
  this.intervalId = setInterval(() => {
@@ -181,14 +185,106 @@ class Beacon {
181
185
  */
182
186
  getStatus() {
183
187
  return {
184
- isRunning: this.intervalId !== null,
188
+ isRunning: this.intervalId !== null || this.degradedMode,
185
189
  installationId: this.installationId || undefined,
186
190
  runtimeToken: this.runtimeToken ? '***' + this.runtimeToken.slice(-4) : undefined,
187
191
  deploymentUrl: this.config.deploymentUrl || detectDeploymentUrl(),
188
192
  lastHeartbeat: this.lastHeartbeat || undefined,
189
193
  failedHeartbeats: this.failedHeartbeats,
194
+ degradedMode: this.degradedMode,
190
195
  };
191
196
  }
197
+ /**
198
+ * Bootstrap with retry and exponential backoff
199
+ *
200
+ * Returns true if bootstrap succeeded, false if all retries exhausted.
201
+ * Does NOT throw - allows agent to continue in degraded mode.
202
+ */
203
+ async bootstrapWithRetry(deploymentUrl, maxRetries = 3, initialDelayMs = 1000) {
204
+ let delay = initialDelayMs;
205
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
206
+ try {
207
+ await this.bootstrap(deploymentUrl);
208
+ return true;
209
+ }
210
+ catch (error) {
211
+ const isLastAttempt = attempt === maxRetries;
212
+ const errorMsg = error instanceof Error ? error.message : String(error);
213
+ // Check if error is permanent (expired/invalid token) vs transient (network)
214
+ const isPermanentError = errorMsg.includes('401') ||
215
+ errorMsg.includes('expired') ||
216
+ errorMsg.includes('invalid') ||
217
+ errorMsg.includes('not found');
218
+ if (isPermanentError) {
219
+ console.error(`[Beacon] Bootstrap failed (permanent error): ${errorMsg}`);
220
+ // Try recovery bootstrap if we have API key
221
+ if (this.config.apiKey && this.config.workspaceId) {
222
+ console.log('[Beacon] Attempting recovery bootstrap with API key...');
223
+ const recovered = await this.tryRecoveryBootstrap(deploymentUrl);
224
+ if (recovered) {
225
+ return true;
226
+ }
227
+ }
228
+ console.error('[Beacon] Install token expired or invalid - generate a new one from the dashboard');
229
+ return false;
230
+ }
231
+ if (isLastAttempt) {
232
+ console.error(`[Beacon] Bootstrap failed after ${maxRetries} attempts: ${errorMsg}`);
233
+ return false;
234
+ }
235
+ console.warn(`[Beacon] Bootstrap attempt ${attempt}/${maxRetries} failed: ${errorMsg}`);
236
+ console.warn(`[Beacon] Retrying in ${delay}ms...`);
237
+ await this.sleep(delay);
238
+ delay *= 2; // Exponential backoff
239
+ }
240
+ }
241
+ return false;
242
+ }
243
+ sleep(ms) {
244
+ return new Promise(resolve => setTimeout(resolve, ms));
245
+ }
246
+ /**
247
+ * Attempt recovery bootstrap using API key when install token fails
248
+ *
249
+ * Recovery bootstrap allows re-issuing runtime credentials without
250
+ * needing a new install token from the dashboard.
251
+ */
252
+ async tryRecoveryBootstrap(deploymentUrl) {
253
+ if (!this.config.apiKey || !this.config.workspaceId) {
254
+ return false;
255
+ }
256
+ try {
257
+ const response = await fetch(`${this.config.apiBaseUrl}/api/v1/installations/recovery`, {
258
+ method: 'POST',
259
+ headers: {
260
+ 'Content-Type': 'application/json',
261
+ 'X-API-Key': this.config.apiKey,
262
+ },
263
+ body: JSON.stringify({
264
+ agent_id: this.config.agentId,
265
+ workspace_id: this.config.workspaceId,
266
+ upstream_url: deploymentUrl,
267
+ capabilities: this.config.capabilities ?? [],
268
+ heartbeat_interval_seconds: this.config.heartbeatIntervalSeconds,
269
+ }),
270
+ });
271
+ if (!response.ok) {
272
+ const body = await response.text();
273
+ console.error(`[Beacon] Recovery bootstrap failed: ${response.status} - ${body}`);
274
+ return false;
275
+ }
276
+ const data = await response.json();
277
+ this.installationId = data.installation_id;
278
+ this.runtimeToken = data.runtime_token;
279
+ this.refreshToken = data.refresh_token;
280
+ console.log(`[Beacon] Recovery bootstrap successful, installation ID: ${this.installationId}`);
281
+ return true;
282
+ }
283
+ catch (error) {
284
+ console.error('[Beacon] Recovery bootstrap error:', error);
285
+ return false;
286
+ }
287
+ }
192
288
  /**
193
289
  * Bootstrap registration with Sekuire
194
290
  *
@@ -247,7 +343,7 @@ class Beacon {
247
343
  }),
248
344
  });
249
345
  if (response.status === 401) {
250
- // Token expired - try to refresh
346
+ // Token expired - try to refresh using refresh token
251
347
  console.warn('[Beacon] Runtime token expired, attempting refresh...');
252
348
  await this.refreshRuntimeToken();
253
349
  // Retry heartbeat after refresh
@@ -257,6 +353,12 @@ class Beacon {
257
353
  const body = await response.text();
258
354
  throw new Error(`Heartbeat failed: ${response.status} - ${body}`);
259
355
  }
356
+ // Check if server auto-refreshed our token (sliding expiration)
357
+ const data = await response.json();
358
+ if (data.refreshed_token) {
359
+ this.runtimeToken = data.refreshed_token.runtime_token;
360
+ console.log(`[Beacon] Token auto-refreshed by server (version ${data.refreshed_token.token_version})`);
361
+ }
260
362
  this.lastHeartbeat = new Date();
261
363
  this.failedHeartbeats = 0;
262
364
  if (prevFailed > 0) {
@@ -16078,13 +16180,21 @@ class TaskWorker {
16078
16180
  this.isConnected = false;
16079
16181
  this.isPaused = false;
16080
16182
  this.installationId = null;
16183
+ this.runtimeToken = null;
16184
+ this.refreshToken = null;
16185
+ this.expiresAt = null;
16186
+ if (!config.installToken && !config.runtimeToken) {
16187
+ throw new Error("WorkerConfig requires either installToken or runtimeToken");
16188
+ }
16081
16189
  this.config = {
16082
16190
  heartbeatIntervalMs: 10000, // 10 seconds
16083
16191
  reconnectDelayMs: 1000, // 1 second initial
16084
16192
  maxReconnectDelayMs: 30000, // 30 seconds max
16193
+ capabilities: [],
16085
16194
  ...config,
16086
16195
  };
16087
16196
  this.reconnectDelay = this.config.reconnectDelayMs;
16197
+ this.runtimeToken = config.runtimeToken || null;
16088
16198
  }
16089
16199
  /**
16090
16200
  * Register a handler for a specific capability
@@ -16162,10 +16272,13 @@ class TaskWorker {
16162
16272
  }
16163
16273
  }
16164
16274
  /**
16165
- * Get API key from config or environment
16275
+ * Get runtime token for API calls
16166
16276
  */
16167
- getApiKey() {
16168
- return this.config.apiKey || (typeof process !== 'undefined' ? process.env.SEKUIRE_API_KEY : undefined);
16277
+ getToken() {
16278
+ if (!this.runtimeToken) {
16279
+ throw new Error("No runtime token available - call bootstrap first");
16280
+ }
16281
+ return this.runtimeToken;
16169
16282
  }
16170
16283
  /**
16171
16284
  * Connect to SSE stream
@@ -16175,8 +16288,7 @@ class TaskWorker {
16175
16288
  console.log(`[Worker] Connecting to SSE stream: ${url}`);
16176
16289
  this.eventSource = new EventSource$1(url, {
16177
16290
  headers: {
16178
- Authorization: `Bearer ${this.config.token}`,
16179
- ...(this.getApiKey() && { "X-API-Key": this.getApiKey() }),
16291
+ Authorization: `Bearer ${this.getToken()}`,
16180
16292
  },
16181
16293
  });
16182
16294
  this.eventSource.onopen = () => {
@@ -16268,8 +16380,7 @@ class TaskWorker {
16268
16380
  method: "POST",
16269
16381
  headers: {
16270
16382
  "Content-Type": "application/json",
16271
- Authorization: `Bearer ${this.config.token}`,
16272
- ...(this.getApiKey() && { "X-API-Key": this.getApiKey() }),
16383
+ Authorization: `Bearer ${this.getToken()}`,
16273
16384
  },
16274
16385
  body: JSON.stringify({ status, output, error }),
16275
16386
  });
@@ -16279,57 +16390,84 @@ class TaskWorker {
16279
16390
  }
16280
16391
  }
16281
16392
  /**
16282
- * Register this agent installation with Sekuire
16393
+ * Register this agent installation with Sekuire using install_token
16394
+ * Returns runtime_token for subsequent API calls
16283
16395
  */
16284
16396
  async bootstrap() {
16285
- const apiKey = this.getApiKey();
16286
- if (!apiKey || !this.config.agentId) {
16397
+ // If we already have a runtime token, skip bootstrap
16398
+ if (this.runtimeToken) {
16399
+ console.log("[Worker] Using provided runtime token, skipping bootstrap");
16287
16400
  return;
16288
16401
  }
16402
+ if (!this.config.installToken) {
16403
+ throw new Error("No install token provided for bootstrap");
16404
+ }
16289
16405
  const deploymentUrl = this.config.deploymentUrl || detectDeploymentUrl();
16290
16406
  try {
16291
16407
  const response = await fetch(`${this.config.apiBaseUrl}/api/v1/installations/bootstrap`, {
16292
16408
  method: "POST",
16293
16409
  headers: {
16294
16410
  "Content-Type": "application/json",
16295
- "X-API-Key": apiKey,
16296
16411
  },
16297
16412
  body: JSON.stringify({
16298
- agent_id: this.config.agentId,
16413
+ install_token: this.config.installToken,
16299
16414
  upstream_url: deploymentUrl,
16415
+ capabilities: this.config.capabilities || [],
16416
+ heartbeat_interval_seconds: Math.floor((this.config.heartbeatIntervalMs || 10000) / 1000),
16300
16417
  }),
16301
16418
  });
16302
- if (response.ok) {
16303
- const data = await response.json();
16304
- this.installationId = data.installation_id;
16305
- console.log(`[Worker] Registered installation: ${this.installationId}`);
16419
+ if (!response.ok) {
16420
+ const errorText = await response.text();
16421
+ throw new Error(`Bootstrap failed: ${response.status} - ${errorText}`);
16306
16422
  }
16423
+ const data = await response.json();
16424
+ this.installationId = data.installation_id;
16425
+ this.runtimeToken = data.runtime_token;
16426
+ this.refreshToken = data.refresh_token;
16427
+ this.expiresAt = data.expires_at
16428
+ ? new Date(data.expires_at).getTime()
16429
+ : null;
16430
+ console.log(`[Worker] Bootstrapped installation: ${this.installationId}`);
16307
16431
  }
16308
16432
  catch (e) {
16309
- console.warn("[Worker] Bootstrap registration failed:", e);
16433
+ console.error("[Worker] Bootstrap registration failed:", e);
16434
+ throw e;
16310
16435
  }
16311
16436
  }
16312
16437
  /**
16313
- * Start heartbeat loop
16438
+ * Start heartbeat/lease renewal loop
16314
16439
  */
16315
16440
  startHeartbeat() {
16316
16441
  this.heartbeatInterval = setInterval(async () => {
16442
+ if (!this.installationId) {
16443
+ return;
16444
+ }
16317
16445
  try {
16318
16446
  const deploymentUrl = this.config.deploymentUrl || detectDeploymentUrl();
16319
- const response = await fetch(`${this.config.apiBaseUrl}/api/v1/agents/heartbeat`, {
16447
+ const response = await fetch(`${this.config.apiBaseUrl}/api/v1/installations/${this.installationId}/lease`, {
16320
16448
  method: "POST",
16321
16449
  headers: {
16322
16450
  "Content-Type": "application/json",
16323
- Authorization: `Bearer ${this.config.token}`,
16324
- ...(this.getApiKey() && { "X-API-Key": this.getApiKey() }),
16451
+ Authorization: `Bearer ${this.getToken()}`,
16325
16452
  },
16326
16453
  body: JSON.stringify({
16327
- agent_id: this.config.agentId,
16328
- status: this.isPaused ? "paused" : "idle",
16329
- public_url: deploymentUrl,
16330
- load: 0.0,
16454
+ status: this.isPaused ? "stopped" : "running",
16455
+ upstream_url: deploymentUrl,
16456
+ heartbeat_interval_seconds: Math.floor((this.config.heartbeatIntervalMs || 10000) / 1000),
16331
16457
  }),
16332
16458
  });
16459
+ if (response.status === 401) {
16460
+ console.warn("[Worker] Runtime token expired, attempting refresh...");
16461
+ try {
16462
+ await this.refreshRuntimeToken();
16463
+ return; // Will retry on next interval
16464
+ }
16465
+ catch {
16466
+ console.error("[Worker] Token refresh failed, stopping worker");
16467
+ await this.stop();
16468
+ return;
16469
+ }
16470
+ }
16333
16471
  if (response.ok) {
16334
16472
  const data = await response.json();
16335
16473
  if (data.command) {
@@ -16338,10 +16476,34 @@ class TaskWorker {
16338
16476
  }
16339
16477
  }
16340
16478
  catch (e) {
16341
- console.warn("[Worker] Heartbeat failed:", e);
16479
+ console.warn("[Worker] Heartbeat/lease failed:", e);
16342
16480
  }
16343
16481
  }, this.config.heartbeatIntervalMs);
16344
16482
  }
16483
+ /**
16484
+ * Refresh the runtime token using the refresh token
16485
+ */
16486
+ async refreshRuntimeToken() {
16487
+ if (!this.installationId || !this.refreshToken) {
16488
+ throw new Error("Cannot refresh token - missing installation ID or refresh token");
16489
+ }
16490
+ const response = await fetch(`${this.config.apiBaseUrl}/api/v1/installations/${this.installationId}/refresh`, {
16491
+ method: "POST",
16492
+ headers: {
16493
+ "Content-Type": "application/json",
16494
+ },
16495
+ body: JSON.stringify({
16496
+ refresh_token: this.refreshToken,
16497
+ }),
16498
+ });
16499
+ if (!response.ok) {
16500
+ const body = await response.text();
16501
+ throw new Error(`Token refresh failed: ${response.status} - ${body}`);
16502
+ }
16503
+ const data = await response.json();
16504
+ this.runtimeToken = data.runtime_token;
16505
+ console.log("[Worker] Runtime token refreshed successfully");
16506
+ }
16345
16507
  }
16346
16508
  // ============================================================================
16347
16509
  // Convenience Export
@@ -38,4 +38,4 @@ export interface MemoryFactoryConfig {
38
38
  instance?: MemoryStorage;
39
39
  }
40
40
  export declare function createMemoryStorage(config: MemoryFactoryConfig): MemoryStorage;
41
- export { registerStorage, hasStorage, listStorageTypes, getStorageInfo, };
41
+ export { registerStorage, hasStorage, listStorageTypes, getStorageInfo };
@@ -73,24 +73,24 @@ export interface AgentSkill {
73
73
  /** A2A message with role and parts */
74
74
  export interface A2AMessage {
75
75
  /** Message role: "user" or "agent" */
76
- role: "user" | "agent";
76
+ role: 'user' | 'agent';
77
77
  /** Message content parts */
78
78
  parts: A2AMessagePart[];
79
79
  }
80
80
  /** Message part content */
81
81
  export type A2AMessagePart = {
82
- type: "text";
82
+ type: 'text';
83
83
  text: string;
84
84
  } | {
85
- type: "file";
85
+ type: 'file';
86
86
  uri: string;
87
87
  mimeType?: string;
88
88
  } | {
89
- type: "data";
89
+ type: 'data';
90
90
  data: unknown;
91
91
  };
92
92
  /** A2A task states */
93
- export type A2ATaskState = "pending" | "working" | "input-required" | "completed" | "failed" | "cancelled";
93
+ export type A2ATaskState = 'pending' | 'working' | 'input-required' | 'completed' | 'failed' | 'cancelled';
94
94
  /** A2A task status with timestamp */
95
95
  export interface A2ATaskStatus {
96
96
  state: A2ATaskState;
@@ -142,14 +142,14 @@ export interface A2ARouteResponse {
142
142
  }
143
143
  /** JSON-RPC 2.0 Request */
144
144
  export interface JsonRpcRequest {
145
- jsonrpc: "2.0";
145
+ jsonrpc: '2.0';
146
146
  method: string;
147
147
  id: string | number;
148
148
  params?: unknown;
149
149
  }
150
150
  /** JSON-RPC 2.0 Response */
151
151
  export interface JsonRpcResponse<T = unknown> {
152
- jsonrpc: "2.0";
152
+ jsonrpc: '2.0';
153
153
  id: string | number;
154
154
  result?: T;
155
155
  error?: JsonRpcError;
package/dist/worker.d.ts CHANGED
@@ -26,13 +26,14 @@ export interface TaskContext {
26
26
  export type TaskHandler = (ctx: TaskContext, input: Record<string, unknown>) => Promise<unknown>;
27
27
  export interface WorkerConfig {
28
28
  apiBaseUrl: string;
29
- token: string;
30
- agentId?: string;
31
- apiKey?: string;
29
+ agentId: string;
30
+ installToken?: string;
31
+ runtimeToken?: string;
32
32
  heartbeatIntervalMs?: number;
33
33
  reconnectDelayMs?: number;
34
34
  maxReconnectDelayMs?: number;
35
35
  deploymentUrl?: string;
36
+ capabilities?: string[];
36
37
  }
37
38
  export declare class TaskWorker {
38
39
  private eventSource;
@@ -43,6 +44,9 @@ export declare class TaskWorker {
43
44
  private isConnected;
44
45
  private isPaused;
45
46
  private installationId;
47
+ private runtimeToken;
48
+ private refreshToken;
49
+ private expiresAt;
46
50
  private onCommandCallback?;
47
51
  constructor(config: WorkerConfig);
48
52
  /**
@@ -73,9 +77,9 @@ export declare class TaskWorker {
73
77
  */
74
78
  private handleCommand;
75
79
  /**
76
- * Get API key from config or environment
80
+ * Get runtime token for API calls
77
81
  */
78
- private getApiKey;
82
+ private getToken;
79
83
  /**
80
84
  * Connect to SSE stream
81
85
  */
@@ -93,12 +97,17 @@ export declare class TaskWorker {
93
97
  */
94
98
  private completeTask;
95
99
  /**
96
- * Register this agent installation with Sekuire
100
+ * Register this agent installation with Sekuire using install_token
101
+ * Returns runtime_token for subsequent API calls
97
102
  */
98
103
  private bootstrap;
99
104
  /**
100
- * Start heartbeat loop
105
+ * Start heartbeat/lease renewal loop
101
106
  */
102
107
  private startHeartbeat;
108
+ /**
109
+ * Refresh the runtime token using the refresh token
110
+ */
111
+ private refreshRuntimeToken;
103
112
  }
104
113
  export declare function createWorker(config: WorkerConfig): TaskWorker;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sekuire/sdk",
3
- "version": "0.1.11",
3
+ "version": "0.1.12",
4
4
  "description": "Sekuire Identity Protocol SDK for TypeScript/JavaScript",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",