@realtimex/sdk 1.3.4 → 1.3.5-rc.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/dist/index.mjs CHANGED
@@ -228,6 +228,204 @@ var ActivitiesModule = class {
228
228
  }
229
229
  };
230
230
 
231
+ // src/modules/contract.ts
232
+ import { createHash, createHmac, randomUUID } from "crypto";
233
+ var LOCAL_APP_CONTRACT_VERSION = "local-app-contract/v1";
234
+ var CONTRACT_SIGNATURE_HEADER = "x-rtx-contract-signature";
235
+ var CONTRACT_EVENT_ID_HEADER = "x-rtx-event-id";
236
+ var CONTRACT_SIGNATURE_ALGORITHM = "sha256";
237
+ var CONTRACT_ATTEMPT_PREFIX = "run-";
238
+ var CONTRACT_EVENT_ALIASES = {
239
+ "trigger-agent": "task.trigger",
240
+ "task.trigger": "task.trigger",
241
+ ping: "system.ping",
242
+ "system.ping": "system.ping",
243
+ claim: "task.claimed",
244
+ claimed: "task.claimed",
245
+ "task.claimed": "task.claimed",
246
+ "task-start": "task.started",
247
+ start: "task.started",
248
+ "task.started": "task.started",
249
+ "task-progress": "task.progress",
250
+ progress: "task.progress",
251
+ processing: "task.progress",
252
+ "task.progress": "task.progress",
253
+ "task-complete": "task.completed",
254
+ complete: "task.completed",
255
+ completed: "task.completed",
256
+ "task.completed": "task.completed",
257
+ "task-fail": "task.failed",
258
+ fail: "task.failed",
259
+ failed: "task.failed",
260
+ "task.failed": "task.failed",
261
+ "task-cancel": "task.canceled",
262
+ "task-cancelled": "task.canceled",
263
+ "task-canceled": "task.canceled",
264
+ cancel: "task.canceled",
265
+ cancelled: "task.canceled",
266
+ canceled: "task.canceled",
267
+ "task.canceled": "task.canceled"
268
+ };
269
+ var CONTRACT_LEGACY_ACTIONS = {
270
+ "task.trigger": "trigger-agent",
271
+ "system.ping": "ping",
272
+ "task.claimed": "claim",
273
+ "task.started": "start",
274
+ "task.progress": "progress",
275
+ "task.completed": "complete",
276
+ "task.failed": "fail",
277
+ "task.canceled": "cancel"
278
+ };
279
+ function normalizeContractEvent(eventLike) {
280
+ if (!eventLike || typeof eventLike !== "string") return null;
281
+ const normalized = CONTRACT_EVENT_ALIASES[eventLike.trim().toLowerCase()];
282
+ return normalized || null;
283
+ }
284
+ function normalizeAttemptId(attemptLike) {
285
+ if (attemptLike === null || attemptLike === void 0) return void 0;
286
+ if (typeof attemptLike === "number" && Number.isInteger(attemptLike) && attemptLike > 0) {
287
+ return `${CONTRACT_ATTEMPT_PREFIX}${attemptLike}`;
288
+ }
289
+ if (typeof attemptLike !== "string") return void 0;
290
+ const trimmed = attemptLike.trim();
291
+ if (!trimmed) return void 0;
292
+ if (trimmed.startsWith(CONTRACT_ATTEMPT_PREFIX)) return trimmed;
293
+ if (/^\d+$/.test(trimmed)) return `${CONTRACT_ATTEMPT_PREFIX}${trimmed}`;
294
+ return trimmed;
295
+ }
296
+ function parseAttemptRunId(attemptLike) {
297
+ const attemptId = normalizeAttemptId(attemptLike);
298
+ if (!attemptId) return null;
299
+ const matched = attemptId.match(/^run[-_:]?(\d+)$/i);
300
+ if (!matched) return null;
301
+ const value = Number(matched[1]);
302
+ return Number.isInteger(value) && value > 0 ? value : null;
303
+ }
304
+ function hashContractPayload(payload) {
305
+ const normalized = payload && typeof payload === "object" ? payload : { value: payload ?? null };
306
+ return createHash("sha256").update(JSON.stringify(normalized)).digest("hex");
307
+ }
308
+ function createContractEventId() {
309
+ return randomUUID();
310
+ }
311
+ function buildContractSignatureMessage({
312
+ eventId,
313
+ eventType,
314
+ taskId,
315
+ attemptId,
316
+ timestamp,
317
+ payload
318
+ }) {
319
+ return [
320
+ String(eventId || ""),
321
+ String(normalizeContractEvent(String(eventType || "")) || eventType || ""),
322
+ String(taskId || ""),
323
+ String(normalizeAttemptId(attemptId) || ""),
324
+ String(timestamp || ""),
325
+ hashContractPayload(payload ?? {})
326
+ ].join(".");
327
+ }
328
+ function signContractEvent(input) {
329
+ const signatureMessage = buildContractSignatureMessage(input);
330
+ const digest = createHmac(CONTRACT_SIGNATURE_ALGORITHM, input.secret).update(signatureMessage).digest("hex");
331
+ return `${CONTRACT_SIGNATURE_ALGORITHM}=${digest}`;
332
+ }
333
+ function canonicalEventToLegacyAction(eventLike) {
334
+ const normalized = normalizeContractEvent(eventLike);
335
+ if (!normalized) return null;
336
+ return CONTRACT_LEGACY_ACTIONS[normalized] || null;
337
+ }
338
+ function buildContractIdempotencyKey({
339
+ taskId,
340
+ eventType,
341
+ eventId,
342
+ attemptId,
343
+ machineId,
344
+ timestamp,
345
+ payload
346
+ }) {
347
+ const canonicalEvent = normalizeContractEvent(eventType) || eventType;
348
+ if (eventId) {
349
+ const eventToken = createHash("sha256").update(String(eventId)).digest("hex");
350
+ return `${taskId}:${canonicalEvent}:event:${eventToken}`;
351
+ }
352
+ const hashInput = {
353
+ task_id: taskId,
354
+ event_type: canonicalEvent,
355
+ attempt_id: normalizeAttemptId(attemptId),
356
+ machine_id: machineId || null,
357
+ timestamp: timestamp || null,
358
+ payload_hash: hashContractPayload(payload ?? {})
359
+ };
360
+ const token = createHash("sha256").update(JSON.stringify(hashInput)).digest("hex");
361
+ return `${taskId}:${canonicalEvent}:hash:${token}`;
362
+ }
363
+ var ContractModule = class {
364
+ constructor(realtimexUrl, appName, appId, apiKey) {
365
+ this.cachedContract = null;
366
+ this.realtimexUrl = realtimexUrl.replace(/\/$/, "");
367
+ this.appName = appName;
368
+ this.appId = appId;
369
+ this.apiKey = apiKey;
370
+ }
371
+ async requestPermission(permission) {
372
+ try {
373
+ const response = await fetch(`${this.realtimexUrl}/api/local-apps/request-permission`, {
374
+ method: "POST",
375
+ headers: { "Content-Type": "application/json" },
376
+ body: JSON.stringify({
377
+ app_id: this.appId,
378
+ app_name: this.appName,
379
+ permission
380
+ })
381
+ });
382
+ const data = await response.json();
383
+ return data.granted === true;
384
+ } catch {
385
+ return false;
386
+ }
387
+ }
388
+ async request(path) {
389
+ const url = `${this.realtimexUrl}${path}`;
390
+ const headers = {
391
+ "Content-Type": "application/json"
392
+ };
393
+ if (this.apiKey) headers.Authorization = `Bearer ${this.apiKey}`;
394
+ if (this.appId) headers["x-app-id"] = this.appId;
395
+ const response = await fetch(url, {
396
+ method: "GET",
397
+ headers
398
+ });
399
+ const data = await response.json();
400
+ if (response.status === 403) {
401
+ const errorCode = data.error;
402
+ const permission = data.permission;
403
+ const message = data.message;
404
+ if (errorCode === "PERMISSION_REQUIRED" && permission) {
405
+ const granted = await this.requestPermission(permission);
406
+ if (granted) return this.request(path);
407
+ throw new PermissionDeniedError(permission, message);
408
+ }
409
+ if (errorCode === "PERMISSION_DENIED") {
410
+ throw new PermissionDeniedError(permission, message);
411
+ }
412
+ }
413
+ if (!response.ok) {
414
+ throw new Error(data.error || `Request failed: ${response.status}`);
415
+ }
416
+ return data;
417
+ }
418
+ async getLocalAppV1(forceRefresh = false) {
419
+ if (!forceRefresh && this.cachedContract) return this.cachedContract;
420
+ const data = await this.request("/contracts/local-app/v1");
421
+ this.cachedContract = data.contract;
422
+ return data.contract;
423
+ }
424
+ clearCache() {
425
+ this.cachedContract = null;
426
+ }
427
+ };
428
+
231
429
  // src/modules/webhook.ts
232
430
  var WebhookModule = class {
233
431
  constructor(realtimexUrl, appName, appId, apiKey) {
@@ -304,7 +502,9 @@ var WebhookModule = class {
304
502
  body: JSON.stringify({
305
503
  app_name: this.appName,
306
504
  app_id: this.appId,
307
- event: "trigger-agent",
505
+ event: "task.trigger",
506
+ event_id: payload.event_id || createContractEventId(),
507
+ attempt_id: normalizeAttemptId(payload.attempt_id),
308
508
  payload: {
309
509
  raw_data: payload.raw_data,
310
510
  auto_run: payload.auto_run ?? false,
@@ -322,7 +522,8 @@ var WebhookModule = class {
322
522
  body: JSON.stringify({
323
523
  app_name: this.appName,
324
524
  app_id: this.appId,
325
- event: "ping"
525
+ event: "system.ping",
526
+ event_id: createContractEventId()
326
527
  })
327
528
  });
328
529
  }
@@ -335,49 +536,148 @@ var TaskModule = class {
335
536
  this.appName = appName;
336
537
  this.appId = appId;
337
538
  this.apiKey = apiKey;
539
+ this.callbackSecret = process.env.RTX_CONTRACT_CALLBACK_SECRET;
540
+ this.signCallbacksByDefault = process.env.RTX_CONTRACT_SIGN_CALLBACKS === "true";
338
541
  }
339
542
  /**
340
- * Mark task as processing
543
+ * Configure callback signing behavior.
341
544
  */
342
- async start(taskUuid, machineId) {
343
- return this._sendEvent("task-start", taskUuid, { machine_id: machineId });
545
+ configureContract(config) {
546
+ if (typeof config.callbackSecret === "string") {
547
+ this.callbackSecret = config.callbackSecret;
548
+ }
549
+ if (typeof config.signCallbacksByDefault === "boolean") {
550
+ this.signCallbacksByDefault = config.signCallbacksByDefault;
551
+ }
344
552
  }
345
553
  /**
346
- * Mark task as completed with result
554
+ * Claim a task before processing.
347
555
  */
348
- async complete(taskUuid, result, machineId) {
349
- return this._sendEvent("task-complete", taskUuid, { result, machine_id: machineId });
556
+ async claim(taskUuid, options = {}) {
557
+ return this._sendEvent("task.claimed", taskUuid, {}, options);
350
558
  }
351
559
  /**
352
- * Mark task as failed with error
560
+ * Alias for claim()
353
561
  */
354
- async fail(taskUuid, error, machineId) {
355
- return this._sendEvent("task-fail", taskUuid, { error, machine_id: machineId });
562
+ async claimed(taskUuid, options = {}) {
563
+ return this.claim(taskUuid, options);
356
564
  }
357
- async _sendEvent(event, taskUuid, extra) {
565
+ /**
566
+ * Mark task as processing.
567
+ * Backward compatible signature: start(taskUuid, machineId?)
568
+ */
569
+ async start(taskUuid, machineIdOrOptions) {
570
+ return this._sendEvent("task.started", taskUuid, {}, this._normalizeOptions(machineIdOrOptions));
571
+ }
572
+ /**
573
+ * Report incremental task progress.
574
+ */
575
+ async progress(taskUuid, progressData = {}, options = {}) {
576
+ return this._sendEvent("task.progress", taskUuid, progressData, options);
577
+ }
578
+ /**
579
+ * Mark task as completed with result.
580
+ * Backward compatible signature: complete(taskUuid, result?, machineId?)
581
+ */
582
+ async complete(taskUuid, result = {}, machineIdOrOptions) {
583
+ return this._sendEvent("task.completed", taskUuid, { result }, this._normalizeOptions(machineIdOrOptions));
584
+ }
585
+ /**
586
+ * Mark task as failed with error.
587
+ * Backward compatible signature: fail(taskUuid, error, machineId?)
588
+ */
589
+ async fail(taskUuid, error, machineIdOrOptions) {
590
+ return this._sendEvent("task.failed", taskUuid, { error }, this._normalizeOptions(machineIdOrOptions));
591
+ }
592
+ /**
593
+ * Mark task as canceled.
594
+ */
595
+ async cancel(taskUuid, reason, options = {}) {
596
+ const payload = reason ? { error: reason } : {};
597
+ return this._sendEvent("task.canceled", taskUuid, payload, options);
598
+ }
599
+ _normalizeOptions(machineIdOrOptions) {
600
+ if (!machineIdOrOptions) return {};
601
+ if (typeof machineIdOrOptions === "string") {
602
+ return { machineId: machineIdOrOptions };
603
+ }
604
+ return machineIdOrOptions;
605
+ }
606
+ async _sendEvent(event, taskUuid, eventData = {}, options = {}) {
607
+ if (!taskUuid || !taskUuid.trim()) {
608
+ throw new Error("taskUuid is required");
609
+ }
610
+ const attemptId = normalizeAttemptId(options.attemptId);
611
+ const timestamp = options.timestamp || (/* @__PURE__ */ new Date()).toISOString();
612
+ const eventId = options.eventId || createContractEventId();
613
+ const callbackUrl = options.callbackUrl;
614
+ const targetUrl = callbackUrl || `${this.realtimexUrl}/webhooks/realtimex`;
615
+ const sendingToMainWebhook = !callbackUrl;
616
+ const includeAppAuth = sendingToMainWebhook || targetUrl.startsWith(this.realtimexUrl);
617
+ const payloadData = eventData && typeof eventData === "object" ? eventData : {};
358
618
  const headers = { "Content-Type": "application/json" };
359
- if (this.apiKey) {
360
- headers["Authorization"] = `Bearer ${this.apiKey}`;
619
+ headers[CONTRACT_EVENT_ID_HEADER] = eventId;
620
+ if (includeAppAuth) {
621
+ if (this.apiKey) headers.Authorization = `Bearer ${this.apiKey}`;
622
+ if (this.appId) headers["x-app-id"] = this.appId;
361
623
  }
362
- if (this.appId) {
363
- headers["x-app-id"] = this.appId;
624
+ const callbackSecret = options.callbackSecret || this.callbackSecret;
625
+ const shouldSign = options.sign ?? this.signCallbacksByDefault;
626
+ if (shouldSign) {
627
+ if (!callbackSecret) {
628
+ throw new Error(
629
+ "Callback signing is enabled but no callbackSecret is configured. Use task.configureContract({ callbackSecret }) or pass options.callbackSecret."
630
+ );
631
+ }
632
+ headers[CONTRACT_SIGNATURE_HEADER] = signContractEvent({
633
+ secret: callbackSecret,
634
+ eventId,
635
+ eventType: event,
636
+ taskId: taskUuid,
637
+ attemptId,
638
+ timestamp,
639
+ payload: payloadData
640
+ });
364
641
  }
365
- const response = await fetch(`${this.realtimexUrl}/webhooks/realtimex`, {
642
+ const requestBody = sendingToMainWebhook ? {
643
+ app_name: this.appName,
644
+ app_id: this.appId,
645
+ event,
646
+ event_id: eventId,
647
+ attempt_id: attemptId,
648
+ payload: {
649
+ task_uuid: taskUuid,
650
+ machine_id: options.machineId,
651
+ timestamp,
652
+ attempt_id: attemptId,
653
+ ...payloadData
654
+ }
655
+ } : {
656
+ event,
657
+ action: canonicalEventToLegacyAction(event),
658
+ event_id: eventId,
659
+ attempt_id: attemptId,
660
+ machine_id: options.machineId,
661
+ user_email: options.userEmail,
662
+ activity_id: options.activityId,
663
+ table_name: options.tableName,
664
+ timestamp,
665
+ data: payloadData
666
+ };
667
+ const response = await fetch(targetUrl, {
366
668
  method: "POST",
367
669
  headers,
368
- body: JSON.stringify({
369
- app_name: this.appName,
370
- app_id: this.appId,
371
- event,
372
- payload: {
373
- task_uuid: taskUuid,
374
- ...extra
375
- }
376
- })
670
+ body: JSON.stringify(requestBody)
377
671
  });
378
- const data = await response.json();
379
- if (!response.ok) throw new Error(data.error || `Failed to ${event}`);
380
- return data;
672
+ const responseData = await response.json();
673
+ if (!response.ok) throw new Error(responseData.error || `Failed to ${event}`);
674
+ return {
675
+ ...responseData,
676
+ task_uuid: responseData.task_uuid || responseData.task_id || taskUuid,
677
+ event_id: responseData.event_id || eventId,
678
+ attempt_id: responseData.attempt_id || attemptId,
679
+ event_type: responseData.event_type || event
680
+ };
381
681
  }
382
682
  };
383
683
 
@@ -1429,12 +1729,16 @@ var _RealtimeXSDK = class _RealtimeXSDK {
1429
1729
  this.webhook = new WebhookModule(this.realtimexUrl, this.appName, this.appId, this.apiKey);
1430
1730
  this.api = new ApiModule(this.realtimexUrl, this.appId, this.appName, this.apiKey);
1431
1731
  this.task = new TaskModule(this.realtimexUrl, this.appName, this.appId, this.apiKey);
1732
+ if (config.contract) {
1733
+ this.task.configureContract(config.contract);
1734
+ }
1432
1735
  this.port = new PortModule(config.defaultPort);
1433
1736
  this.llm = new LLMModule(this.realtimexUrl, this.appId, this.appName, this.apiKey);
1434
1737
  this.tts = new TTSModule(this.realtimexUrl, this.appId, this.appName, this.apiKey);
1435
1738
  this.stt = new STTModule(this.realtimexUrl, this.appId, this.appName, this.apiKey);
1436
1739
  this.agent = new AgentModule(this.httpClient);
1437
1740
  this.mcp = new MCPModule(this.realtimexUrl, this.appId, this.appName, this.apiKey);
1741
+ this.contract = new ContractModule(this.realtimexUrl, this.appName, this.appId, this.apiKey);
1438
1742
  if (this.permissions.length > 0 && this.appId && !this.apiKey) {
1439
1743
  this.register().catch((err) => {
1440
1744
  console.error("[RealtimeX SDK] Auto-registration failed:", err.message);
@@ -1538,9 +1842,15 @@ export {
1538
1842
  ActivitiesModule,
1539
1843
  AgentModule,
1540
1844
  ApiModule,
1845
+ CONTRACT_ATTEMPT_PREFIX,
1846
+ CONTRACT_EVENT_ID_HEADER,
1847
+ CONTRACT_SIGNATURE_ALGORITHM,
1848
+ CONTRACT_SIGNATURE_HEADER,
1849
+ ContractModule,
1541
1850
  LLMModule,
1542
1851
  LLMPermissionError,
1543
1852
  LLMProviderError,
1853
+ LOCAL_APP_CONTRACT_VERSION,
1544
1854
  MCPModule,
1545
1855
  PermissionDeniedError,
1546
1856
  PermissionRequiredError,
@@ -1550,5 +1860,14 @@ export {
1550
1860
  TTSModule,
1551
1861
  TaskModule,
1552
1862
  VectorStore,
1553
- WebhookModule
1863
+ WebhookModule,
1864
+ buildContractIdempotencyKey,
1865
+ buildContractSignatureMessage,
1866
+ canonicalEventToLegacyAction,
1867
+ createContractEventId,
1868
+ hashContractPayload,
1869
+ normalizeAttemptId,
1870
+ normalizeContractEvent,
1871
+ parseAttemptRunId,
1872
+ signContractEvent
1554
1873
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@realtimex/sdk",
3
- "version": "1.3.4",
3
+ "version": "1.3.5-rc.1",
4
4
  "description": "SDK for building Local Apps that integrate with RealtimeX",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -16,6 +16,7 @@
16
16
  "build": "tsup src/index.ts --format cjs,esm --dts --clean",
17
17
  "dev": "tsup src/index.ts --format cjs,esm --dts --watch",
18
18
  "test": "vitest run",
19
+ "contract:verify": "node ../scripts/verify-contract-compat.mjs",
19
20
  "prepublishOnly": "npm run build"
20
21
  },
21
22
  "keywords": [