@stacklink/sdk 0.2.2 → 0.3.8

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.
Files changed (37) hide show
  1. package/README.md +50 -1
  2. package/compatibility.json +3 -1
  3. package/dist/src/domain/bot-contract.d.ts +4336 -0
  4. package/dist/src/domain/bot-contract.d.ts.map +1 -0
  5. package/dist/src/domain/bot-contract.js +382 -0
  6. package/dist/src/domain/bot-contract.js.map +1 -0
  7. package/dist/src/domain/memory-contract.d.ts +452 -452
  8. package/dist/src/domain/memory-contract.js +1 -1
  9. package/dist/src/domain/memory-contract.js.map +1 -1
  10. package/dist/src/domain/platform-approval-contract.d.ts +508 -0
  11. package/dist/src/domain/platform-approval-contract.d.ts.map +1 -0
  12. package/dist/src/domain/platform-approval-contract.js +77 -0
  13. package/dist/src/domain/platform-approval-contract.js.map +1 -0
  14. package/dist/src/domain/provider-branding.d.ts.map +1 -1
  15. package/dist/src/domain/provider-branding.js +17 -0
  16. package/dist/src/domain/provider-branding.js.map +1 -1
  17. package/dist/src/domain/stacklink-meta-tools.d.ts +2 -2
  18. package/dist/src/domain/stacklink-meta-tools.d.ts.map +1 -1
  19. package/dist/src/domain/stacklink-meta-tools.js +15 -4
  20. package/dist/src/domain/stacklink-meta-tools.js.map +1 -1
  21. package/dist/src/sdk/framework-tools.d.ts +21 -0
  22. package/dist/src/sdk/framework-tools.d.ts.map +1 -1
  23. package/dist/src/sdk/framework-tools.js +49 -0
  24. package/dist/src/sdk/framework-tools.js.map +1 -1
  25. package/dist/src/sdk/index.d.ts +225 -6
  26. package/dist/src/sdk/index.d.ts.map +1 -1
  27. package/dist/src/sdk/index.js +215 -21
  28. package/dist/src/sdk/index.js.map +1 -1
  29. package/dist/src/sdk/vercel-ai.d.ts +4 -0
  30. package/dist/src/sdk/vercel-ai.d.ts.map +1 -1
  31. package/dist/src/sdk/vercel-ai.js +13 -4
  32. package/dist/src/sdk/vercel-ai.js.map +1 -1
  33. package/dist/src/sdk/x-chat.d.ts +143 -0
  34. package/dist/src/sdk/x-chat.d.ts.map +1 -0
  35. package/dist/src/sdk/x-chat.js +160 -0
  36. package/dist/src/sdk/x-chat.js.map +1 -0
  37. package/package.json +15 -3
@@ -1,6 +1,7 @@
1
+ import { botApiSignaturePayload } from '../domain/bot-contract.js';
1
2
  import { materializeStacklinkWorkbenchExecutionResponse, } from '../domain/stacklink-meta-tools.js';
2
3
  export { StacklinkMemoryLifecycle, StacklinkMemoryLifecycleError, createStacklinkMemoryLifecycle, serializeCompletedConversationTurn, } from './memory-lifecycle.js';
3
- export const stacklinkTypeScriptSdkVersion = '0.2.2';
4
+ export const stacklinkTypeScriptSdkVersion = '0.3.8';
4
5
  const workbenchReusableRunComparisonWaitMs = 120_000;
5
6
  const workbenchReusableRunComparisonPollMs = 1_000;
6
7
  export class StacklinkApiError extends Error {
@@ -140,6 +141,39 @@ export async function verifyWebhookSignature(input) {
140
141
  }
141
142
  return false;
142
143
  }
144
+ /** Sign the exact JSON bytes sent to a saved inbound Bot webhook. Reuse requestId when retrying. */
145
+ export async function signBotTriggerRequest(input) {
146
+ const cryptoImpl = globalThis.crypto ?? (await import('node:crypto')).webcrypto;
147
+ const timestamp = String(Math.floor((input.now ?? new Date()).getTime() / 1000));
148
+ const requestId = input.requestId ?? cryptoImpl.randomUUID();
149
+ const digest = await cryptoImpl.subtle.digest('SHA-256', utf8(input.body));
150
+ const digestHex = Array.from(new Uint8Array(digest), value => value.toString(16).padStart(2, '0')).join('');
151
+ const key = await cryptoImpl.subtle.importKey('raw', utf8(input.secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
152
+ const signature = await cryptoImpl.subtle.sign('HMAC', key, utf8(JSON.stringify(['stacklink-trigger-v1', input.triggerId, timestamp, requestId, digestHex])));
153
+ return { 'content-type': 'application/json', 'stacklink-timestamp': timestamp, 'stacklink-request-id': requestId, 'stacklink-signature': Array.from(new Uint8Array(signature), value => value.toString(16).padStart(2, '0')).join('') };
154
+ }
155
+ /** Verify the original request bytes before using identity. consumeRequestId must atomically reject replays. */
156
+ export async function verifyBotApiRequest(input) {
157
+ const headers = new Headers(input.headers);
158
+ const get = (name) => headers.get(`x-stacklink-${name}`) ?? '';
159
+ const keyId = get('key-id'), timestamp = get('timestamp'), requestId = get('request-id');
160
+ const key = input.keys[keyId], projectId = get('project'), tenantId = get('tenant'), userId = get('customer');
161
+ const secret = typeof key === 'string' ? key : key?.secret;
162
+ const seconds = Number(timestamp), now = Math.floor((input.now ?? new Date()).getTime() / 1000), tolerance = input.toleranceSeconds ?? 300;
163
+ if (key && typeof key !== 'string' && key.validUntil !== null && (!Number.isFinite(Date.parse(key.validUntil)) || Date.parse(key.validUntil) <= now * 1000))
164
+ return null;
165
+ if (typeof secret !== 'string' || !secret || !tenantId || !requestId || projectId !== input.projectId || !Number.isInteger(seconds) || Math.abs(now - seconds) > tolerance)
166
+ return null;
167
+ const cryptoImpl = globalThis.crypto ?? (await import('node:crypto')).webcrypto;
168
+ const digest = await cryptoImpl.subtle.digest('SHA-256', utf8(input.body));
169
+ const bodySha256 = Array.from(new Uint8Array(digest), byte => byte.toString(16).padStart(2, '0')).join('');
170
+ const expected = await hmacSha256Base64(secret, botApiSignaturePayload({ method: input.method, url: input.url, requestId, timestamp, keyId, projectId, tenantId, userId, bodySha256 }));
171
+ if (!constantTimeStringEqual(get('signature'), `v1,${expected}`))
172
+ return null;
173
+ if (!await input.consumeRequestId(requestId, new Date((seconds + tolerance) * 1000)))
174
+ return null;
175
+ return { tenantId, userId: userId || null, requestId };
176
+ }
143
177
  function signatureCandidates(header) {
144
178
  const candidates = [];
145
179
  for (const entry of header.split(' ')) {
@@ -194,6 +228,8 @@ export class Stacklink {
194
228
  discoveryRequests = new Map();
195
229
  timeoutMs;
196
230
  maxAttempts;
231
+ bots;
232
+ approvals;
197
233
  webhooks;
198
234
  triggers;
199
235
  sessions;
@@ -220,6 +256,8 @@ export class Stacklink {
220
256
  searchTtlMs: options.cache?.searchTtlMs ?? 120_000,
221
257
  maxEntries: options.cache?.maxEntries ?? 500,
222
258
  };
259
+ this.bots = new StacklinkBotsClient(this);
260
+ this.approvals = new StacklinkApprovalsClient(this);
223
261
  this.webhooks = new StacklinkWebhooksClient(this);
224
262
  this.triggers = new StacklinkTriggersClient(this);
225
263
  this.sessions = new StacklinkSessionsClient(this);
@@ -697,7 +735,7 @@ export class Stacklink {
697
735
  const executionKind = accepted.executionKind === 'workbench_code' || accepted.executionKind === 'workbench_bash'
698
736
  ? accepted.executionKind
699
737
  : undefined;
700
- if (!sessionId || !executionId || !executionKind)
738
+ if (!sessionId || !executionId || !executionKind || accepted.status === 'waiting_for_approval')
701
739
  return accepted;
702
740
  while (true) {
703
741
  const waited = await this.waitForExecution({
@@ -707,7 +745,7 @@ export class Stacklink {
707
745
  waitTimeoutMs: 10_000,
708
746
  }, options);
709
747
  const execution = isSdkRecord(waited.execution) ? waited.execution : undefined;
710
- if (execution && isTerminalSdkExecution(execution)) {
748
+ if (execution && (isTerminalSdkExecution(execution) || execution.status === 'waiting_for_approval')) {
711
749
  return materializeStacklinkWorkbenchExecutionResponse(accepted, execution);
712
750
  }
713
751
  }
@@ -745,6 +783,56 @@ export class Stacklink {
745
783
  signal: options.signal,
746
784
  });
747
785
  }
786
+ async *streamBotEvents(botId, runId, options = {}) {
787
+ let cursor = options.after ?? '0';
788
+ const deadline = Date.now() + positiveSdkInteger(options.totalTimeoutMs ?? 300_000, 'totalTimeoutMs');
789
+ const terminal = (event) => ['run.completed', 'run.failed', 'run.stopped'].includes(String(event.type));
790
+ while (!options.signal?.aborted) {
791
+ const remaining = deadline - Date.now();
792
+ if (remaining <= 0)
793
+ throw new StacklinkApiError('Bot observation timed out. The same run may still be active; resume with the last sequence.', 408, 'sdk_stream_timeout', { runId, after: cursor });
794
+ try {
795
+ const response = await this.requestRaw(`/v1/bots/${encodeURIComponent(botId)}/runs/${encodeURIComponent(runId)}/events/stream`, { headers: { accept: 'text/event-stream', 'Last-Event-ID': cursor }, signal: options.signal, timeoutMs: Math.min(options.timeoutMs ?? 30_000, remaining) });
796
+ for await (const event of this.readJsonEventStream(response)) {
797
+ if (!isSdkRecord(event) || typeof event.sequence !== 'string')
798
+ continue;
799
+ if (event.runId !== runId)
800
+ throw new StacklinkApiError('Bot event belongs to a different run.', 502, 'bot_event_invalid');
801
+ if (BigInt(event.sequence) <= BigInt(cursor))
802
+ continue;
803
+ cursor = event.sequence;
804
+ yield event;
805
+ if (terminal(event))
806
+ return;
807
+ }
808
+ }
809
+ catch (error) {
810
+ if (options.signal?.aborted)
811
+ throw options.signal.reason ?? error;
812
+ const retryable = error instanceof TypeError || (error instanceof Error && ['AbortError', 'TimeoutError'].includes(error.name))
813
+ || (error instanceof StacklinkApiError && [408, 429, 502, 503, 504].includes(error.statusCode) && error.code !== 'bot_event_invalid');
814
+ if (!retryable)
815
+ throw error;
816
+ }
817
+ // Fetch the durable tail before considering completion. A status read alone
818
+ // could miss the final text and terminal event that arrived during a disconnect.
819
+ const page = await this.bots.events(botId, runId, cursor);
820
+ for (const event of page.events) {
821
+ if (BigInt(event.sequence) <= BigInt(cursor))
822
+ continue;
823
+ cursor = event.sequence;
824
+ yield event;
825
+ if (terminal(event))
826
+ return;
827
+ }
828
+ if (page.terminal && page.events.length === 0)
829
+ return;
830
+ if (page.events.length < 200)
831
+ await sdkDelay(Math.min(1000, Math.max(1, deadline - Date.now())), options.signal ?? AbortSignal.timeout(Math.max(1, deadline - Date.now())));
832
+ }
833
+ if (options.signal?.aborted)
834
+ throw options.signal.reason;
835
+ }
748
836
  async *streamExecutionEvents(input, options = {}) {
749
837
  let afterSequence = input.afterSequence === undefined ? undefined : String(input.afterSequence);
750
838
  while (!options.signal?.aborted) {
@@ -849,6 +937,12 @@ export class Stacklink {
849
937
  async getConnectedAccount(accountId) {
850
938
  return this.request(`/v1/connections/${accountId}`);
851
939
  }
940
+ async setConnectedAccountCredentialProfile(accountId, profileKey, credentials) {
941
+ return this.request(`/v1/connections/${encodeURIComponent(accountId)}/credential-profiles/${encodeURIComponent(profileKey)}`, { method: 'PUT', body: { credentials } });
942
+ }
943
+ async removeConnectedAccountCredentialProfile(accountId, profileKey) {
944
+ return this.request(`/v1/connections/${encodeURIComponent(accountId)}/credential-profiles/${encodeURIComponent(profileKey)}`, { method: 'DELETE' });
945
+ }
852
946
  async listConnectedAccountTools(accountId, input) {
853
947
  return this.request(`/v1/connections/${accountId}/tools`, {
854
948
  query: {
@@ -1609,29 +1703,35 @@ export class Stacklink {
1609
1703
  }
1610
1704
  return undefined;
1611
1705
  };
1612
- while (true) {
1613
- const read = await reader.read();
1614
- if (read.done)
1615
- break;
1616
- buffer += decoder.decode(read.value, { stream: true });
1617
- let index = buffer.indexOf('\n');
1618
- while (index >= 0) {
1619
- const payload = payloadForLine(buffer.slice(0, index));
1706
+ try {
1707
+ while (true) {
1708
+ const read = await reader.read();
1709
+ if (read.done)
1710
+ break;
1711
+ buffer += decoder.decode(read.value, { stream: true });
1712
+ let index = buffer.indexOf('\n');
1713
+ while (index >= 0) {
1714
+ const payload = payloadForLine(buffer.slice(0, index));
1715
+ if (payload !== undefined)
1716
+ yield payload;
1717
+ buffer = buffer.slice(index + 1);
1718
+ index = buffer.indexOf('\n');
1719
+ }
1720
+ }
1721
+ buffer += decoder.decode();
1722
+ if (buffer.length > 0) {
1723
+ const payload = payloadForLine(buffer);
1620
1724
  if (payload !== undefined)
1621
1725
  yield payload;
1622
- buffer = buffer.slice(index + 1);
1623
- index = buffer.indexOf('\n');
1624
1726
  }
1727
+ const finalPayload = payloadForLine('');
1728
+ if (finalPayload !== undefined)
1729
+ yield finalPayload;
1625
1730
  }
1626
- buffer += decoder.decode();
1627
- if (buffer.length > 0) {
1628
- const payload = payloadForLine(buffer);
1629
- if (payload !== undefined)
1630
- yield payload;
1731
+ finally {
1732
+ await reader.cancel().catch(() => undefined);
1733
+ reader.releaseLock();
1631
1734
  }
1632
- const finalPayload = payloadForLine('');
1633
- if (finalPayload !== undefined)
1634
- yield finalPayload;
1635
1735
  }
1636
1736
  requestHeaders(registration, extraHeaders = {}) {
1637
1737
  if (registration) {
@@ -1931,6 +2031,55 @@ export class StacklinkMemoryClient {
1931
2031
  return this.client.request(`/v1/memory/legal-holds/${holdId}/release`, { method: 'POST' });
1932
2032
  }
1933
2033
  }
2034
+ export class StacklinkApprovalsClient {
2035
+ client;
2036
+ constructor(client) {
2037
+ this.client = client;
2038
+ }
2039
+ /** Server-only access. Identity comes from the authenticated application session. */
2040
+ getCustomerGrant(sessionId) {
2041
+ return this.client.request('/v1/approvals/customer-grant', { query: { sessionId } });
2042
+ }
2043
+ saveCustomerGrant(sessionId, grant) {
2044
+ return this.client.request('/v1/approvals/customer-grant', { method: 'PUT', query: { sessionId }, body: grant });
2045
+ }
2046
+ removeCustomerGrant(sessionId, revision) {
2047
+ return this.client.request('/v1/approvals/customer-grant', { method: 'DELETE', query: { sessionId }, body: { revision } });
2048
+ }
2049
+ list(input) {
2050
+ return this.client.request('/v1/approvals', { query: { ...input } });
2051
+ }
2052
+ get(id, access) {
2053
+ return this.client.request(`/v1/approvals/${encodeURIComponent(id)}`, { query: { ...access } });
2054
+ }
2055
+ /** Decide only after displaying the exact proposal and checking permissions. Never resubmit the original action. */
2056
+ decide(id, access, decision) {
2057
+ return this.client.request(`/v1/approvals/${encodeURIComponent(id)}/decide`, { method: 'POST', query: { ...access }, body: decision });
2058
+ }
2059
+ change(id, access, change) {
2060
+ return this.client.request(`/v1/approvals/${encodeURIComponent(id)}/change`, { method: 'POST', query: { ...access }, body: change });
2061
+ }
2062
+ events(access, after = '0', options = {}) {
2063
+ return this.client.request('/v1/approvals/events', { ...options, query: { ...access, after } });
2064
+ }
2065
+ /** Durable cursor observation. Save each sequence and pass it as `after` when reconnecting. */
2066
+ async *observe(access, options = {}) {
2067
+ let cursor = options.after ?? '0';
2068
+ const deadline = Date.now() + (options.timeoutMs ?? 300_000);
2069
+ const signal = options.signal ?? AbortSignal.timeout(Math.max(1, deadline - Date.now()));
2070
+ while (!signal.aborted && Date.now() < deadline) {
2071
+ const page = await this.events(access, cursor, { signal, timeoutMs: Math.min(15_000, Math.max(1, deadline - Date.now())) });
2072
+ for (const event of page.events) {
2073
+ if (BigInt(event.sequence) <= BigInt(cursor))
2074
+ continue;
2075
+ cursor = event.sequence;
2076
+ yield event;
2077
+ }
2078
+ if (page.events.length < 100)
2079
+ await sdkDelay(Math.min(1000, Math.max(1, deadline - Date.now())), signal);
2080
+ }
2081
+ }
2082
+ }
1934
2083
  export class StacklinkSessionsClient {
1935
2084
  client;
1936
2085
  constructor(client) {
@@ -2988,4 +3137,49 @@ function validateSdkMcpResponse(value, requestId) {
2988
3137
  throw new StacklinkApiError('Stacklink returned an invalid or unrelated MCP response.', 502, 'mcp_response_invalid');
2989
3138
  }
2990
3139
  }
3140
+ /** Hosted bots share durable runs across the API, MCP and dashboard. */
3141
+ export class StacklinkBotsClient {
3142
+ client;
3143
+ constructor(client) {
3144
+ this.client = client;
3145
+ }
3146
+ apis() { return this.client.request('/v1/bots/apis'); }
3147
+ updateApi(apiId, revision, api) { return this.client.request(`/v1/bots/apis/${encodeURIComponent(apiId)}`, { method: 'PATCH', body: { revision, api } }); }
3148
+ deleteApi(apiId) { return this.client.request(`/v1/bots/apis/${encodeURIComponent(apiId)}`, { method: 'DELETE' }); }
3149
+ webhookSecret(botId) { return this.client.request(`/v1/bots/${encodeURIComponent(botId)}/webhook-secret`, { method: 'POST', body: {} }); }
3150
+ triggerWebhook(botId, triggerId) { return this.client.request(`/v1/bots/${encodeURIComponent(botId)}/triggers/${encodeURIComponent(triggerId)}/webhook`, { method: 'POST', body: {} }); }
3151
+ triggerAdmission(botId, triggerId, admissionId) { return this.client.request(`/v1/bots/${encodeURIComponent(botId)}/triggers/${encodeURIComponent(triggerId)}/admissions/${encodeURIComponent(admissionId)}`); }
3152
+ apiSigningKey(botId, apiId, request = { action: 'read' }) { return this.client.request(`/v1/bots/${encodeURIComponent(botId)}/apis/${encodeURIComponent(apiId)}/signing-key`, { method: 'POST', body: request }); }
3153
+ models() { return this.client.request('/v1/bots/models'); }
3154
+ setCredential(botId, secret) { return this.client.request(`/v1/bots/${encodeURIComponent(botId)}/credentials`, { method: 'POST', body: { secret } }); }
3155
+ deliveries(botId, runId) { return this.client.request(`/v1/bots/${encodeURIComponent(botId)}/runs/${encodeURIComponent(runId)}/deliveries`); }
3156
+ retryDelivery(botId, runId, deliveryId) { return this.client.request(`/v1/bots/${encodeURIComponent(botId)}/runs/${encodeURIComponent(runId)}/deliveries/retry`, { method: 'POST', body: { deliveryId } }); }
3157
+ setCustomerCredential(botId, input) { return this.client.request(`/v1/bots/${encodeURIComponent(botId)}/credentials`, { method: 'POST', body: input }); }
3158
+ delete(botId) { return this.client.request(`/v1/bots/${encodeURIComponent(botId)}`, { method: 'DELETE' }); }
3159
+ deleteCustomerData(botId, sessionId) { return this.client.request(`/v1/bots/${encodeURIComponent(botId)}/customer-data/delete`, { method: 'POST', body: { sessionId } }); }
3160
+ setState(botId, input) { return this.client.request(`/v1/bots/${encodeURIComponent(botId)}/state`, { method: 'POST', body: input }); }
3161
+ list() { return this.client.request('/v1/bots'); }
3162
+ create(name) { return this.client.request('/v1/bots', { method: 'POST', body: { name } }); }
3163
+ get(botId) { return this.client.request(`/v1/bots/${encodeURIComponent(botId)}`); }
3164
+ update(botId, revision, patch) { return this.client.request(`/v1/bots/${encodeURIComponent(botId)}`, { method: 'PATCH', body: { revision, patch } }); }
3165
+ publish(botId, revision, channels) { return this.client.request(`/v1/bots/${encodeURIComponent(botId)}/publish`, { method: 'POST', body: { revision, channels } }); }
3166
+ run(botId, input) { return this.client.request(`/v1/bots/${encodeURIComponent(botId)}/runs`, { method: 'POST', body: { ...input, runId: input.runId ?? globalThis.crypto.randomUUID() } }); }
3167
+ getRun(botId, runId) { return this.client.request(`/v1/bots/${encodeURIComponent(botId)}/runs/${encodeURIComponent(runId)}`); }
3168
+ /** Observe the same durable run. Returns when finished or when a person must act. */
3169
+ async wait(botId, runId, options = {}) {
3170
+ const deadline = Date.now() + positiveSdkInteger(options.totalTimeoutMs ?? 300_000, 'totalTimeoutMs');
3171
+ while (Date.now() < deadline) {
3172
+ if (options.signal?.aborted)
3173
+ throw options.signal.reason;
3174
+ const run = await this.client.request(`/v1/bots/${encodeURIComponent(botId)}/runs/${encodeURIComponent(runId)}`, { signal: options.signal, timeoutMs: Math.min(30_000, deadline - Date.now()) });
3175
+ if (['completed', 'failed', 'stopped', 'cancelled', 'waiting_for_approval', 'waiting_for_connection'].includes(run.status))
3176
+ return run;
3177
+ await sdkDelay(Math.min(1000, Math.max(1, deadline - Date.now())), options.signal ?? AbortSignal.timeout(Math.max(1, deadline - Date.now())));
3178
+ }
3179
+ throw new StacklinkApiError('Bot observation timed out. Continue observing the same run.', 408, 'sdk_run_timeout', { botId, runId });
3180
+ }
3181
+ events(botId, runId, after = '0') { return this.client.request(`/v1/bots/${encodeURIComponent(botId)}/runs/${encodeURIComponent(runId)}/events`, { query: { after } }); }
3182
+ stream(botId, runId, options = {}) { return this.client.streamBotEvents(botId, runId, options); }
3183
+ cancel(botId, runId) { return this.client.request(`/v1/bots/${encodeURIComponent(botId)}/runs/${encodeURIComponent(runId)}/stop`, { method: 'POST', body: {} }); }
3184
+ }
2991
3185
  //# sourceMappingURL=index.js.map