wizards 0.4.1 → 0.4.3

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 (81) hide show
  1. package/apps/runtime/dist/docs/dev/plugins/reloading.md +3 -3
  2. package/apps/runtime/dist/docs/dev/plugins/runners.md +9 -12
  3. package/apps/runtime/dist/docs/user/test-and-share.md +22 -58
  4. package/apps/runtime/dist/media/conversation.js +7 -1
  5. package/apps/web/dist/assets/{AboutDialog-CXEpsI6L.js → AboutDialog-DWaBKvI6.js} +1 -1
  6. package/apps/web/dist/assets/{ChangelogDialog-Cc2APUGI.js → ChangelogDialog-BcM1gXPM.js} +1 -1
  7. package/apps/web/dist/assets/{ChatView-B2r4dW_m.js → ChatView-D0RRIl15.js} +2 -2
  8. package/apps/web/dist/assets/{EditorPage-CNAYlSms.js → EditorPage-DrJ6e_2q.js} +1 -1
  9. package/apps/web/dist/assets/{FlowPreview-DzU1nGDb.js → FlowPreview-S1s1yRaP.js} +1 -1
  10. package/apps/web/dist/assets/{Harness-ALQ9Csm6.js → Harness-DILx5Edb.js} +1 -1
  11. package/apps/web/dist/assets/{NewWizardChannels-DMPqHkCF.js → NewWizardChannels-CEILDADp.js} +1 -1
  12. package/apps/web/dist/assets/{NewWizardPage-pi053e8N.js → NewWizardPage-DxOtK2E-.js} +1 -1
  13. package/apps/web/dist/assets/{OnboardingPage-TYNX6Umd.js → OnboardingPage-xoGrKIJ-.js} +1 -1
  14. package/apps/web/dist/assets/{ProjectPage-CvEkx_Ux.js → ProjectPage-6E5z4nZA.js} +1 -1
  15. package/apps/web/dist/assets/{PublicRunner-BrivKNHo.js → PublicRunner-Cab2jUy6.js} +1 -1
  16. package/apps/web/dist/assets/{RunDrawer-3osugizk.js → RunDrawer-B3FNYpgZ.js} +1 -1
  17. package/apps/web/dist/assets/{RunnerView-Ci4_WzVv.js → RunnerView-D6sGVnRy.js} +1 -1
  18. package/apps/web/dist/assets/{SettingsPage-DKntf7-9.js → SettingsPage-DHtIqmOJ.js} +1 -1
  19. package/apps/web/dist/assets/{SetupPage-DZQxEQyk.js → SetupPage-Cn9iQbJx.js} +1 -1
  20. package/apps/web/dist/assets/{SharePage-B9l4KthU.js → SharePage-CMr72PeU.js} +1 -1
  21. package/apps/web/dist/assets/changelog-DfmWTlav.js +1 -0
  22. package/apps/web/dist/assets/{esm-CuqXcPS3.js → esm-BPfTI3lL.js} +1 -1
  23. package/apps/web/dist/assets/index-B-D8UvL9.js +14 -0
  24. package/apps/web/dist/assets/{settings-menu-C7VsRpe9.js → settings-menu-BJm0CwOj.js} +1 -1
  25. package/apps/web/dist/index.html +1 -1
  26. package/package.json +1 -1
  27. package/apps/web/dist/assets/changelog-CxdXZo3V.js +0 -1
  28. package/apps/web/dist/assets/index-j09nLtEs.js +0 -14
  29. package/modules/calls/dist/client.css +0 -3
  30. package/modules/calls/dist/client.js +0 -1
  31. package/modules/calls/engenty.plugin.json +0 -8
  32. package/modules/calls/migrations/0001_init.sql +0 -26
  33. package/modules/calls/package.json +0 -25
  34. package/modules/calls/src/openai.ts +0 -155
  35. package/modules/calls/src/plugin.ts +0 -407
  36. package/modules/calls/src/rules.ts +0 -226
  37. package/modules/calls/src/schema.ts +0 -39
  38. package/modules/calls/src/session.ts +0 -353
  39. package/modules/calls/src/store.ts +0 -124
  40. package/modules/calls/src/twilio.ts +0 -90
  41. package/modules/calls/tsconfig.json +0 -10
  42. package/modules/calls/ui/Settings.tsx +0 -137
  43. package/modules/calls/ui/app.ts +0 -50
  44. package/modules/calls/ui/messages.ts +0 -81
  45. package/modules/calls/ui/parts.tsx +0 -36
  46. package/modules/calls/ui/plugin.tsx +0 -15
  47. package/modules/sms/dist/client.css +0 -3
  48. package/modules/sms/dist/client.js +0 -1
  49. package/modules/sms/engenty.plugin.json +0 -8
  50. package/modules/sms/migrations/0001_init.sql +0 -26
  51. package/modules/sms/package.json +0 -24
  52. package/modules/sms/src/plugin.ts +0 -329
  53. package/modules/sms/src/schema.ts +0 -26
  54. package/modules/sms/src/store.ts +0 -140
  55. package/modules/sms/src/surface.ts +0 -70
  56. package/modules/sms/src/twilio.ts +0 -130
  57. package/modules/sms/tsconfig.json +0 -10
  58. package/modules/sms/ui/Settings.tsx +0 -113
  59. package/modules/sms/ui/Share.tsx +0 -60
  60. package/modules/sms/ui/app.ts +0 -51
  61. package/modules/sms/ui/messages.ts +0 -70
  62. package/modules/sms/ui/parts.tsx +0 -36
  63. package/modules/sms/ui/plugin.tsx +0 -20
  64. package/modules/whatsapp/dist/client.css +0 -3
  65. package/modules/whatsapp/dist/client.js +0 -1
  66. package/modules/whatsapp/engenty.plugin.json +0 -8
  67. package/modules/whatsapp/migrations/0001_init.sql +0 -30
  68. package/modules/whatsapp/package.json +0 -24
  69. package/modules/whatsapp/src/graph.ts +0 -271
  70. package/modules/whatsapp/src/inbound.ts +0 -143
  71. package/modules/whatsapp/src/plugin.ts +0 -374
  72. package/modules/whatsapp/src/schema.ts +0 -32
  73. package/modules/whatsapp/src/store.ts +0 -156
  74. package/modules/whatsapp/src/surface.ts +0 -127
  75. package/modules/whatsapp/tsconfig.json +0 -10
  76. package/modules/whatsapp/ui/Settings.tsx +0 -141
  77. package/modules/whatsapp/ui/Share.tsx +0 -60
  78. package/modules/whatsapp/ui/app.ts +0 -54
  79. package/modules/whatsapp/ui/messages.ts +0 -80
  80. package/modules/whatsapp/ui/parts.tsx +0 -36
  81. package/modules/whatsapp/ui/plugin.tsx +0 -20
@@ -1,271 +0,0 @@
1
- /**
2
- * Meta's WhatsApp Cloud API, the part this plugin uses: sending a message to a number, marking
3
- * one read, and fetching a file the person sent. The tenant's own number and token; the limits
4
- * of the API (how long a button may be, how many rows a list holds) are kept here.
5
- */
6
-
7
- export const GRAPH = "https://graph.facebook.com/v22.0";
8
-
9
- export interface Account {
10
- phoneNumberId: string;
11
- number: string;
12
- accessToken: string;
13
- appSecret: string | null;
14
- verifyToken: string;
15
- template: string | null;
16
- }
17
-
18
- /** Three buttons of twenty characters at most; a list holds ten rows of twenty-four. */
19
- export const LIMITS = {
20
- buttons: 3,
21
- buttonTitle: 20,
22
- rows: 10,
23
- rowTitle: 24,
24
- rowDescription: 72,
25
- listButton: 20,
26
- body: 1024,
27
- text: 4096,
28
- caption: 1024,
29
- header: 60,
30
- } as const;
31
-
32
- export type Outgoing =
33
- | { type: "text"; text: string; preview?: boolean }
34
- | { type: "buttons"; text: string; buttons: { id: string; title: string }[]; header?: string }
35
- | {
36
- type: "list";
37
- text: string;
38
- button: string;
39
- rows: { id: string; title: string; description?: string }[];
40
- header?: string;
41
- }
42
- | { type: "image"; link: string; caption?: string }
43
- | { type: "document"; link: string; caption?: string; filename?: string }
44
- | { type: "video"; link: string; caption?: string }
45
- | { type: "audio"; link: string }
46
- | { type: "link"; text: string; url: string; label: string }
47
- | { type: "location_request"; text: string }
48
- | { type: "template"; name: string; lang: string; params: string[] };
49
-
50
- export class GraphError extends Error {
51
- constructor(
52
- message: string,
53
- readonly status: number,
54
- readonly code: number | null,
55
- ) {
56
- super(message);
57
- this.name = "GraphError";
58
- }
59
- }
60
-
61
- /** Outside the 24-hour window a free-form message is refused with this code. */
62
- export const RE_ENGAGEMENT = 131047;
63
-
64
- /** Cut to a length, with an ellipsis where something was cut. */
65
- export function clip(text: string, max: number): string {
66
- const t = text.trim();
67
- return t.length > max ? `${t.slice(0, max - 1).trimEnd()}…` : t;
68
- }
69
-
70
- function body(to: string, message: Outgoing): Record<string, unknown> {
71
- const base = { messaging_product: "whatsapp", recipient_type: "individual", to };
72
- switch (message.type) {
73
- case "text":
74
- return {
75
- ...base,
76
- type: "text",
77
- text: { body: clip(message.text, LIMITS.text), preview_url: message.preview ?? false },
78
- };
79
- case "buttons":
80
- return {
81
- ...base,
82
- type: "interactive",
83
- interactive: {
84
- type: "button",
85
- ...(message.header
86
- ? { header: { type: "text", text: clip(message.header, LIMITS.header) } }
87
- : {}),
88
- body: { text: clip(message.text, LIMITS.body) },
89
- action: {
90
- buttons: message.buttons.slice(0, LIMITS.buttons).map((b) => ({
91
- type: "reply",
92
- reply: { id: b.id, title: clip(b.title, LIMITS.buttonTitle) },
93
- })),
94
- },
95
- },
96
- };
97
- case "list":
98
- return {
99
- ...base,
100
- type: "interactive",
101
- interactive: {
102
- type: "list",
103
- ...(message.header
104
- ? { header: { type: "text", text: clip(message.header, LIMITS.header) } }
105
- : {}),
106
- body: { text: clip(message.text, LIMITS.body) },
107
- action: {
108
- button: clip(message.button, LIMITS.listButton),
109
- sections: [
110
- {
111
- rows: message.rows.slice(0, LIMITS.rows).map((r) => ({
112
- id: r.id,
113
- title: clip(r.title, LIMITS.rowTitle),
114
- ...(r.description
115
- ? { description: clip(r.description, LIMITS.rowDescription) }
116
- : {}),
117
- })),
118
- },
119
- ],
120
- },
121
- },
122
- };
123
- case "image":
124
- return {
125
- ...base,
126
- type: "image",
127
- image: {
128
- link: message.link,
129
- ...(message.caption ? { caption: clip(message.caption, LIMITS.caption) } : {}),
130
- },
131
- };
132
- case "document":
133
- return {
134
- ...base,
135
- type: "document",
136
- document: {
137
- link: message.link,
138
- ...(message.caption ? { caption: clip(message.caption, LIMITS.caption) } : {}),
139
- ...(message.filename ? { filename: message.filename } : {}),
140
- },
141
- };
142
- case "video":
143
- return {
144
- ...base,
145
- type: "video",
146
- video: {
147
- link: message.link,
148
- ...(message.caption ? { caption: clip(message.caption, LIMITS.caption) } : {}),
149
- },
150
- };
151
- case "audio":
152
- return { ...base, type: "audio", audio: { link: message.link } };
153
- case "link":
154
- return {
155
- ...base,
156
- type: "interactive",
157
- interactive: {
158
- type: "cta_url",
159
- body: { text: clip(message.text, LIMITS.body) },
160
- action: {
161
- name: "cta_url",
162
- parameters: { display_text: clip(message.label, LIMITS.buttonTitle), url: message.url },
163
- },
164
- },
165
- };
166
- case "location_request":
167
- return {
168
- ...base,
169
- type: "interactive",
170
- interactive: {
171
- type: "location_request_message",
172
- body: { text: clip(message.text, LIMITS.body) },
173
- action: { name: "send_location" },
174
- },
175
- };
176
- case "template":
177
- return {
178
- ...base,
179
- type: "template",
180
- template: {
181
- name: message.name,
182
- language: { code: message.lang },
183
- components: message.params.length
184
- ? [
185
- {
186
- type: "body",
187
- parameters: message.params.map((text) => ({ type: "text", text })),
188
- },
189
- ]
190
- : [],
191
- },
192
- };
193
- }
194
- }
195
-
196
- export class Graph {
197
- constructor(
198
- private readonly account: Account,
199
- private readonly fetchImpl: typeof fetch = (...args) => globalThis.fetch(...args),
200
- ) {}
201
-
202
- private async call(path: string, init: RequestInit): Promise<Record<string, unknown>> {
203
- const res = await this.fetchImpl(`${GRAPH}/${path}`, {
204
- ...init,
205
- headers: {
206
- authorization: `Bearer ${this.account.accessToken}`,
207
- "content-type": "application/json",
208
- ...(init.headers ?? {}),
209
- },
210
- signal: AbortSignal.timeout(20_000),
211
- });
212
- const text = await res.text();
213
- let parsed: Record<string, unknown> = {};
214
- try {
215
- parsed = text ? (JSON.parse(text) as Record<string, unknown>) : {};
216
- } catch {
217
- parsed = {};
218
- }
219
- if (!res.ok) {
220
- const error = (parsed.error ?? {}) as { message?: string; code?: number };
221
- throw new GraphError(
222
- error.message ?? `WhatsApp answered ${res.status}.`,
223
- res.status,
224
- typeof error.code === "number" ? error.code : null,
225
- );
226
- }
227
- return parsed;
228
- }
229
-
230
- /** Sends one message; gives the message's id. */
231
- async send(to: string, message: Outgoing): Promise<string | null> {
232
- const answer = await this.call(`${this.account.phoneNumberId}/messages`, {
233
- method: "POST",
234
- body: JSON.stringify(body(to, message)),
235
- });
236
- const messages = answer.messages as { id?: string }[] | undefined;
237
- return messages?.[0]?.id ?? null;
238
- }
239
-
240
- /** The two ticks turn blue: the person sees their message arrived. */
241
- async markRead(messageId: string): Promise<void> {
242
- await this.call(`${this.account.phoneNumberId}/messages`, {
243
- method: "POST",
244
- body: JSON.stringify({
245
- messaging_product: "whatsapp",
246
- status: "read",
247
- message_id: messageId,
248
- }),
249
- });
250
- }
251
-
252
- /** A file the person sent, by the id the webhook named: its bytes and type. */
253
- async media(id: string): Promise<{ data: Uint8Array; mime: string }> {
254
- const meta = await this.call(id, { method: "GET" });
255
- const url = typeof meta.url === "string" ? meta.url : null;
256
- if (!url) {
257
- throw new GraphError("The file has no address.", 404, null);
258
- }
259
- const res = await this.fetchImpl(url, {
260
- headers: { authorization: `Bearer ${this.account.accessToken}` },
261
- signal: AbortSignal.timeout(60_000),
262
- });
263
- if (!res.ok) {
264
- throw new GraphError(`The file could not be fetched (${res.status}).`, res.status, null);
265
- }
266
- const mime =
267
- (typeof meta.mime_type === "string" ? meta.mime_type : res.headers.get("content-type")) ??
268
- "application/octet-stream";
269
- return { data: new Uint8Array(await res.arrayBuffer()), mime: mime.split(";")[0].trim() };
270
- }
271
- }
@@ -1,143 +0,0 @@
1
- import { createHmac, timingSafeEqual } from "node:crypto";
2
-
3
- /**
4
- * What Meta posts to the webhook, read into one shape per message: text, a tap on a button or
5
- * a row, a file, a location. Statuses (sent, delivered, read) are left out.
6
- */
7
-
8
- export interface Inbound {
9
- id: string;
10
- /** The person's number, as WhatsApp names it (digits, no plus). */
11
- from: string;
12
- name: string | null;
13
- /** The number it was sent to, by its id. */
14
- phoneNumberId: string;
15
- at: number;
16
- kind: "text" | "reply" | "image" | "document" | "video" | "audio" | "location" | "other";
17
- text?: string;
18
- /** The id of the button or the row the person tapped. */
19
- replyId?: string;
20
- replyTitle?: string;
21
- media?: { id: string; mime: string; filename?: string; caption?: string };
22
- location?: { lat: number; lng: number; name?: string; address?: string };
23
- }
24
-
25
- type Rec = Record<string, unknown>;
26
- const rec = (v: unknown): Rec =>
27
- v && typeof v === "object" && !Array.isArray(v) ? (v as Rec) : {};
28
- const str = (v: unknown): string | undefined => (typeof v === "string" && v ? v : undefined);
29
-
30
- function oneMessage(m: Rec, phoneNumberId: string, name: string | null): Inbound | null {
31
- const id = str(m.id);
32
- const from = str(m.from);
33
- if (!id || !from) {
34
- return null;
35
- }
36
- const at = Number(m.timestamp) * 1000 || Date.now();
37
- const base = { id, from, name, phoneNumberId, at };
38
- switch (m.type) {
39
- case "text":
40
- return { ...base, kind: "text", text: str(rec(m.text).body) ?? "" };
41
- case "interactive": {
42
- const i = rec(m.interactive);
43
- const reply = rec(i.button_reply ?? i.list_reply);
44
- return {
45
- ...base,
46
- kind: "reply",
47
- replyId: str(reply.id) ?? "",
48
- replyTitle: str(reply.title),
49
- text: str(reply.title),
50
- };
51
- }
52
- case "button":
53
- // A tap on a template's quick-reply button.
54
- return {
55
- ...base,
56
- kind: "reply",
57
- replyId: str(rec(m.button).payload) ?? "",
58
- text: str(rec(m.button).text),
59
- };
60
- case "image":
61
- case "document":
62
- case "video":
63
- case "audio": {
64
- const file = rec(m[m.type]);
65
- const mediaId = str(file.id);
66
- if (!mediaId) {
67
- return null;
68
- }
69
- return {
70
- ...base,
71
- kind: m.type,
72
- media: {
73
- id: mediaId,
74
- mime: str(file.mime_type) ?? "application/octet-stream",
75
- filename: str(file.filename),
76
- caption: str(file.caption),
77
- },
78
- text: str(file.caption),
79
- };
80
- }
81
- case "location": {
82
- const l = rec(m.location);
83
- const lat = Number(l.latitude);
84
- const lng = Number(l.longitude);
85
- if (!(Number.isFinite(lat) && Number.isFinite(lng))) {
86
- return null;
87
- }
88
- return {
89
- ...base,
90
- kind: "location",
91
- location: { lat, lng, name: str(l.name), address: str(l.address) },
92
- };
93
- }
94
- default:
95
- return { ...base, kind: "other" };
96
- }
97
- }
98
-
99
- /** Every message in a webhook's body, oldest first. */
100
- export function parseWebhook(body: unknown): Inbound[] {
101
- const out: Inbound[] = [];
102
- const root = rec(body);
103
- if (root.object !== "whatsapp_business_account") {
104
- return out;
105
- }
106
- for (const entry of Array.isArray(root.entry) ? root.entry : []) {
107
- for (const change of Array.isArray(rec(entry).changes)
108
- ? (rec(entry).changes as unknown[])
109
- : []) {
110
- const value = rec(rec(change).value);
111
- if (rec(change).field !== "messages") {
112
- continue;
113
- }
114
- const phoneNumberId = str(rec(value.metadata).phone_number_id) ?? "";
115
- const contacts = Array.isArray(value.contacts) ? (value.contacts as unknown[]) : [];
116
- const names = new Map(
117
- contacts.map((c) => [str(rec(c).wa_id) ?? "", str(rec(rec(c).profile).name) ?? null]),
118
- );
119
- for (const m of Array.isArray(value.messages) ? (value.messages as unknown[]) : []) {
120
- const message = rec(m);
121
- const parsed = oneMessage(
122
- message,
123
- phoneNumberId,
124
- names.get(str(message.from) ?? "") ?? null,
125
- );
126
- if (parsed) {
127
- out.push(parsed);
128
- }
129
- }
130
- }
131
- }
132
- return out.sort((a, b) => a.at - b.at);
133
- }
134
-
135
- /** `X-Hub-Signature-256` is the HMAC of the raw body with the app's secret. */
136
- export function signatureOk(secret: string, raw: string, header: string | null): boolean {
137
- if (!header?.startsWith("sha256=")) {
138
- return false;
139
- }
140
- const expected = Buffer.from(createHmac("sha256", secret).update(raw, "utf8").digest("hex"));
141
- const given = Buffer.from(header.slice("sha256=".length));
142
- return expected.length === given.length && timingSafeEqual(expected, given);
143
- }