@fonoster/common 0.27.0 → 0.30.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.
Files changed (176) hide show
  1. package/dist/assistants/index.d.ts +2 -6
  2. package/dist/assistants/index.js +2 -6
  3. package/dist/assistants/sessionAssistantSchema.d.ts +741 -0
  4. package/dist/assistants/sessionAssistantSchema.js +112 -0
  5. package/dist/assistants/testCasesSchema.d.ts +4 -4
  6. package/dist/{voice/Dial.d.ts → calls/index.d.ts} +16 -13
  7. package/dist/calls/index.js +44 -0
  8. package/dist/cellsecrets/index.d.ts +91 -0
  9. package/dist/cellsecrets/index.js +299 -0
  10. package/dist/config/cli/check.d.ts +6 -0
  11. package/dist/config/cli/check.js +202 -0
  12. package/dist/config/cli/envFile.d.ts +23 -0
  13. package/dist/config/cli/envFile.js +81 -0
  14. package/dist/config/cli/exposure.d.ts +10 -0
  15. package/dist/config/cli/exposure.js +93 -0
  16. package/dist/config/cli/index.d.ts +2 -0
  17. package/dist/{identity/tokenHasAccessKeyId.js → config/cli/index.js} +7 -8
  18. package/dist/config/cli/info.d.ts +6 -0
  19. package/dist/config/cli/info.js +62 -0
  20. package/dist/config/cli/init.d.ts +9 -0
  21. package/dist/config/cli/init.js +152 -0
  22. package/dist/config/cli/main.d.ts +11 -0
  23. package/dist/config/cli/main.js +155 -0
  24. package/dist/config/cli/render.d.ts +44 -0
  25. package/dist/config/cli/render.js +129 -0
  26. package/dist/config/cli/secrets.d.ts +21 -0
  27. package/dist/config/cli/secrets.js +117 -0
  28. package/dist/{utils/findIntegrationsCredentials.d.ts → config/cli/types.d.ts} +6 -5
  29. package/dist/config/cli/types.js +7 -0
  30. package/dist/config/configHints.d.ts +39 -0
  31. package/dist/config/configHints.js +61 -0
  32. package/dist/{voice → config}/index.d.ts +12 -13
  33. package/dist/config/index.js +46 -0
  34. package/dist/config/integrationsSchema.d.ts +32 -0
  35. package/dist/config/integrationsSchema.js +56 -0
  36. package/dist/config/interpolateEnv.d.ts +34 -0
  37. package/dist/config/interpolateEnv.js +70 -0
  38. package/dist/config/loadServiceConfig.d.ts +33 -0
  39. package/dist/config/loadServiceConfig.js +121 -0
  40. package/dist/config/logsSchema.d.ts +76 -0
  41. package/dist/config/logsSchema.js +49 -0
  42. package/dist/config/natsConnection.d.ts +53 -0
  43. package/dist/config/natsConnection.js +58 -0
  44. package/dist/config/parseConfigFile.d.ts +7 -0
  45. package/dist/config/parseConfigFile.js +60 -0
  46. package/dist/config/resolveConfigFile.d.ts +17 -0
  47. package/dist/config/resolveConfigFile.js +54 -0
  48. package/dist/config/resolveSecretRefs.d.ts +35 -0
  49. package/dist/config/resolveSecretRefs.js +129 -0
  50. package/dist/{assistants/audioFiltersSchema.d.ts → config/strictDeep.d.ts} +9 -12
  51. package/dist/config/strictDeep.js +58 -0
  52. package/dist/config/telemetrySchema.d.ts +39 -0
  53. package/dist/config/telemetrySchema.js +44 -0
  54. package/dist/config/toJsonSchema.d.ts +30 -0
  55. package/dist/config/toJsonSchema.js +161 -0
  56. package/dist/constants.d.ts +1 -4
  57. package/dist/constants.js +4 -5
  58. package/dist/envs.d.ts +2 -1
  59. package/dist/envs.js +13 -11
  60. package/dist/identity/createAuthInterceptor.d.ts +9 -2
  61. package/dist/identity/createAuthInterceptor.js +36 -30
  62. package/dist/identity/hasAccess.d.ts +9 -3
  63. package/dist/identity/hasAccess.js +32 -8
  64. package/dist/identity/index.d.ts +1 -0
  65. package/dist/identity/index.js +1 -0
  66. package/dist/identity/isValidToken.d.ts +12 -2
  67. package/dist/identity/isValidToken.js +25 -9
  68. package/dist/identity/roles.js +22 -6
  69. package/dist/identity/types.d.ts +6 -1
  70. package/dist/identity/types.js +1 -0
  71. package/dist/{assistants/LanguageModelProvider.js → identity/verifyAccessToken.d.ts} +10 -11
  72. package/dist/identity/verifyAccessToken.js +44 -0
  73. package/dist/index.d.ts +11 -1
  74. package/dist/index.js +14 -2
  75. package/dist/net/assertPublicDestination.d.ts +52 -0
  76. package/dist/net/assertPublicDestination.js +342 -0
  77. package/dist/{voice/PlayDtmf.d.ts → net/index.d.ts} +1 -5
  78. package/dist/{voice → net}/index.js +1 -13
  79. package/dist/notifications/createSendEmail.js +3 -1
  80. package/dist/protos/applications.proto +124 -3
  81. package/dist/protos/authz.proto +23 -4
  82. package/dist/protos/capabilities.proto +94 -0
  83. package/dist/protos/identity.proto +26 -1
  84. package/dist/protos/providers.proto +135 -0
  85. package/dist/protos/webhooks.proto +140 -0
  86. package/dist/{voice/StopSay.d.ts → regions/hub/index.d.ts} +1 -6
  87. package/dist/regions/hub/index.js +35 -0
  88. package/dist/regions/hub/natsJwt.d.ts +63 -0
  89. package/dist/regions/hub/natsJwt.js +195 -0
  90. package/dist/registry/Registry.d.ts +79 -0
  91. package/dist/registry/Registry.js +418 -0
  92. package/dist/registry/cli.d.ts +20 -0
  93. package/dist/registry/cli.js +424 -0
  94. package/dist/{assistants/audioFiltersSchema.js → registry/features.d.ts} +13 -14
  95. package/dist/registry/features.js +36 -0
  96. package/dist/{assistants/LanguageModelProvider.d.ts → registry/index.d.ts} +6 -8
  97. package/dist/registry/index.js +40 -0
  98. package/dist/registry/prisma.d.ts +67 -0
  99. package/dist/registry/prisma.js +391 -0
  100. package/dist/registry/stores.d.ts +21 -0
  101. package/dist/{assistants/eventsHookSchema.js → registry/stores.js} +55 -18
  102. package/dist/registry/types.d.ts +98 -0
  103. package/dist/{voice/Say.d.ts → registry/types.js} +4 -10
  104. package/dist/registry/workspaceKeys.d.ts +41 -0
  105. package/dist/registry/workspaceKeys.js +64 -0
  106. package/dist/session/index.d.ts +19 -0
  107. package/dist/session/index.js +35 -0
  108. package/dist/session/schema.d.ts +1951 -0
  109. package/dist/session/schema.js +397 -0
  110. package/dist/sessions/index.d.ts +93 -0
  111. package/dist/sessions/index.js +194 -0
  112. package/dist/telemetry.d.ts +76 -0
  113. package/dist/telemetry.js +360 -0
  114. package/dist/types.d.ts +3 -5
  115. package/dist/utils/{findIntegrationsCredentials.js → WriteBehind.d.ts} +13 -10
  116. package/dist/utils/WriteBehind.js +64 -0
  117. package/dist/utils/index.d.ts +1 -1
  118. package/dist/utils/index.js +1 -1
  119. package/dist/{voice/Play.d.ts → utils/logUnhandledRejections.d.ts} +8 -12
  120. package/dist/utils/logUnhandledRejections.js +37 -0
  121. package/dist/validators/calls.d.ts +2 -2
  122. package/dist/validators/calls.js +5 -2
  123. package/dist/validators/identity.d.ts +37 -8
  124. package/dist/validators/identity.js +22 -3
  125. package/dist/validators/index.d.ts +1 -0
  126. package/dist/validators/index.js +1 -0
  127. package/dist/validators/sip.d.ts +40 -0
  128. package/dist/validators/sip.js +99 -0
  129. package/dist/validators/sipnet/credentials.d.ts +2 -2
  130. package/dist/validators/sipnet/domains.js +7 -7
  131. package/dist/validators/sipnet/trunks.d.ts +14 -14
  132. package/dist/validators/sipnet/trunks.js +4 -2
  133. package/dist/webhooks/Dispatcher.d.ts +87 -0
  134. package/dist/webhooks/Dispatcher.js +225 -0
  135. package/dist/webhooks/bus.d.ts +49 -0
  136. package/dist/webhooks/bus.js +170 -0
  137. package/dist/webhooks/events.d.ts +66 -0
  138. package/dist/webhooks/events.js +62 -0
  139. package/dist/webhooks/index.d.ts +23 -0
  140. package/dist/webhooks/index.js +39 -0
  141. package/dist/webhooks/prisma.d.ts +55 -0
  142. package/dist/webhooks/prisma.js +242 -0
  143. package/dist/webhooks/signing.d.ts +8 -0
  144. package/dist/webhooks/signing.js +53 -0
  145. package/package.json +25 -12
  146. package/schema/session.v1.json +791 -0
  147. package/dist/assistants/assistantSchema.d.ts +0 -739
  148. package/dist/assistants/assistantSchema.js +0 -37
  149. package/dist/assistants/conversationSettingsSchema.d.ts +0 -122
  150. package/dist/assistants/conversationSettingsSchema.js +0 -133
  151. package/dist/assistants/eventsHookSchema.d.ts +0 -38
  152. package/dist/assistants/languageModelConfigSchema.d.ts +0 -206
  153. package/dist/assistants/languageModelConfigSchema.js +0 -91
  154. package/dist/identity/tokenHasAccessKeyId.d.ts +0 -2
  155. package/dist/protos/voice.proto +0 -557
  156. package/dist/voice/Dial.js +0 -20
  157. package/dist/voice/Gather.d.ts +0 -37
  158. package/dist/voice/Gather.js +0 -9
  159. package/dist/voice/Mute.d.ts +0 -31
  160. package/dist/voice/Mute.js +0 -9
  161. package/dist/voice/Play.js +0 -2
  162. package/dist/voice/PlayDtmf.js +0 -2
  163. package/dist/voice/PlaybackControl.d.ts +0 -31
  164. package/dist/voice/PlaybackControl.js +0 -11
  165. package/dist/voice/Record.d.ts +0 -36
  166. package/dist/voice/Record.js +0 -7
  167. package/dist/voice/Say.js +0 -2
  168. package/dist/voice/StopSay.js +0 -2
  169. package/dist/voice/Stream.d.ts +0 -53
  170. package/dist/voice/Stream.js +0 -19
  171. package/dist/voice/StreamGather.d.ts +0 -34
  172. package/dist/voice/StreamGather.js +0 -9
  173. package/dist/voice/Verb.d.ts +0 -27
  174. package/dist/voice/Verb.js +0 -2
  175. package/dist/voice/voice.d.ts +0 -171
  176. package/dist/voice/voice.js +0 -65
@@ -0,0 +1,225 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.WebhookDispatcher = exports.WebhookError = exports.MemoryWebhookStore = void 0;
13
+ exports.deliveryId = deliveryId;
14
+ /**
15
+ * Copyright (C) 2025 by Fonoster Inc (https://fonoster.com)
16
+ * http://github.com/fonoster/fonoster
17
+ *
18
+ * This file is part of Fonoster
19
+ *
20
+ * Licensed under the MIT License (the "License");
21
+ * you may not use this file except in compliance with
22
+ * the License. You may obtain a copy of the License at
23
+ *
24
+ * https://opensource.org/licenses/MIT
25
+ *
26
+ * Unless required by applicable law or agreed to in writing, software
27
+ * distributed under the License is distributed on an "AS IS" BASIS,
28
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
29
+ * See the License for the specific language governing permissions and
30
+ * limitations under the License.
31
+ */
32
+ const crypto_1 = require("crypto");
33
+ const url_1 = require("url");
34
+ const net_1 = require("../net");
35
+ const events_1 = require("./events");
36
+ const signing_1 = require("./signing");
37
+ class MemoryWebhookStore {
38
+ constructor() {
39
+ this.e = new Map();
40
+ this.d = new Map();
41
+ }
42
+ endpoints(workspaceRef) {
43
+ return [...this.e.values()].filter((x) => !workspaceRef || x.workspaceRef === workspaceRef);
44
+ }
45
+ putEndpoint(x) {
46
+ this.e.set(x.id, x);
47
+ }
48
+ deleteEndpoint(id) {
49
+ this.e.delete(id);
50
+ }
51
+ deliveries(endpointId) {
52
+ return [...this.d.values()].filter((x) => !endpointId || x.endpointId === endpointId);
53
+ }
54
+ putDelivery(x) {
55
+ this.d.set(x.id, x);
56
+ }
57
+ }
58
+ exports.MemoryWebhookStore = MemoryWebhookStore;
59
+ class WebhookError extends Error {
60
+ }
61
+ exports.WebhookError = WebhookError;
62
+ /**
63
+ * The id of an event's delivery to one endpoint. It depends only on the two, so the same event
64
+ * fanned out again (a bus message redelivered after a replica died, or published twice) finds
65
+ * the delivery it already made instead of making a second one (ST-2).
66
+ */
67
+ function deliveryId(eventId, endpointId) {
68
+ const h = (0, crypto_1.createHash)("sha256")
69
+ .update(`${eventId}\0${endpointId}`)
70
+ .digest("hex");
71
+ return `whd_${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20, 32)}`;
72
+ }
73
+ class WebhookDispatcher {
74
+ constructor(options) {
75
+ this.options = options;
76
+ }
77
+ policy() {
78
+ return this.options.allowedDestinations
79
+ ? { allow: this.options.allowedDestinations }
80
+ : undefined;
81
+ }
82
+ now() {
83
+ var _a, _b, _c;
84
+ return (_c = (_b = (_a = this.options).now) === null || _b === void 0 ? void 0 : _b.call(_a)) !== null && _c !== void 0 ? _c : Date.now();
85
+ }
86
+ /** Registers an endpoint; the returned secret is the only time it is shown. */
87
+ createEndpoint(input) {
88
+ let url;
89
+ try {
90
+ url = new url_1.URL(input.url);
91
+ }
92
+ catch (_a) {
93
+ throw new WebhookError("invalid webhook URL");
94
+ }
95
+ if (url.protocol !== "https:" &&
96
+ !(this.options.allowInsecureUrls && url.protocol === "http:")) {
97
+ throw new WebhookError("webhook URLs must use HTTPS");
98
+ }
99
+ try {
100
+ (0, net_1.assertPublicUrlShape)(url, this.policy());
101
+ }
102
+ catch (e) {
103
+ throw new WebhookError(`webhook URL not allowed: ${e.message}`);
104
+ }
105
+ const unknown = input.events.filter((e) => !events_1.WEBHOOK_EVENT_TYPES.includes(e));
106
+ const retired = unknown.filter((e) => events_1.RETIRED_EVENT_TYPES[e]);
107
+ if (retired.length)
108
+ throw new WebhookError(`retired event types: ${retired
109
+ .map((e) => `${e} (use ${events_1.RETIRED_EVENT_TYPES[e].join(" or ")})`)
110
+ .join(", ")}; the conversation's transcript and recording are in the call record`);
111
+ if (unknown.length)
112
+ throw new WebhookError(`unknown event types: ${unknown.join(", ")}`);
113
+ const endpoint = {
114
+ id: `whe_${(0, crypto_1.randomUUID)()}`,
115
+ workspaceRef: input.workspaceRef,
116
+ url: input.url,
117
+ events: input.events,
118
+ secret: `whsec_${(0, crypto_1.randomBytes)(24).toString("base64url")}`
119
+ };
120
+ this.options.store.putEndpoint(endpoint);
121
+ return endpoint;
122
+ }
123
+ /** New secret; the old one keeps verifying for `windowMs`. Returns the new secret. */
124
+ rotateSecret(endpointId, windowMs = 24 * 3600 * 1000) {
125
+ const e = this.options.store.endpoints().find((x) => x.id === endpointId);
126
+ if (!e)
127
+ throw new WebhookError(`no endpoint ${endpointId}`);
128
+ const secret = `whsec_${(0, crypto_1.randomBytes)(24).toString("base64url")}`;
129
+ this.options.store.putEndpoint(Object.assign(Object.assign({}, e), { secret, previousSecret: e.secret, previousUntil: this.now() + windowMs }));
130
+ return secret;
131
+ }
132
+ /** The deliveries an event makes, one per subscribed endpoint of its workspace (not stored). */
133
+ plan(event) {
134
+ return this.options.store
135
+ .endpoints(event.workspaceRef)
136
+ .filter((e) => e.events.includes(event.type))
137
+ .map((e) => ({
138
+ id: deliveryId(event.id, e.id),
139
+ endpointId: e.id,
140
+ event,
141
+ status: "pending",
142
+ attempts: 0,
143
+ nextAttemptAt: this.now()
144
+ }));
145
+ }
146
+ /** Fans an event out to every endpoint of its workspace subscribed to its type. */
147
+ publish(event) {
148
+ const deliveries = this.plan(event);
149
+ deliveries.forEach((d) => this.options.store.putDelivery(d));
150
+ return deliveries;
151
+ }
152
+ /** Attempts every pending delivery that is due; returns how many were attempted. */
153
+ runDue() {
154
+ return __awaiter(this, void 0, void 0, function* () {
155
+ const due = this.options.store
156
+ .deliveries()
157
+ .filter((d) => d.status === "pending" && d.nextAttemptAt <= this.now());
158
+ yield Promise.all(due.map((d) => this.attempt(d)));
159
+ return due.length;
160
+ });
161
+ }
162
+ /** Sends a delivery again now, with the original event id and a fresh signature. */
163
+ resend(deliveryId) {
164
+ return __awaiter(this, void 0, void 0, function* () {
165
+ const d = this.options.store.deliveries().find((x) => x.id === deliveryId);
166
+ if (!d)
167
+ throw new WebhookError(`no delivery ${deliveryId}`);
168
+ const again = Object.assign(Object.assign({}, d), { status: "pending", nextAttemptAt: this.now(), firstAttemptAt: this.now() });
169
+ this.options.store.putDelivery(again);
170
+ return this.attempt(again);
171
+ });
172
+ }
173
+ attempt(d) {
174
+ return __awaiter(this, void 0, void 0, function* () {
175
+ var _a, _b, _c, _d, _e, _f;
176
+ const endpoint = this.options.store
177
+ .endpoints()
178
+ .find((e) => e.id === d.endpointId);
179
+ if (!endpoint) {
180
+ const failed = Object.assign(Object.assign({}, d), { status: "failed" });
181
+ this.options.store.putDelivery(failed);
182
+ return failed;
183
+ }
184
+ const now = this.now();
185
+ const secrets = [endpoint.secret];
186
+ if (endpoint.previousSecret && ((_a = endpoint.previousUntil) !== null && _a !== void 0 ? _a : 0) > now)
187
+ secrets.push(endpoint.previousSecret);
188
+ const body = JSON.stringify(d.event);
189
+ let status = 0;
190
+ try {
191
+ // Checked again at delivery: DNS may have changed since the endpoint was created.
192
+ const res = yield (0, net_1.safeFetch)(endpoint.url, {
193
+ method: "POST",
194
+ headers: Object.assign({ "content-type": "application/json" }, (0, signing_1.webhookHeaders)(body, secrets, Math.floor(now / 1000))),
195
+ body,
196
+ timeoutMs: (_b = this.options.attemptTimeoutMs) !== null && _b !== void 0 ? _b : 10000,
197
+ maxRedirects: 0,
198
+ policy: this.policy()
199
+ });
200
+ status = res.status;
201
+ yield res.arrayBuffer().catch(() => undefined);
202
+ }
203
+ catch (_g) {
204
+ status = 0;
205
+ }
206
+ const attempts = d.attempts + 1;
207
+ const firstAttemptAt = (_c = d.firstAttemptAt) !== null && _c !== void 0 ? _c : now;
208
+ let next;
209
+ if (status >= 200 && status < 300) {
210
+ next = Object.assign(Object.assign({}, d), { status: "succeeded", attempts,
211
+ firstAttemptAt, lastAttemptAt: now, lastStatus: status });
212
+ }
213
+ else {
214
+ const delay = Math.min(((_d = this.options.firstDelayMs) !== null && _d !== void 0 ? _d : 10000) * Math.pow(2, (attempts - 1)), (_e = this.options.maxDelayMs) !== null && _e !== void 0 ? _e : 3600000);
215
+ const giveUp = now + delay - firstAttemptAt >
216
+ ((_f = this.options.retryPeriodMs) !== null && _f !== void 0 ? _f : 24 * 3600000);
217
+ next = Object.assign(Object.assign({}, d), { status: giveUp ? "failed" : "pending", attempts,
218
+ firstAttemptAt, lastAttemptAt: now, lastStatus: status, nextAttemptAt: now + delay });
219
+ }
220
+ this.options.store.putDelivery(next);
221
+ return next;
222
+ });
223
+ }
224
+ }
225
+ exports.WebhookDispatcher = WebhookDispatcher;
@@ -0,0 +1,49 @@
1
+ import { Delivery } from "./Dispatcher";
2
+ import { WebhookEvent } from "./events";
3
+ export declare const WEBHOOK_EVENTS_SUBJECT = "webhooks.events";
4
+ /** The slice of a NATS connection the bus uses (so `@fonoster/common` needs no NATS client). */
5
+ export type BusConnection = {
6
+ publish(subject: string, data: Uint8Array): void;
7
+ subscribe(subject: string, opts: {
8
+ queue?: string;
9
+ callback: (err: Error | null, msg: {
10
+ data: Uint8Array;
11
+ }) => void;
12
+ }): {
13
+ unsubscribe(): void;
14
+ };
15
+ };
16
+ /** A producer that publishes events on the bus instead of delivering them itself. */
17
+ export declare function busWebhookPublisher(nc: Pick<BusConnection, "publish">): (event: WebhookEvent) => void;
18
+ /** Parses a bus message; undefined for anything that is not a v1 webhook event. */
19
+ export declare function parseBusEvent(data: Uint8Array): WebhookEvent | undefined;
20
+ /** Where media nodes report calls no gateway claimed (the last token is never a room ref). */
21
+ export declare const NODE_CALL_FAILED_SUBJECT = "media.*.events.call-failed";
22
+ /** The reasons a node may give: it only ever fails a call nobody claimed. */
23
+ export declare const NODE_CALL_FAILED_REASONS: readonly ["node_error"];
24
+ /**
25
+ * The call.failed event for a media node's report on `subject`
26
+ * (`media.<nodeId>.events.call-failed`); undefined for anything else. Only `call.failed` with a
27
+ * node reason is accepted, the report must name the node of its subject, and the workspace is left
28
+ * empty for the fan-out to resolve from the app: a node never chooses the workspace, the event type
29
+ * or extra data. The id depends on the node and the call, so a repeated report is delivered once.
30
+ */
31
+ export declare function nodeCallFailedEvent(subject: string, data: Uint8Array, now?: Date): WebhookEvent | undefined;
32
+ export type FanoutOptions = {
33
+ /** Usually `WebhookDispatcher.publish`: stores one pending delivery per subscribed endpoint. */
34
+ publish(event: WebhookEvent): Delivery[];
35
+ /**
36
+ * The workspace of an app, for events whose producer did not know it (empty or `default`
37
+ * workspaceRef with a `data.appRef`).
38
+ */
39
+ workspaceOfApp?(appRef: string): Promise<string | undefined>;
40
+ /** Called after an event was fanned out (e.g. to attempt the new deliveries at once). */
41
+ onPublished?(event: WebhookEvent, deliveries: Delivery[]): void;
42
+ onError?(e: Error): void;
43
+ /** Queue group: one replica handles each event. */
44
+ queue?: string;
45
+ };
46
+ /** Fills in a missing workspace from the app; undefined when it cannot be known. */
47
+ export declare function resolveWorkspace(event: WebhookEvent, workspaceOfApp?: FanoutOptions["workspaceOfApp"]): Promise<WebhookEvent | undefined>;
48
+ /** Subscribes to the bus and fans every event out; returns the unsubscribe function. */
49
+ export declare function subscribeWebhookFanout(nc: Pick<BusConnection, "subscribe">, options: FanoutOptions): () => void;
@@ -0,0 +1,170 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.NODE_CALL_FAILED_REASONS = exports.NODE_CALL_FAILED_SUBJECT = exports.WEBHOOK_EVENTS_SUBJECT = void 0;
13
+ exports.busWebhookPublisher = busWebhookPublisher;
14
+ exports.parseBusEvent = parseBusEvent;
15
+ exports.nodeCallFailedEvent = nodeCallFailedEvent;
16
+ exports.resolveWorkspace = resolveWorkspace;
17
+ exports.subscribeWebhookFanout = subscribeWebhookFanout;
18
+ /**
19
+ * Copyright (C) 2025 by Fonoster Inc (https://fonoster.com)
20
+ * http://github.com/fonoster/fonoster
21
+ *
22
+ * This file is part of Fonoster
23
+ *
24
+ * Licensed under the MIT License (the "License");
25
+ * you may not use this file except in compliance with
26
+ * the License. You may obtain a copy of the License at
27
+ *
28
+ * https://opensource.org/licenses/MIT
29
+ *
30
+ * Unless required by applicable law or agreed to in writing, software
31
+ * distributed under the License is distributed on an "AS IS" BASIS,
32
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
33
+ * See the License for the specific language governing permissions and
34
+ * limitations under the License.
35
+ */
36
+ const crypto_1 = require("crypto");
37
+ const util_1 = require("util");
38
+ const events_1 = require("./events");
39
+ // Event fan-out from the event bus (add-workspace-webhooks task 2.2, build log LO-B1). Producers
40
+ // that have no database (media nodes, gateways in bus mode, other regions) publish events on
41
+ // `webhooks.events`; one control-plane replica per event (queue group) fans each event out to the
42
+ // workspace's subscribed endpoints. A producer that does not know the workspace leaves
43
+ // `workspaceRef` empty and the fan-out resolves it from `data.appRef`.
44
+ //
45
+ // Media nodes may not publish `webhooks.events` (M6: a node could otherwise forge any event for
46
+ // any workspace). A node reports a call no gateway claimed on its own subject,
47
+ // `media.<nodeId>.events.call-failed`; the control plane builds the call.failed event from it
48
+ // (nodeCallFailedEvent), with the workspace taken from the app and nothing else trusted.
49
+ exports.WEBHOOK_EVENTS_SUBJECT = "webhooks.events";
50
+ const encoder = new util_1.TextEncoder();
51
+ const decoder = new util_1.TextDecoder();
52
+ /** A producer that publishes events on the bus instead of delivering them itself. */
53
+ function busWebhookPublisher(nc) {
54
+ return (event) => nc.publish(exports.WEBHOOK_EVENTS_SUBJECT, encoder.encode(JSON.stringify(event)));
55
+ }
56
+ /** Parses a bus message; undefined for anything that is not a v1 webhook event. */
57
+ function parseBusEvent(data) {
58
+ let e;
59
+ try {
60
+ e = JSON.parse(decoder.decode(data));
61
+ }
62
+ catch (_a) {
63
+ return undefined;
64
+ }
65
+ if (!e ||
66
+ typeof e.id !== "string" ||
67
+ !/^evt_[\w-]+$/.test(e.id) ||
68
+ !events_1.WEBHOOK_EVENT_TYPES.includes(e.type) ||
69
+ typeof e.data !== "object" ||
70
+ e.data === null)
71
+ return undefined;
72
+ return e;
73
+ }
74
+ /** Where media nodes report calls no gateway claimed (the last token is never a room ref). */
75
+ exports.NODE_CALL_FAILED_SUBJECT = "media.*.events.call-failed";
76
+ /** The reasons a node may give: it only ever fails a call nobody claimed. */
77
+ exports.NODE_CALL_FAILED_REASONS = ["node_error"];
78
+ const MAX_FIELD = 256;
79
+ const field = (v, required = false) => typeof v === "string" && v.length <= MAX_FIELD && (!required || v.length > 0)
80
+ ? v
81
+ : undefined;
82
+ /**
83
+ * The call.failed event for a media node's report on `subject`
84
+ * (`media.<nodeId>.events.call-failed`); undefined for anything else. Only `call.failed` with a
85
+ * node reason is accepted, the report must name the node of its subject, and the workspace is left
86
+ * empty for the fan-out to resolve from the app: a node never chooses the workspace, the event type
87
+ * or extra data. The id depends on the node and the call, so a repeated report is delivered once.
88
+ */
89
+ function nodeCallFailedEvent(subject, data, now = new Date()) {
90
+ var _a;
91
+ const tokens = subject.split(".");
92
+ if (tokens.length !== 4 ||
93
+ tokens[0] !== "media" ||
94
+ tokens[2] !== "events" ||
95
+ tokens[3] !== "call-failed")
96
+ return undefined;
97
+ let r;
98
+ try {
99
+ r = JSON.parse(decoder.decode(data));
100
+ }
101
+ catch (_b) {
102
+ return undefined;
103
+ }
104
+ if (!r || typeof r !== "object" || r.type !== "call.failed")
105
+ return undefined;
106
+ const nodeId = tokens[1];
107
+ const callRef = field(r.callRef, true);
108
+ const appRef = field(r.appRef, true);
109
+ const from = field(r.from);
110
+ const to = field(r.to);
111
+ const reason = field(r.reason);
112
+ const direction = (_a = r.direction) !== null && _a !== void 0 ? _a : "inbound";
113
+ if (r.nodeId !== nodeId ||
114
+ !callRef ||
115
+ !appRef ||
116
+ from === undefined ||
117
+ to === undefined ||
118
+ !exports.NODE_CALL_FAILED_REASONS.includes(reason !== null && reason !== void 0 ? reason : "") ||
119
+ (direction !== "inbound" && direction !== "outbound"))
120
+ return undefined;
121
+ const event = (0, events_1.callFailed)("", {
122
+ callRef,
123
+ appRef,
124
+ from,
125
+ to,
126
+ direction,
127
+ reason: reason
128
+ });
129
+ const digest = (0, crypto_1.createHash)("sha256")
130
+ .update(`${nodeId}\n${callRef}`)
131
+ .digest("hex")
132
+ .slice(0, 32);
133
+ return Object.assign(Object.assign({}, event), { id: `evt_node_${digest}`, createdAt: now.toISOString() });
134
+ }
135
+ /** Fills in a missing workspace from the app; undefined when it cannot be known. */
136
+ function resolveWorkspace(event, workspaceOfApp) {
137
+ return __awaiter(this, void 0, void 0, function* () {
138
+ if (event.workspaceRef && event.workspaceRef !== "default")
139
+ return event;
140
+ const appRef = event.data.appRef;
141
+ if (typeof appRef !== "string" || !appRef || !workspaceOfApp)
142
+ return undefined;
143
+ const workspaceRef = yield workspaceOfApp(appRef);
144
+ return workspaceRef ? Object.assign(Object.assign({}, event), { workspaceRef }) : undefined;
145
+ });
146
+ }
147
+ /** Subscribes to the bus and fans every event out; returns the unsubscribe function. */
148
+ function subscribeWebhookFanout(nc, options) {
149
+ var _a;
150
+ const sub = nc.subscribe(exports.WEBHOOK_EVENTS_SUBJECT, {
151
+ queue: (_a = options.queue) !== null && _a !== void 0 ? _a : "webhooks-fanout",
152
+ callback: (err, msg) => {
153
+ if (err)
154
+ return;
155
+ const event = parseBusEvent(msg.data);
156
+ if (!event)
157
+ return;
158
+ resolveWorkspace(event, options.workspaceOfApp)
159
+ .then((resolved) => {
160
+ var _a;
161
+ if (!resolved)
162
+ return;
163
+ const deliveries = options.publish(resolved);
164
+ (_a = options.onPublished) === null || _a === void 0 ? void 0 : _a.call(options, resolved, deliveries);
165
+ })
166
+ .catch((e) => { var _a; return (_a = options.onError) === null || _a === void 0 ? void 0 : _a.call(options, e); });
167
+ }
168
+ });
169
+ return () => sub.unsubscribe();
170
+ }
@@ -0,0 +1,66 @@
1
+ export declare const WEBHOOK_EVENT_TYPES: readonly ["call.started", "call.ended", "call.failed", "call_record.sealed", "provider.deprecated"];
2
+ export type WebhookEventType = (typeof WEBHOOK_EVENT_TYPES)[number];
3
+ /**
4
+ * Event names of the retired Autopilot assistant webhook (`eventsHook`) and what replaces them
5
+ * (add-call-record 7.3). The record of a conversation (transcript, turns, recording) is in the call
6
+ * record that `call_record.sealed` links to.
7
+ */
8
+ export declare const RETIRED_EVENT_TYPES: Record<string, WebhookEventType[]>;
9
+ export type WebhookEvent<D = Record<string, unknown>> = {
10
+ id: string;
11
+ type: WebhookEventType;
12
+ createdAt: string;
13
+ workspaceRef: string;
14
+ data: D;
15
+ };
16
+ type CallParties = {
17
+ callRef: string;
18
+ appRef: string;
19
+ from: string;
20
+ to: string;
21
+ direction: "inbound" | "outbound";
22
+ };
23
+ export declare const callStarted: (workspaceRef: string, call: CallParties) => WebhookEvent<CallParties>;
24
+ export declare const callEnded: (workspaceRef: string, call: CallParties & {
25
+ durationSeconds: number;
26
+ endReason: string;
27
+ }) => WebhookEvent<CallParties & {
28
+ durationSeconds: number;
29
+ endReason: string;
30
+ }>;
31
+ /** A call that never reached an app (busy, no answer, failed, app unreachable). */
32
+ export declare const callFailed: (workspaceRef: string, call: {
33
+ callRef: string;
34
+ appRef?: string;
35
+ from: string;
36
+ to: string;
37
+ direction: "inbound" | "outbound";
38
+ reason: string;
39
+ }) => WebhookEvent<{
40
+ callRef: string;
41
+ appRef?: string;
42
+ from: string;
43
+ to: string;
44
+ direction: "inbound" | "outbound";
45
+ reason: string;
46
+ }>;
47
+ /** Points to the record; never carries the transcript, recording or SIP trace. */
48
+ export declare const callRecordSealed: (workspaceRef: string, sealed: {
49
+ callRef: string;
50
+ version: number;
51
+ recordUrl: string;
52
+ }) => WebhookEvent<{
53
+ callRef: string;
54
+ version: number;
55
+ recordUrl: string;
56
+ }>;
57
+ export declare const providerDeprecated: (workspaceRef: string, d: {
58
+ id: string;
59
+ replacement: string;
60
+ retireOn: string;
61
+ }) => WebhookEvent<{
62
+ id: string;
63
+ replacement: string;
64
+ retireOn: string;
65
+ }>;
66
+ export {};
@@ -0,0 +1,62 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.providerDeprecated = exports.callRecordSealed = exports.callFailed = exports.callEnded = exports.callStarted = exports.RETIRED_EVENT_TYPES = exports.WEBHOOK_EVENT_TYPES = void 0;
4
+ /**
5
+ * Copyright (C) 2025 by Fonoster Inc (https://fonoster.com)
6
+ * http://github.com/fonoster/fonoster
7
+ *
8
+ * This file is part of Fonoster
9
+ *
10
+ * Licensed under the MIT License (the "License");
11
+ * you may not use this file except in compliance with
12
+ * the License. You may obtain a copy of the License at
13
+ *
14
+ * https://opensource.org/licenses/MIT
15
+ *
16
+ * Unless required by applicable law or agreed to in writing, software
17
+ * distributed under the License is distributed on an "AS IS" BASIS,
18
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
19
+ * See the License for the specific language governing permissions and
20
+ * limitations under the License.
21
+ */
22
+ const crypto_1 = require("crypto");
23
+ // Workspace webhook events, v1 (add-workspace-webhooks W2, W3). Every payload uses the envelope
24
+ // id, type, createdAt, workspaceRef, data; ids stay the same across retries.
25
+ exports.WEBHOOK_EVENT_TYPES = [
26
+ "call.started",
27
+ "call.ended",
28
+ "call.failed",
29
+ "call_record.sealed",
30
+ "provider.deprecated"
31
+ ];
32
+ /**
33
+ * Event names of the retired Autopilot assistant webhook (`eventsHook`) and what replaces them
34
+ * (add-call-record 7.3). The record of a conversation (transcript, turns, recording) is in the call
35
+ * record that `call_record.sealed` links to.
36
+ */
37
+ exports.RETIRED_EVENT_TYPES = {
38
+ "conversation.started": ["call.started"],
39
+ "conversation.ended": ["call.ended", "call_record.sealed"],
40
+ all: ["call.started", "call.ended", "call_record.sealed"]
41
+ };
42
+ function envelope(type, workspaceRef, data, now = new Date()) {
43
+ return {
44
+ id: `evt_${(0, crypto_1.randomUUID)()}`,
45
+ type,
46
+ createdAt: now.toISOString(),
47
+ workspaceRef,
48
+ data
49
+ };
50
+ }
51
+ const callStarted = (workspaceRef, call) => envelope("call.started", workspaceRef, call);
52
+ exports.callStarted = callStarted;
53
+ const callEnded = (workspaceRef, call) => envelope("call.ended", workspaceRef, call);
54
+ exports.callEnded = callEnded;
55
+ /** A call that never reached an app (busy, no answer, failed, app unreachable). */
56
+ const callFailed = (workspaceRef, call) => envelope("call.failed", workspaceRef, call);
57
+ exports.callFailed = callFailed;
58
+ /** Points to the record; never carries the transcript, recording or SIP trace. */
59
+ const callRecordSealed = (workspaceRef, sealed) => envelope("call_record.sealed", workspaceRef, sealed);
60
+ exports.callRecordSealed = callRecordSealed;
61
+ const providerDeprecated = (workspaceRef, d) => envelope("provider.deprecated", workspaceRef, d);
62
+ exports.providerDeprecated = providerDeprecated;
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Copyright (C) 2025 by Fonoster Inc (https://fonoster.com)
3
+ * http://github.com/fonoster/fonoster
4
+ *
5
+ * This file is part of Fonoster
6
+ *
7
+ * Licensed under the MIT License (the "License");
8
+ * you may not use this file except in compliance with
9
+ * the License. You may obtain a copy of the License at
10
+ *
11
+ * https://opensource.org/licenses/MIT
12
+ *
13
+ * Unless required by applicable law or agreed to in writing, software
14
+ * distributed under the License is distributed on an "AS IS" BASIS,
15
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ * See the License for the specific language governing permissions and
17
+ * limitations under the License.
18
+ */
19
+ export * from "./Dispatcher";
20
+ export * from "./events";
21
+ export * from "./signing";
22
+ export * from "./prisma";
23
+ export * from "./bus";
@@ -0,0 +1,39 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ /**
18
+ * Copyright (C) 2025 by Fonoster Inc (https://fonoster.com)
19
+ * http://github.com/fonoster/fonoster
20
+ *
21
+ * This file is part of Fonoster
22
+ *
23
+ * Licensed under the MIT License (the "License");
24
+ * you may not use this file except in compliance with
25
+ * the License. You may obtain a copy of the License at
26
+ *
27
+ * https://opensource.org/licenses/MIT
28
+ *
29
+ * Unless required by applicable law or agreed to in writing, software
30
+ * distributed under the License is distributed on an "AS IS" BASIS,
31
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
32
+ * See the License for the specific language governing permissions and
33
+ * limitations under the License.
34
+ */
35
+ __exportStar(require("./Dispatcher"), exports);
36
+ __exportStar(require("./events"), exports);
37
+ __exportStar(require("./signing"), exports);
38
+ __exportStar(require("./prisma"), exports);
39
+ __exportStar(require("./bus"), exports);