@instantkom/cli 3.129.2
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/README.md +34 -0
- package/bin/run.js +3 -0
- package/dist/api-client.d.ts +55 -0
- package/dist/api-client.js +199 -0
- package/dist/auth/device-flow-client.d.ts +22 -0
- package/dist/auth/device-flow-client.js +70 -0
- package/dist/auth/token-resolver.d.ts +31 -0
- package/dist/auth/token-resolver.js +48 -0
- package/dist/auth/token-store.d.ts +13 -0
- package/dist/auth/token-store.js +39 -0
- package/dist/base-command.d.ts +18 -0
- package/dist/base-command.js +84 -0
- package/dist/commands/ai/reply.d.ts +18 -0
- package/dist/commands/ai/reply.js +46 -0
- package/dist/commands/auth/login.d.ts +15 -0
- package/dist/commands/auth/login.js +37 -0
- package/dist/commands/auth/logout.d.ts +14 -0
- package/dist/commands/auth/logout.js +17 -0
- package/dist/commands/auth/tokens/create.d.ts +17 -0
- package/dist/commands/auth/tokens/create.js +62 -0
- package/dist/commands/auth/tokens/list.d.ts +14 -0
- package/dist/commands/auth/tokens/list.js +41 -0
- package/dist/commands/auth/tokens/revoke.d.ts +20 -0
- package/dist/commands/auth/tokens/revoke.js +41 -0
- package/dist/commands/autocomplete/script.d.ts +11 -0
- package/dist/commands/autocomplete/script.js +90 -0
- package/dist/commands/autocomplete.d.ts +13 -0
- package/dist/commands/autocomplete.js +95 -0
- package/dist/commands/bots/create.d.ts +22 -0
- package/dist/commands/bots/create.js +39 -0
- package/dist/commands/bots/delete.d.ts +20 -0
- package/dist/commands/bots/delete.js +19 -0
- package/dist/commands/bots/env-vars/bots.d.ts +20 -0
- package/dist/commands/bots/env-vars/bots.js +18 -0
- package/dist/commands/bots/env-vars/create.d.ts +18 -0
- package/dist/commands/bots/env-vars/create.js +28 -0
- package/dist/commands/bots/env-vars/delete.d.ts +20 -0
- package/dist/commands/bots/env-vars/delete.js +19 -0
- package/dist/commands/bots/env-vars/get.d.ts +20 -0
- package/dist/commands/bots/env-vars/get.js +19 -0
- package/dist/commands/bots/env-vars/list.d.ts +18 -0
- package/dist/commands/bots/env-vars/list.js +28 -0
- package/dist/commands/bots/env-vars/update.d.ts +23 -0
- package/dist/commands/bots/env-vars/update.js +29 -0
- package/dist/commands/bots/env-vars/values/delete.d.ts +20 -0
- package/dist/commands/bots/env-vars/values/delete.js +18 -0
- package/dist/commands/bots/env-vars/values/update.d.ts +23 -0
- package/dist/commands/bots/env-vars/values/update.js +28 -0
- package/dist/commands/bots/env-vars/values.d.ts +24 -0
- package/dist/commands/bots/env-vars/values.js +30 -0
- package/dist/commands/bots/filters/create.d.ts +28 -0
- package/dist/commands/bots/filters/create.js +38 -0
- package/dist/commands/bots/filters/delete.d.ts +24 -0
- package/dist/commands/bots/filters/delete.js +19 -0
- package/dist/commands/bots/filters/get.d.ts +24 -0
- package/dist/commands/bots/filters/get.js +19 -0
- package/dist/commands/bots/filters/list.d.ts +24 -0
- package/dist/commands/bots/filters/list.js +30 -0
- package/dist/commands/bots/filters/update.d.ts +32 -0
- package/dist/commands/bots/filters/update.js +39 -0
- package/dist/commands/bots/get.d.ts +20 -0
- package/dist/commands/bots/get.js +19 -0
- package/dist/commands/bots/list.d.ts +21 -0
- package/dist/commands/bots/list.js +34 -0
- package/dist/commands/bots/matches.d.ts +23 -0
- package/dist/commands/bots/matches.js +28 -0
- package/dist/commands/bots/tags/add.d.ts +24 -0
- package/dist/commands/bots/tags/add.js +19 -0
- package/dist/commands/bots/tags/list.d.ts +20 -0
- package/dist/commands/bots/tags/list.js +18 -0
- package/dist/commands/bots/tags/remove.d.ts +24 -0
- package/dist/commands/bots/tags/remove.js +19 -0
- package/dist/commands/bots/update.d.ts +27 -0
- package/dist/commands/bots/update.js +36 -0
- package/dist/commands/broadcast/create.d.ts +25 -0
- package/dist/commands/broadcast/create.js +117 -0
- package/dist/commands/channels/create.d.ts +19 -0
- package/dist/commands/channels/create.js +43 -0
- package/dist/commands/channels/get.d.ts +20 -0
- package/dist/commands/channels/get.js +25 -0
- package/dist/commands/channels/kpis.d.ts +21 -0
- package/dist/commands/channels/kpis.js +29 -0
- package/dist/commands/channels/list.d.ts +19 -0
- package/dist/commands/channels/list.js +43 -0
- package/dist/commands/channels/update.d.ts +24 -0
- package/dist/commands/channels/update.js +43 -0
- package/dist/commands/chats/reply.d.ts +21 -0
- package/dist/commands/chats/reply.js +28 -0
- package/dist/commands/config/get.d.ts +17 -0
- package/dist/commands/config/get.js +29 -0
- package/dist/commands/config/set.d.ts +18 -0
- package/dist/commands/config/set.js +30 -0
- package/dist/commands/config/unset.d.ts +17 -0
- package/dist/commands/config/unset.js +25 -0
- package/dist/commands/contacts/create.d.ts +18 -0
- package/dist/commands/contacts/create.js +39 -0
- package/dist/commands/contacts/delete.d.ts +20 -0
- package/dist/commands/contacts/delete.js +25 -0
- package/dist/commands/contacts/export.d.ts +19 -0
- package/dist/commands/contacts/export.js +50 -0
- package/dist/commands/contacts/get.d.ts +20 -0
- package/dist/commands/contacts/get.js +25 -0
- package/dist/commands/contacts/import.d.ts +16 -0
- package/dist/commands/contacts/import.js +62 -0
- package/dist/commands/contacts/list.d.ts +25 -0
- package/dist/commands/contacts/list.js +71 -0
- package/dist/commands/contacts/update.d.ts +23 -0
- package/dist/commands/contacts/update.js +39 -0
- package/dist/commands/exports/create.d.ts +23 -0
- package/dist/commands/exports/create.js +69 -0
- package/dist/commands/exports/delete.d.ts +20 -0
- package/dist/commands/exports/delete.js +25 -0
- package/dist/commands/exports/download.d.ts +21 -0
- package/dist/commands/exports/download.js +37 -0
- package/dist/commands/exports/get.d.ts +20 -0
- package/dist/commands/exports/get.js +25 -0
- package/dist/commands/exports/list.d.ts +18 -0
- package/dist/commands/exports/list.js +39 -0
- package/dist/commands/flow/edges/create.d.ts +25 -0
- package/dist/commands/flow/edges/create.js +32 -0
- package/dist/commands/flow/edges/delete.d.ts +24 -0
- package/dist/commands/flow/edges/delete.js +19 -0
- package/dist/commands/flow/edges/get.d.ts +24 -0
- package/dist/commands/flow/edges/get.js +19 -0
- package/dist/commands/flow/edges/list.d.ts +20 -0
- package/dist/commands/flow/edges/list.js +18 -0
- package/dist/commands/flow/edges/update.d.ts +29 -0
- package/dist/commands/flow/edges/update.js +33 -0
- package/dist/commands/flow/nodes/create.d.ts +25 -0
- package/dist/commands/flow/nodes/create.js +32 -0
- package/dist/commands/flow/nodes/delete.d.ts +24 -0
- package/dist/commands/flow/nodes/delete.js +19 -0
- package/dist/commands/flow/nodes/get.d.ts +24 -0
- package/dist/commands/flow/nodes/get.js +19 -0
- package/dist/commands/flow/nodes/list.d.ts +20 -0
- package/dist/commands/flow/nodes/list.js +18 -0
- package/dist/commands/flow/nodes/update.d.ts +29 -0
- package/dist/commands/flow/nodes/update.js +33 -0
- package/dist/commands/flows/create.d.ts +20 -0
- package/dist/commands/flows/create.js +32 -0
- package/dist/commands/flows/delete.d.ts +20 -0
- package/dist/commands/flows/delete.js +19 -0
- package/dist/commands/flows/get.d.ts +20 -0
- package/dist/commands/flows/get.js +19 -0
- package/dist/commands/flows/list.d.ts +19 -0
- package/dist/commands/flows/list.js +30 -0
- package/dist/commands/flows/update.d.ts +25 -0
- package/dist/commands/flows/update.js +33 -0
- package/dist/commands/send.d.ts +23 -0
- package/dist/commands/send.js +129 -0
- package/dist/commands/status.d.ts +23 -0
- package/dist/commands/status.js +81 -0
- package/dist/commands/tail.d.ts +15 -0
- package/dist/commands/tail.js +36 -0
- package/dist/commands/templates/get.d.ts +20 -0
- package/dist/commands/templates/get.js +24 -0
- package/dist/commands/templates/list.d.ts +18 -0
- package/dist/commands/templates/list.js +37 -0
- package/dist/commands/templates/render.d.ts +21 -0
- package/dist/commands/templates/render.js +30 -0
- package/dist/commands/ticket/messages/create.d.ts +22 -0
- package/dist/commands/ticket/messages/create.js +26 -0
- package/dist/commands/ticket/messages/delete.d.ts +24 -0
- package/dist/commands/ticket/messages/delete.js +20 -0
- package/dist/commands/ticket/messages/get.d.ts +24 -0
- package/dist/commands/ticket/messages/get.js +19 -0
- package/dist/commands/ticket/messages/list.d.ts +20 -0
- package/dist/commands/ticket/messages/list.js +18 -0
- package/dist/commands/tickets/create.d.ts +22 -0
- package/dist/commands/tickets/create.js +34 -0
- package/dist/commands/tickets/delete.d.ts +21 -0
- package/dist/commands/tickets/delete.js +21 -0
- package/dist/commands/tickets/get.d.ts +21 -0
- package/dist/commands/tickets/get.js +21 -0
- package/dist/commands/tickets/list.d.ts +17 -0
- package/dist/commands/tickets/list.js +24 -0
- package/dist/commands/tickets/update.d.ts +25 -0
- package/dist/commands/tickets/update.js +31 -0
- package/dist/commands/webhooks/add.d.ts +16 -0
- package/dist/commands/webhooks/add.js +38 -0
- package/dist/commands/webhooks/list.d.ts +14 -0
- package/dist/commands/webhooks/list.js +18 -0
- package/dist/commands/webhooks/remove.d.ts +17 -0
- package/dist/commands/webhooks/remove.js +24 -0
- package/dist/commands/whoami.d.ts +15 -0
- package/dist/commands/whoami.js +40 -0
- package/dist/config/config-file.d.ts +36 -0
- package/dist/config/config-file.js +111 -0
- package/dist/config/config-path.d.ts +8 -0
- package/dist/config/config-path.js +25 -0
- package/dist/crud/csv.d.ts +6 -0
- package/dist/crud/csv.js +89 -0
- package/dist/crud/data.d.ts +4 -0
- package/dist/crud/data.js +38 -0
- package/dist/crud/request.d.ts +14 -0
- package/dist/crud/request.js +26 -0
- package/dist/errors/api-error.d.ts +6 -0
- package/dist/errors/api-error.js +10 -0
- package/dist/errors/exit-codes.d.ts +12 -0
- package/dist/errors/exit-codes.js +24 -0
- package/dist/errors/redact-token.d.ts +10 -0
- package/dist/errors/redact-token.js +17 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/output/formatter.d.ts +15 -0
- package/dist/output/formatter.js +57 -0
- package/dist/output/text-format.d.ts +13 -0
- package/dist/output/text-format.js +67 -0
- package/dist/send/api.d.ts +3 -0
- package/dist/send/api.js +21 -0
- package/dist/send/fallback.d.ts +5 -0
- package/dist/send/fallback.js +38 -0
- package/dist/send/media.d.ts +2 -0
- package/dist/send/media.js +41 -0
- package/dist/send/payload.d.ts +23 -0
- package/dist/send/payload.js +22 -0
- package/dist/send/recipient-resolver.d.ts +13 -0
- package/dist/send/recipient-resolver.js +28 -0
- package/dist/send/schedule.d.ts +1 -0
- package/dist/send/schedule.js +19 -0
- package/dist/tail/sse.d.ts +24 -0
- package/dist/tail/sse.js +139 -0
- package/dist/templates/render-template.d.ts +14 -0
- package/dist/templates/render-template.js +46 -0
- package/npm-shrinkwrap.json +11444 -0
- package/oclif.manifest.json +9286 -0
- package/package.json +157 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { ApiError } from '../errors/api-error.js';
|
|
2
|
+
const ALIASES = {
|
|
3
|
+
wa: ['whatsapp', 'wa'],
|
|
4
|
+
whatsapp: ['whatsapp', 'wa'],
|
|
5
|
+
sms: ['sms'],
|
|
6
|
+
};
|
|
7
|
+
function channelsFromResponse(response) {
|
|
8
|
+
if (Array.isArray(response))
|
|
9
|
+
return response;
|
|
10
|
+
return response.data ?? response.items ?? [];
|
|
11
|
+
}
|
|
12
|
+
function matchesAlias(channel, alias) {
|
|
13
|
+
const needles = ALIASES[alias.toLowerCase()] ?? [alias.toLowerCase()];
|
|
14
|
+
const haystack = [
|
|
15
|
+
String(channel.id),
|
|
16
|
+
channel.name ?? '',
|
|
17
|
+
channel.gatewayType ?? '',
|
|
18
|
+
].join(' ').toLowerCase();
|
|
19
|
+
return needles.some((needle) => haystack.includes(needle));
|
|
20
|
+
}
|
|
21
|
+
export async function resolveFallbackChannels(client, aliasesCsv) {
|
|
22
|
+
const aliases = aliasesCsv
|
|
23
|
+
.split(',')
|
|
24
|
+
.map((alias) => alias.trim())
|
|
25
|
+
.filter(Boolean);
|
|
26
|
+
if (aliases.length === 0) {
|
|
27
|
+
throw new ApiError('user_error', '--channels must contain at least one channel alias');
|
|
28
|
+
}
|
|
29
|
+
const response = await client.get('/v1/channels', { limit: 100 });
|
|
30
|
+
const channels = channelsFromResponse(response);
|
|
31
|
+
return aliases.map((alias) => {
|
|
32
|
+
const channel = channels.find((candidate) => matchesAlias(candidate, alias));
|
|
33
|
+
if (!channel) {
|
|
34
|
+
throw new ApiError('user_error', `No channel found for alias '${alias}'`);
|
|
35
|
+
}
|
|
36
|
+
return { alias, channelId: channel.id };
|
|
37
|
+
});
|
|
38
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { readFileSync, statSync } from 'fs';
|
|
2
|
+
import { basename } from 'path';
|
|
3
|
+
import { ApiError } from '../errors/api-error.js';
|
|
4
|
+
const MIME_BY_EXTENSION = {
|
|
5
|
+
jpg: 'image/jpeg',
|
|
6
|
+
jpeg: 'image/jpeg',
|
|
7
|
+
png: 'image/png',
|
|
8
|
+
gif: 'image/gif',
|
|
9
|
+
webp: 'image/webp',
|
|
10
|
+
mp4: 'video/mp4',
|
|
11
|
+
mp3: 'audio/mpeg',
|
|
12
|
+
wav: 'audio/wav',
|
|
13
|
+
ogg: 'audio/ogg',
|
|
14
|
+
pdf: 'application/pdf',
|
|
15
|
+
};
|
|
16
|
+
export function prepareMediaFiles(paths) {
|
|
17
|
+
if (!paths || paths.length === 0)
|
|
18
|
+
return [];
|
|
19
|
+
if (paths.length > 1) {
|
|
20
|
+
throw new ApiError('user_error', 'Only one --media attachment is supported by the current API');
|
|
21
|
+
}
|
|
22
|
+
return paths.map((filePath) => {
|
|
23
|
+
let stat;
|
|
24
|
+
try {
|
|
25
|
+
stat = statSync(filePath);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
throw new ApiError('user_error', `Media file not found: ${filePath}`);
|
|
29
|
+
}
|
|
30
|
+
if (!stat.isFile()) {
|
|
31
|
+
throw new ApiError('user_error', `Media path is not a file: ${filePath}`);
|
|
32
|
+
}
|
|
33
|
+
const extension = filePath.split('.').pop()?.toLowerCase() ?? '';
|
|
34
|
+
return {
|
|
35
|
+
fieldName: 'media',
|
|
36
|
+
filename: basename(filePath),
|
|
37
|
+
contentType: MIME_BY_EXTENSION[extension] ?? 'application/octet-stream',
|
|
38
|
+
buffer: readFileSync(filePath),
|
|
39
|
+
};
|
|
40
|
+
});
|
|
41
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export interface MessagePayloadInput {
|
|
2
|
+
recipientId: number;
|
|
3
|
+
text: string;
|
|
4
|
+
scheduledAt?: number;
|
|
5
|
+
templateId?: number;
|
|
6
|
+
}
|
|
7
|
+
export interface BroadcastPayloadInput {
|
|
8
|
+
channelId?: number;
|
|
9
|
+
segmentId?: number;
|
|
10
|
+
targets?: Array<{
|
|
11
|
+
channelId: number;
|
|
12
|
+
segmentId?: number;
|
|
13
|
+
}>;
|
|
14
|
+
text: string;
|
|
15
|
+
scheduledAt?: number;
|
|
16
|
+
templateId?: number;
|
|
17
|
+
sendStatus?: 'draft' | 'scheduled' | 'sending' | 'sent' | 'failed';
|
|
18
|
+
autoApprove?: boolean;
|
|
19
|
+
}
|
|
20
|
+
type ApiPayload = Record<string, string | number | boolean | undefined>;
|
|
21
|
+
export declare function createMessagePayload(input: MessagePayloadInput): ApiPayload;
|
|
22
|
+
export declare function createBroadcastPayload(input: BroadcastPayloadInput): ApiPayload;
|
|
23
|
+
export {};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export function createMessagePayload(input) {
|
|
2
|
+
return {
|
|
3
|
+
recipientId: input.recipientId,
|
|
4
|
+
message: input.text,
|
|
5
|
+
messageType: 'text',
|
|
6
|
+
...(input.scheduledAt ? { scheduledAt: input.scheduledAt } : {}),
|
|
7
|
+
...(input.templateId ? { templateId: input.templateId } : {}),
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
export function createBroadcastPayload(input) {
|
|
11
|
+
return {
|
|
12
|
+
...(input.channelId ? { channelId: input.channelId } : {}),
|
|
13
|
+
...(input.segmentId ? { segmentId: input.segmentId } : {}),
|
|
14
|
+
...(input.targets ? { targets: JSON.stringify(input.targets) } : {}),
|
|
15
|
+
message: input.text,
|
|
16
|
+
broadcastType: 'text',
|
|
17
|
+
...(input.scheduledAt ? { scheduledAt: input.scheduledAt } : {}),
|
|
18
|
+
...(input.templateId ? { templateId: input.templateId } : {}),
|
|
19
|
+
...(input.sendStatus ? { sendStatus: input.sendStatus } : {}),
|
|
20
|
+
...(input.autoApprove !== undefined ? { autoApprove: input.autoApprove } : {}),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ApiClient } from '../api-client.js';
|
|
2
|
+
export interface RecipientRecord {
|
|
3
|
+
id: number;
|
|
4
|
+
channelId: number;
|
|
5
|
+
identifier: string;
|
|
6
|
+
name?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface ResolveRecipientOptions {
|
|
9
|
+
client: Pick<ApiClient, 'get' | 'post'>;
|
|
10
|
+
channelId: number;
|
|
11
|
+
to: string;
|
|
12
|
+
}
|
|
13
|
+
export declare function resolveRecipient({ client, channelId, to, }: ResolveRecipientOptions): Promise<RecipientRecord>;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { ApiError } from '../errors/api-error.js';
|
|
2
|
+
function normalizeIdentifier(value) {
|
|
3
|
+
return value.replace(/[\s().-]/g, '');
|
|
4
|
+
}
|
|
5
|
+
function extractRecipients(response) {
|
|
6
|
+
if (Array.isArray(response))
|
|
7
|
+
return response;
|
|
8
|
+
return response.data ?? response.items ?? [];
|
|
9
|
+
}
|
|
10
|
+
export async function resolveRecipient({ client, channelId, to, }) {
|
|
11
|
+
if (!Number.isInteger(channelId) || channelId < 1) {
|
|
12
|
+
throw new ApiError('user_error', '--channel must be a positive integer');
|
|
13
|
+
}
|
|
14
|
+
const trimmed = to.trim();
|
|
15
|
+
if (!trimmed) {
|
|
16
|
+
throw new ApiError('user_error', '--to is required');
|
|
17
|
+
}
|
|
18
|
+
const searched = await client.get('/v1/contacts', { channelId, search: trimmed, limit: 20 });
|
|
19
|
+
const normalizedTo = normalizeIdentifier(trimmed);
|
|
20
|
+
const existing = extractRecipients(searched).find((recipient) => (recipient.channelId === channelId
|
|
21
|
+
&& normalizeIdentifier(recipient.identifier) === normalizedTo));
|
|
22
|
+
if (existing)
|
|
23
|
+
return existing;
|
|
24
|
+
return client.post('/v1/contacts', {
|
|
25
|
+
channelId,
|
|
26
|
+
identifier: trimmed,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function parseSendAt(input: string | undefined): number | undefined;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { ApiError } from '../errors/api-error.js';
|
|
2
|
+
const ISO_WITH_TIME = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}/;
|
|
3
|
+
export function parseSendAt(input) {
|
|
4
|
+
if (!input)
|
|
5
|
+
return undefined;
|
|
6
|
+
if (!ISO_WITH_TIME.test(input)) {
|
|
7
|
+
throw new ApiError('user_error', '--send-at must be an ISO-8601 date-time');
|
|
8
|
+
}
|
|
9
|
+
const date = new Date(input);
|
|
10
|
+
if (Number.isNaN(date.getTime())) {
|
|
11
|
+
throw new ApiError('user_error', '--send-at must be a valid ISO-8601 date-time');
|
|
12
|
+
}
|
|
13
|
+
const seconds = Math.floor(date.getTime() / 1000);
|
|
14
|
+
const now = Math.floor(Date.now() / 1000);
|
|
15
|
+
if (seconds <= now) {
|
|
16
|
+
throw new ApiError('user_error', '--send-at must be in the future');
|
|
17
|
+
}
|
|
18
|
+
return seconds;
|
|
19
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export interface RealtimeEvent {
|
|
2
|
+
event: string;
|
|
3
|
+
payload?: unknown;
|
|
4
|
+
timestamp?: string;
|
|
5
|
+
[key: string]: unknown;
|
|
6
|
+
}
|
|
7
|
+
export interface SseFrame {
|
|
8
|
+
event?: string;
|
|
9
|
+
data: string;
|
|
10
|
+
}
|
|
11
|
+
export declare class SseParser {
|
|
12
|
+
private buffer;
|
|
13
|
+
push(chunk: string): SseFrame[];
|
|
14
|
+
flush(): SseFrame[];
|
|
15
|
+
}
|
|
16
|
+
export declare function parseFrame(rawFrame: string): SseFrame | null;
|
|
17
|
+
export declare function parseRealtimeEvent(frame: SseFrame): RealtimeEvent | null;
|
|
18
|
+
export declare function shouldEmitForChannel(event: RealtimeEvent, channelId: number): boolean;
|
|
19
|
+
export declare function streamRealtimeEvents({ apiKey, apiUrl, channelId, onEvent, }: {
|
|
20
|
+
apiKey: string;
|
|
21
|
+
apiUrl: string;
|
|
22
|
+
channelId: number;
|
|
23
|
+
onEvent: (event: RealtimeEvent) => void;
|
|
24
|
+
}): Promise<void>;
|
package/dist/tail/sse.js
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { ApiError } from '../errors/api-error.js';
|
|
2
|
+
export class SseParser {
|
|
3
|
+
buffer = '';
|
|
4
|
+
push(chunk) {
|
|
5
|
+
this.buffer += chunk.replace(/\r\n/g, '\n');
|
|
6
|
+
const frames = [];
|
|
7
|
+
let boundary = this.buffer.indexOf('\n\n');
|
|
8
|
+
while (boundary !== -1) {
|
|
9
|
+
const rawFrame = this.buffer.slice(0, boundary);
|
|
10
|
+
this.buffer = this.buffer.slice(boundary + 2);
|
|
11
|
+
const frame = parseFrame(rawFrame);
|
|
12
|
+
if (frame)
|
|
13
|
+
frames.push(frame);
|
|
14
|
+
boundary = this.buffer.indexOf('\n\n');
|
|
15
|
+
}
|
|
16
|
+
return frames;
|
|
17
|
+
}
|
|
18
|
+
flush() {
|
|
19
|
+
if (!this.buffer.trim()) {
|
|
20
|
+
this.buffer = '';
|
|
21
|
+
return [];
|
|
22
|
+
}
|
|
23
|
+
const frame = parseFrame(this.buffer);
|
|
24
|
+
this.buffer = '';
|
|
25
|
+
return frame ? [frame] : [];
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export function parseFrame(rawFrame) {
|
|
29
|
+
const data = [];
|
|
30
|
+
let event;
|
|
31
|
+
for (const line of rawFrame.split('\n')) {
|
|
32
|
+
if (!line || line.startsWith(':'))
|
|
33
|
+
continue;
|
|
34
|
+
const separator = line.indexOf(':');
|
|
35
|
+
const field = separator === -1 ? line : line.slice(0, separator);
|
|
36
|
+
const rawValue = separator === -1 ? '' : line.slice(separator + 1);
|
|
37
|
+
const value = rawValue.startsWith(' ') ? rawValue.slice(1) : rawValue;
|
|
38
|
+
if (field === 'event')
|
|
39
|
+
event = value;
|
|
40
|
+
if (field === 'data')
|
|
41
|
+
data.push(value);
|
|
42
|
+
}
|
|
43
|
+
if (data.length === 0)
|
|
44
|
+
return null;
|
|
45
|
+
return { event, data: data.join('\n') };
|
|
46
|
+
}
|
|
47
|
+
export function parseRealtimeEvent(frame) {
|
|
48
|
+
try {
|
|
49
|
+
const parsed = JSON.parse(frame.data);
|
|
50
|
+
if (frame.event && typeof parsed.event !== 'string') {
|
|
51
|
+
parsed.event = frame.event;
|
|
52
|
+
}
|
|
53
|
+
return parsed;
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
export function shouldEmitForChannel(event, channelId) {
|
|
60
|
+
if (event.event === 'system:connected' || event.event === 'heartbeat')
|
|
61
|
+
return false;
|
|
62
|
+
if (event.event !== 'inbox:new-message')
|
|
63
|
+
return false;
|
|
64
|
+
const ids = collectChannelIds(event.payload);
|
|
65
|
+
return ids.has(String(channelId));
|
|
66
|
+
}
|
|
67
|
+
function collectChannelIds(value) {
|
|
68
|
+
const ids = new Set();
|
|
69
|
+
collectChannelIdsInto(value, ids);
|
|
70
|
+
return ids;
|
|
71
|
+
}
|
|
72
|
+
function collectChannelIdsInto(value, ids) {
|
|
73
|
+
if (value === null || value === undefined)
|
|
74
|
+
return;
|
|
75
|
+
if (Array.isArray(value)) {
|
|
76
|
+
for (const item of value)
|
|
77
|
+
collectChannelIdsInto(item, ids);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (typeof value !== 'object')
|
|
81
|
+
return;
|
|
82
|
+
for (const [key, nested] of Object.entries(value)) {
|
|
83
|
+
if (isChannelIdKey(key) && (typeof nested === 'number' || typeof nested === 'string')) {
|
|
84
|
+
ids.add(String(nested));
|
|
85
|
+
}
|
|
86
|
+
collectChannelIdsInto(nested, ids);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function isChannelIdKey(key) {
|
|
90
|
+
return key === 'channelId' || key === 'channel_id' || key === 'cid';
|
|
91
|
+
}
|
|
92
|
+
export async function streamRealtimeEvents({ apiKey, apiUrl, channelId, onEvent, }) {
|
|
93
|
+
const response = await fetch(new URL('/app/realtime/stream', normalizeApiUrl(apiUrl)), {
|
|
94
|
+
headers: {
|
|
95
|
+
Accept: 'text/event-stream',
|
|
96
|
+
'X-API-KEY': apiKey,
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
if (!response.ok) {
|
|
100
|
+
throw new ApiError(mapStatus(response.status), `Realtime stream failed with HTTP ${response.status}`, response.status);
|
|
101
|
+
}
|
|
102
|
+
if (!response.body) {
|
|
103
|
+
throw new ApiError('network', 'Realtime stream response had no body');
|
|
104
|
+
}
|
|
105
|
+
const parser = new SseParser();
|
|
106
|
+
const reader = response.body.getReader();
|
|
107
|
+
const decoder = new TextDecoder();
|
|
108
|
+
try {
|
|
109
|
+
for (;;) {
|
|
110
|
+
const { done, value } = await reader.read();
|
|
111
|
+
const frames = done
|
|
112
|
+
? parser.flush()
|
|
113
|
+
: parser.push(decoder.decode(value, { stream: true }));
|
|
114
|
+
for (const frame of frames) {
|
|
115
|
+
const event = parseRealtimeEvent(frame);
|
|
116
|
+
if (event && shouldEmitForChannel(event, channelId)) {
|
|
117
|
+
onEvent(event);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (done)
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
finally {
|
|
125
|
+
reader.releaseLock();
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
function normalizeApiUrl(apiUrl) {
|
|
129
|
+
return apiUrl.endsWith('/') ? apiUrl : `${apiUrl}/`;
|
|
130
|
+
}
|
|
131
|
+
function mapStatus(status) {
|
|
132
|
+
if (status === 401 || status === 403)
|
|
133
|
+
return 'unauthorized';
|
|
134
|
+
if (status === 429)
|
|
135
|
+
return 'rate_limited';
|
|
136
|
+
if (status >= 500)
|
|
137
|
+
return 'server_error';
|
|
138
|
+
return 'user_error';
|
|
139
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface TemplateLike {
|
|
2
|
+
id: number;
|
|
3
|
+
name?: string;
|
|
4
|
+
message?: string;
|
|
5
|
+
headerFooter?: unknown;
|
|
6
|
+
}
|
|
7
|
+
export interface RenderedTemplate {
|
|
8
|
+
id: number;
|
|
9
|
+
name?: string;
|
|
10
|
+
message: string;
|
|
11
|
+
headerFooter?: unknown;
|
|
12
|
+
}
|
|
13
|
+
export declare function parseVars(values: string[] | undefined): Record<string, string>;
|
|
14
|
+
export declare function renderTemplate(template: TemplateLike, vars: Record<string, string>): RenderedTemplate;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { ApiError } from '../errors/api-error.js';
|
|
2
|
+
const PLACEHOLDER = /\{\{\s*([A-Za-z0-9_.-]+)\s*\}\}/g;
|
|
3
|
+
export function parseVars(values) {
|
|
4
|
+
const vars = {};
|
|
5
|
+
for (const value of values ?? []) {
|
|
6
|
+
const eq = value.indexOf('=');
|
|
7
|
+
if (eq <= 0) {
|
|
8
|
+
throw new ApiError('user_error', `Invalid --vars value '${value}'. Use key=value.`);
|
|
9
|
+
}
|
|
10
|
+
vars[value.slice(0, eq)] = value.slice(eq + 1);
|
|
11
|
+
}
|
|
12
|
+
return vars;
|
|
13
|
+
}
|
|
14
|
+
function renderString(input, vars, missing) {
|
|
15
|
+
return input.replace(PLACEHOLDER, (_match, key) => {
|
|
16
|
+
if (!(key in vars)) {
|
|
17
|
+
missing.add(key);
|
|
18
|
+
return `{{${key}}}`;
|
|
19
|
+
}
|
|
20
|
+
return vars[key] ?? '';
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
function renderUnknown(value, vars, missing) {
|
|
24
|
+
if (typeof value === 'string')
|
|
25
|
+
return renderString(value, vars, missing);
|
|
26
|
+
if (Array.isArray(value))
|
|
27
|
+
return value.map((item) => renderUnknown(item, vars, missing));
|
|
28
|
+
if (value && typeof value === 'object') {
|
|
29
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, renderUnknown(item, vars, missing)]));
|
|
30
|
+
}
|
|
31
|
+
return value;
|
|
32
|
+
}
|
|
33
|
+
export function renderTemplate(template, vars) {
|
|
34
|
+
const missing = new Set();
|
|
35
|
+
const message = renderString(template.message ?? '', vars, missing);
|
|
36
|
+
const headerFooter = renderUnknown(template.headerFooter, vars, missing);
|
|
37
|
+
if (missing.size > 0) {
|
|
38
|
+
throw new ApiError('user_error', `Missing template variables: ${Array.from(missing).sort().join(', ')}`);
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
id: template.id,
|
|
42
|
+
name: template.name,
|
|
43
|
+
message,
|
|
44
|
+
headerFooter,
|
|
45
|
+
};
|
|
46
|
+
}
|