@kivel.io/sdk 0.9.0 → 0.10.0

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/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @kivel.io/sdk
2
2
 
3
+ ## 0.10.0
4
+
5
+ ### Minor Changes
6
+
7
+ - ad5805c: Add live event streams to the SDK and use them in the webhook listener.
8
+
3
9
  ## 0.9.0
4
10
 
5
11
  ### Minor Changes
package/README.md CHANGED
@@ -24,6 +24,11 @@ await kivel.remove('inboxes', inbox.id);
24
24
 
25
25
  `createInbox()` returns a `webhookUrl` that accepts senders without credentials. Reading and waiting still require your workspace key or the private `inspectionToken`. For token-required receiving, pass `{ visibility: 'private' }` and give the sender the returned `accessToken` for its `X-Access-Token` header.
26
26
 
27
+
28
+ Use `stream(['webhook:' + inbox.id], { token, signal })` to receive change notices
29
+ when live updates are available. Fetch the request bodies with the normal inbox
30
+ methods. Pass the last notice's `cursor` as `lastEventId` when reconnecting.
31
+
27
32
  `baseUrl` defaults to `https://kivel.io`; set it to your self-hosted origin.
28
33
  Remote origins require HTTPS. The default request timeout is 25 seconds. Requests
29
34
  are not automatically retried and redirects are refused.
package/dist/index.d.ts CHANGED
@@ -76,6 +76,22 @@ export interface KivelLimitDetails {
76
76
  max?: number;
77
77
  plan?: KivelUsage['plan'];
78
78
  }
79
+ export interface StreamOptions {
80
+ token?: string;
81
+ lastEventId?: string;
82
+ signal?: AbortSignal;
83
+ }
84
+ export type KivelLiveEvent = {
85
+ type: 'ready';
86
+ cursor: string;
87
+ } | {
88
+ type: 'change';
89
+ cursor: string;
90
+ topic: string;
91
+ scopeId: string;
92
+ id: string;
93
+ event: string;
94
+ };
79
95
  export declare class KivelError extends Error {
80
96
  readonly status: number;
81
97
  readonly requestId?: string | undefined;
@@ -92,6 +108,7 @@ export declare class Kivel {
92
108
  private timeoutMs;
93
109
  constructor(options?: ClientOptions);
94
110
  request<T = unknown>(method: string, path: string, body?: unknown, resourceToken?: string, headers?: Record<string, string>): Promise<T>;
111
+ stream(topics: string[], options?: StreamOptions): AsyncGenerator<KivelLiveEvent>;
95
112
  tools(): Promise<{
96
113
  tools: {
97
114
  id: string;
@@ -388,7 +405,9 @@ export declare class Kivel {
388
405
  createInbox(settings?: ResourceSettings): Promise<ResourceCredentials & {
389
406
  webhookUrl: string;
390
407
  }>;
391
- inspectInbox(id: string, token?: string): Promise<unknown>;
408
+ inspectInbox(id: string, token?: string): Promise<{
409
+ requests: CapturedRequest[];
410
+ }>;
392
411
  /**
393
412
  * Long-polls until the inbox captures a request newer than `after` (any request when omitted).
394
413
  * Resolves with `timedOut: true` when nothing arrives; call again to keep waiting.
package/dist/index.js CHANGED
@@ -17,6 +17,26 @@ export class KivelError extends Error {
17
17
  this.name = 'KivelError';
18
18
  }
19
19
  }
20
+ function apiFailure(response, value) {
21
+ const error = value && typeof value === 'object' ? value : {};
22
+ const code = error.code ?? error.error;
23
+ const details = (code === 'limit_reached' || code === 'rate_limited') && typeof error.limit === 'string'
24
+ ? {
25
+ code,
26
+ limit: error.limit,
27
+ ...(typeof error.used === 'number' ? { used: error.used } : {}),
28
+ ...(typeof error.max === 'number' ? { max: error.max } : {}),
29
+ ...(['anonymous', 'free', 'pro', 'team'].some((plan) => plan === error.plan)
30
+ ? { plan: error.plan }
31
+ : {}),
32
+ }
33
+ : undefined;
34
+ return new KivelError(typeof error.message === 'string'
35
+ ? error.message
36
+ : typeof error.error === 'string'
37
+ ? error.error
38
+ : `Request failed (${response.status}).`, response.status, response.headers.get('x-request-id') ?? undefined, Number(response.headers.get('retry-after')) || undefined, typeof error.requiredPlan === 'string' ? error.requiredPlan : undefined, typeof error.upgradeUrl === 'string' ? error.upgradeUrl : undefined, details);
39
+ }
20
40
  export class Kivel {
21
41
  options;
22
42
  baseUrl;
@@ -86,28 +106,89 @@ export class Kivel {
86
106
  return text;
87
107
  throw new KivelError('The server returned an invalid response.', response.status, response.headers.get('x-request-id') ?? undefined);
88
108
  }
89
- if (!response.ok) {
90
- const error = value && typeof value === 'object' ? value : {};
91
- const code = error.code ?? error.error;
92
- const details = (code === 'limit_reached' || code === 'rate_limited') &&
93
- typeof error.limit === 'string'
94
- ? {
95
- code,
96
- limit: error.limit,
97
- ...(typeof error.used === 'number' ? { used: error.used } : {}),
98
- ...(typeof error.max === 'number' ? { max: error.max } : {}),
99
- ...(['anonymous', 'free', 'pro', 'team'].some((plan) => plan === error.plan)
100
- ? { plan: error.plan }
101
- : {}),
109
+ if (!response.ok)
110
+ throw apiFailure(response, value);
111
+ return value;
112
+ }
113
+ async *stream(topics, options = {}) {
114
+ if (!topics.length ||
115
+ topics.length > 10 ||
116
+ topics.some((topic) => !topic || /[,\s]/.test(topic)))
117
+ throw new Error('Choose 1 to 10 live topics.');
118
+ const query = new URLSearchParams({ topics: topics.join(',') });
119
+ const headers = new Headers({ Accept: 'text/event-stream' });
120
+ if (this.options.apiKey)
121
+ headers.set('Authorization', `Bearer ${this.options.apiKey}`);
122
+ if (options.token)
123
+ headers.set('X-Resource-Token', options.token);
124
+ if (options.lastEventId)
125
+ headers.set('Last-Event-ID', options.lastEventId);
126
+ const timeout = AbortSignal.timeout(60_000);
127
+ const response = await this.fetcher(`${this.baseUrl}/api/stream?${query}`, {
128
+ headers,
129
+ redirect: 'error',
130
+ credentials: 'omit',
131
+ signal: options.signal ? AbortSignal.any([timeout, options.signal]) : timeout,
132
+ });
133
+ if (!response.ok)
134
+ throw apiFailure(response, await response.json().catch(() => undefined));
135
+ const invalid = () => new KivelError('The server returned an invalid event stream.', response.status);
136
+ if (!response.body ||
137
+ !response.headers.get('content-type')?.startsWith('text/event-stream'))
138
+ throw invalid();
139
+ const reader = response.body.getReader();
140
+ const decoder = new TextDecoder();
141
+ let pending = '';
142
+ try {
143
+ for (;;) {
144
+ const { value, done } = await reader.read();
145
+ if (done)
146
+ break;
147
+ pending += decoder.decode(value, { stream: true });
148
+ for (let end = /\r?\n\r?\n/.exec(pending); end; end = /\r?\n\r?\n/.exec(pending)) {
149
+ if (end.index > 16_384)
150
+ throw invalid();
151
+ const lines = pending.slice(0, end.index).split(/\r?\n/);
152
+ pending = pending.slice(end.index + end[0].length);
153
+ const cursor = lines.find((line) => line.startsWith('id: '))?.slice(4);
154
+ const type = lines.find((line) => line.startsWith('event: '))?.slice(7);
155
+ if (!cursor || (type !== 'ready' && type !== 'change'))
156
+ continue;
157
+ if (type === 'ready') {
158
+ yield { type, cursor };
159
+ continue;
160
+ }
161
+ const json = lines
162
+ .filter((line) => line.startsWith('data: '))
163
+ .map((line) => line.slice(6))
164
+ .join('\n');
165
+ let data;
166
+ try {
167
+ data = JSON.parse(json);
168
+ }
169
+ catch {
170
+ throw invalid();
171
+ }
172
+ if (!data ||
173
+ ['topic', 'scopeId', 'id', 'event'].some((key) => typeof data[key] !== 'string'))
174
+ throw invalid();
175
+ yield {
176
+ type,
177
+ cursor,
178
+ topic: data.topic,
179
+ scopeId: data.scopeId,
180
+ id: data.id,
181
+ event: data.event,
182
+ };
102
183
  }
103
- : undefined;
104
- throw new KivelError(typeof error.message === 'string'
105
- ? error.message
106
- : typeof error.error === 'string'
107
- ? error.error
108
- : `Request failed (${response.status}).`, response.status, response.headers.get('x-request-id') ?? undefined, Number(response.headers.get('retry-after')) || undefined, typeof error.requiredPlan === 'string' ? error.requiredPlan : undefined, typeof error.upgradeUrl === 'string' ? error.upgradeUrl : undefined, details);
184
+ if (pending.length > 16_384)
185
+ throw invalid();
186
+ }
187
+ }
188
+ finally {
189
+ await reader.cancel().catch(() => { });
190
+ reader.releaseLock();
109
191
  }
110
- return value;
111
192
  }
112
193
  tools() {
113
194
  return this.request('GET', '/api/tools');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kivel.io/sdk",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {