@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 +6 -0
- package/README.md +5 -0
- package/dist/index.d.ts +20 -1
- package/dist/index.js +101 -20
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
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<
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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');
|