@fonoster/common 0.27.0 → 0.31.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 (209) hide show
  1. package/dist/{voice/PlayDtmf.d.ts → agents/index.d.ts} +3 -5
  2. package/dist/{assistants → agents}/index.js +2 -6
  3. package/dist/agents/sessionAgentSchema.d.ts +755 -0
  4. package/dist/agents/sessionAgentSchema.js +117 -0
  5. package/dist/{voice/Dial.d.ts → calls/index.d.ts} +16 -13
  6. package/dist/calls/index.js +44 -0
  7. package/dist/cellsecrets/index.d.ts +91 -0
  8. package/dist/cellsecrets/index.js +299 -0
  9. package/dist/config/cli/check.d.ts +6 -0
  10. package/dist/config/cli/check.js +202 -0
  11. package/dist/config/cli/envFile.d.ts +23 -0
  12. package/dist/config/cli/envFile.js +81 -0
  13. package/dist/config/cli/exposure.d.ts +10 -0
  14. package/dist/config/cli/exposure.js +93 -0
  15. package/dist/config/cli/index.d.ts +2 -0
  16. package/dist/{identity/tokenHasAccessKeyId.js → config/cli/index.js} +7 -8
  17. package/dist/config/cli/info.d.ts +6 -0
  18. package/dist/config/cli/info.js +62 -0
  19. package/dist/config/cli/init.d.ts +9 -0
  20. package/dist/config/cli/init.js +152 -0
  21. package/dist/config/cli/main.d.ts +11 -0
  22. package/dist/config/cli/main.js +155 -0
  23. package/dist/config/cli/render.d.ts +44 -0
  24. package/dist/config/cli/render.js +129 -0
  25. package/dist/config/cli/secrets.d.ts +21 -0
  26. package/dist/config/cli/secrets.js +117 -0
  27. package/dist/config/cli/types.d.ts +24 -0
  28. package/dist/config/cli/types.js +7 -0
  29. package/dist/{assistants/index.d.ts → config/configHints.d.ts} +8 -7
  30. package/dist/config/configHints.js +47 -0
  31. package/dist/{voice → config}/index.d.ts +11 -13
  32. package/dist/{voice → config}/index.js +11 -13
  33. package/dist/config/interpolateEnv.d.ts +34 -0
  34. package/dist/config/interpolateEnv.js +70 -0
  35. package/dist/config/loadServiceConfig.d.ts +28 -0
  36. package/dist/config/loadServiceConfig.js +112 -0
  37. package/dist/config/logsSchema.d.ts +76 -0
  38. package/dist/config/logsSchema.js +49 -0
  39. package/dist/config/natsConnection.d.ts +53 -0
  40. package/dist/config/natsConnection.js +58 -0
  41. package/dist/config/parseConfigFile.d.ts +7 -0
  42. package/dist/config/parseConfigFile.js +60 -0
  43. package/dist/config/resolveConfigFile.d.ts +17 -0
  44. package/dist/config/resolveConfigFile.js +54 -0
  45. package/dist/config/resolveSecretRefs.d.ts +35 -0
  46. package/dist/config/resolveSecretRefs.js +129 -0
  47. package/dist/{assistants/audioFiltersSchema.d.ts → config/strictDeep.d.ts} +9 -12
  48. package/dist/config/strictDeep.js +58 -0
  49. package/dist/config/telemetrySchema.d.ts +39 -0
  50. package/dist/config/telemetrySchema.js +44 -0
  51. package/dist/config/toJsonSchema.d.ts +30 -0
  52. package/dist/config/toJsonSchema.js +161 -0
  53. package/dist/constants.d.ts +1 -6
  54. package/dist/constants.js +4 -7
  55. package/dist/envs.d.ts +2 -1
  56. package/dist/envs.js +13 -11
  57. package/dist/errors/handleZodError.js +3 -1
  58. package/dist/filters/index.d.ts +70 -0
  59. package/dist/filters/index.js +101 -0
  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 +33 -7
  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 +15 -2
  74. package/dist/index.js +18 -3
  75. package/dist/net/assertPublicDestination.d.ts +52 -0
  76. package/dist/net/assertPublicDestination.js +342 -0
  77. package/dist/{voice/StopSay.d.ts → net/index.d.ts} +1 -6
  78. package/dist/net/index.js +35 -0
  79. package/dist/notifications/createSendEmail.js +3 -1
  80. package/dist/protos/applications.proto +138 -3
  81. package/dist/protos/authz.proto +26 -4
  82. package/dist/protos/billing.proto +92 -0
  83. package/dist/protos/calls.proto +4 -0
  84. package/dist/protos/capabilities.proto +102 -0
  85. package/dist/protos/identity.proto +26 -1
  86. package/dist/protos/providers.proto +139 -0
  87. package/dist/protos/roomr.proto +146 -0
  88. package/dist/protos/session.proto +194 -0
  89. package/dist/protos/usage.proto +59 -0
  90. package/dist/protos/webhooks.proto +140 -0
  91. package/dist/{utils/types.d.ts → regions/hub/index.d.ts} +1 -5
  92. package/dist/regions/hub/index.js +35 -0
  93. package/dist/regions/hub/natsJwt.d.ts +63 -0
  94. package/dist/regions/hub/natsJwt.js +195 -0
  95. package/dist/registry/Registry.d.ts +87 -0
  96. package/dist/registry/Registry.js +432 -0
  97. package/dist/registry/cli.d.ts +20 -0
  98. package/dist/registry/cli.js +436 -0
  99. package/dist/registry/features.d.ts +31 -0
  100. package/dist/registry/features.js +38 -0
  101. package/dist/registry/index.d.ts +24 -0
  102. package/dist/registry/index.js +40 -0
  103. package/dist/registry/prisma.d.ts +67 -0
  104. package/dist/registry/prisma.js +399 -0
  105. package/dist/registry/stores.d.ts +21 -0
  106. package/dist/{assistants/languageModelConfigSchema.js → registry/stores.js} +55 -38
  107. package/dist/registry/types.d.ts +110 -0
  108. package/dist/{assistants/LanguageModelProvider.d.ts → registry/types.js} +6 -8
  109. package/dist/registry/workspaceKeys.d.ts +41 -0
  110. package/dist/registry/workspaceKeys.js +64 -0
  111. package/dist/roomr/health.d.ts +9 -0
  112. package/dist/{assistants/eventsHookSchema.js → roomr/health.js} +22 -18
  113. package/dist/roomr/index.d.ts +75 -0
  114. package/dist/roomr/index.js +146 -0
  115. package/dist/roomr/session.d.ts +133 -0
  116. package/dist/roomr/session.js +132 -0
  117. package/dist/session/index.d.ts +19 -0
  118. package/dist/session/index.js +35 -0
  119. package/dist/session/schema.d.ts +2140 -0
  120. package/dist/session/schema.js +448 -0
  121. package/dist/sessions/index.d.ts +96 -0
  122. package/dist/sessions/index.js +197 -0
  123. package/dist/telemetry.d.ts +76 -0
  124. package/dist/telemetry.js +360 -0
  125. package/dist/types.d.ts +3 -5
  126. package/dist/{voice/Gather.d.ts → usage/CallUsage.d.ts} +19 -18
  127. package/dist/usage/CallUsage.js +76 -0
  128. package/dist/usage/index.d.ts +22 -0
  129. package/dist/usage/index.js +38 -0
  130. package/dist/usage/meters.d.ts +113 -0
  131. package/dist/usage/meters.js +90 -0
  132. package/dist/usage/types.d.ts +159 -0
  133. package/dist/usage/types.js +84 -0
  134. package/dist/usage/usageId.d.ts +6 -0
  135. package/dist/usage/usageId.js +45 -0
  136. package/dist/utils/{findIntegrationsCredentials.js → WriteBehind.d.ts} +13 -10
  137. package/dist/utils/WriteBehind.js +64 -0
  138. package/dist/utils/index.d.ts +1 -3
  139. package/dist/utils/index.js +1 -3
  140. package/dist/utils/{createFetchSingleCallByCallId.d.ts → logUnhandledRejections.d.ts} +8 -4
  141. package/dist/{assistants/audioFiltersSchema.js → utils/logUnhandledRejections.js} +17 -12
  142. package/dist/utils/withErrorHandling.js +1 -1
  143. package/dist/validators/calls.d.ts +3 -0
  144. package/dist/validators/calls.js +12 -2
  145. package/dist/validators/identity.d.ts +37 -8
  146. package/dist/validators/identity.js +22 -3
  147. package/dist/validators/index.d.ts +1 -0
  148. package/dist/validators/index.js +1 -0
  149. package/dist/validators/sip.d.ts +40 -0
  150. package/dist/validators/sip.js +99 -0
  151. package/dist/validators/sipnet/credentials.d.ts +2 -2
  152. package/dist/validators/sipnet/domains.js +7 -7
  153. package/dist/validators/sipnet/trunks.d.ts +14 -14
  154. package/dist/validators/sipnet/trunks.js +4 -2
  155. package/dist/webhooks/Dispatcher.d.ts +87 -0
  156. package/dist/webhooks/Dispatcher.js +225 -0
  157. package/dist/webhooks/bus.d.ts +68 -0
  158. package/dist/webhooks/bus.js +178 -0
  159. package/dist/webhooks/events.d.ts +66 -0
  160. package/dist/webhooks/events.js +62 -0
  161. package/dist/webhooks/index.d.ts +23 -0
  162. package/dist/webhooks/index.js +39 -0
  163. package/dist/webhooks/prisma.d.ts +55 -0
  164. package/dist/webhooks/prisma.js +242 -0
  165. package/dist/webhooks/signing.d.ts +8 -0
  166. package/dist/webhooks/signing.js +53 -0
  167. package/package.json +25 -11
  168. package/schema/session.v1.json +871 -0
  169. package/dist/assistants/assistantSchema.d.ts +0 -739
  170. package/dist/assistants/assistantSchema.js +0 -37
  171. package/dist/assistants/conversationSettingsSchema.d.ts +0 -122
  172. package/dist/assistants/conversationSettingsSchema.js +0 -133
  173. package/dist/assistants/eventsHookSchema.d.ts +0 -38
  174. package/dist/assistants/languageModelConfigSchema.d.ts +0 -206
  175. package/dist/identity/tokenHasAccessKeyId.d.ts +0 -2
  176. package/dist/protos/voice.proto +0 -557
  177. package/dist/utils/createFetchSingleCallByCallId.js +0 -33
  178. package/dist/utils/findIntegrationsCredentials.d.ts +0 -23
  179. package/dist/utils/types.js +0 -2
  180. package/dist/voice/Dial.js +0 -20
  181. package/dist/voice/Gather.js +0 -9
  182. package/dist/voice/Mute.d.ts +0 -31
  183. package/dist/voice/Mute.js +0 -9
  184. package/dist/voice/Play.d.ts +0 -30
  185. package/dist/voice/Play.js +0 -2
  186. package/dist/voice/PlayDtmf.js +0 -2
  187. package/dist/voice/PlaybackControl.d.ts +0 -31
  188. package/dist/voice/PlaybackControl.js +0 -11
  189. package/dist/voice/Record.d.ts +0 -36
  190. package/dist/voice/Record.js +0 -7
  191. package/dist/voice/Say.d.ts +0 -28
  192. package/dist/voice/Say.js +0 -2
  193. package/dist/voice/StopSay.js +0 -2
  194. package/dist/voice/Stream.d.ts +0 -53
  195. package/dist/voice/Stream.js +0 -19
  196. package/dist/voice/StreamGather.d.ts +0 -34
  197. package/dist/voice/StreamGather.js +0 -9
  198. package/dist/voice/Verb.d.ts +0 -27
  199. package/dist/voice/Verb.js +0 -2
  200. package/dist/voice/voice.d.ts +0 -171
  201. package/dist/voice/voice.js +0 -65
  202. package/dist/{assistants → agents}/testCasesSchema.d.ts +4 -4
  203. /package/dist/{assistants → agents}/testCasesSchema.js +0 -0
  204. /package/dist/{assistants → agents}/tools/index.d.ts +0 -0
  205. /package/dist/{assistants → agents}/tools/index.js +0 -0
  206. /package/dist/{assistants → agents}/tools/propertySchema.d.ts +0 -0
  207. /package/dist/{assistants → agents}/tools/propertySchema.js +0 -0
  208. /package/dist/{assistants → agents}/tools/toolSchema.d.ts +0 -0
  209. /package/dist/{assistants → agents}/tools/toolSchema.js +0 -0
@@ -20,7 +20,7 @@ import { Transport } from "@fonoster/types";
20
20
  import { z } from "zod";
21
21
  declare const createTrunkRequestSchema: z.ZodObject<{
22
22
  name: z.ZodString;
23
- sendRegister: z.ZodBoolean;
23
+ sendRegister: z.ZodOptional<z.ZodBoolean>;
24
24
  inboundUri: z.ZodEffects<z.ZodString, string, string>;
25
25
  uris: z.ZodArray<z.ZodObject<{
26
26
  host: z.ZodEffects<z.ZodString, string, string>;
@@ -29,49 +29,49 @@ declare const createTrunkRequestSchema: z.ZodObject<{
29
29
  user: z.ZodOptional<z.ZodString>;
30
30
  weight: z.ZodNumber;
31
31
  priority: z.ZodNumber;
32
- enabled: z.ZodBoolean;
32
+ enabled: z.ZodOptional<z.ZodBoolean>;
33
33
  }, "strip", z.ZodTypeAny, {
34
+ transport?: Transport;
34
35
  host?: string;
35
36
  port?: number;
36
- priority?: number;
37
- enabled?: boolean;
38
- transport?: Transport;
39
37
  user?: string;
38
+ enabled?: boolean;
40
39
  weight?: number;
40
+ priority?: number;
41
41
  }, {
42
+ transport?: Transport;
42
43
  host?: string;
43
44
  port?: number;
44
- priority?: number;
45
- enabled?: boolean;
46
- transport?: Transport;
47
45
  user?: string;
46
+ enabled?: boolean;
48
47
  weight?: number;
48
+ priority?: number;
49
49
  }>, "many">;
50
50
  }, "strip", z.ZodTypeAny, {
51
51
  name?: string;
52
52
  sendRegister?: boolean;
53
53
  inboundUri?: string;
54
54
  uris?: {
55
+ transport?: Transport;
55
56
  host?: string;
56
57
  port?: number;
57
- priority?: number;
58
- enabled?: boolean;
59
- transport?: Transport;
60
58
  user?: string;
59
+ enabled?: boolean;
61
60
  weight?: number;
61
+ priority?: number;
62
62
  }[];
63
63
  }, {
64
64
  name?: string;
65
65
  sendRegister?: boolean;
66
66
  inboundUri?: string;
67
67
  uris?: {
68
+ transport?: Transport;
68
69
  host?: string;
69
70
  port?: number;
70
- priority?: number;
71
- enabled?: boolean;
72
- transport?: Transport;
73
71
  user?: string;
72
+ enabled?: boolean;
74
73
  weight?: number;
74
+ priority?: number;
75
75
  }[];
76
76
  }>;
77
77
  declare const updateTrunkRequestSchema: z.ZodObject<{}, "strip", z.ZodTypeAny, {}, {}>;
@@ -61,9 +61,11 @@ const hostOrIPSchema = zod_1.z
61
61
  .refine((host) => Validator.isIP(host, 4) || Validator.isFQDN(host), {
62
62
  message: "Must be a valid IP or FQDN"
63
63
  });
64
+ // Booleans are optional: the gRPC loader drops proto3 defaults, so `false`
65
+ // arrives as a missing field.
64
66
  const createTrunkRequestSchema = zod_1.z.object({
65
67
  name: common_1.nameSchema,
66
- sendRegister: zod_1.z.boolean(),
68
+ sendRegister: zod_1.z.boolean().optional(),
67
69
  inboundUri: hostOrIPSchema,
68
70
  uris: zod_1.z.array(zod_1.z.object({
69
71
  host: hostOrIPSchema,
@@ -72,7 +74,7 @@ const createTrunkRequestSchema = zod_1.z.object({
72
74
  user: zod_1.z.string().optional(),
73
75
  weight: zod_1.z.number(),
74
76
  priority: zod_1.z.number(),
75
- enabled: zod_1.z.boolean()
77
+ enabled: zod_1.z.boolean().optional()
76
78
  }))
77
79
  });
78
80
  exports.createTrunkRequestSchema = createTrunkRequestSchema;
@@ -0,0 +1,87 @@
1
+ import { WebhookEvent, WebhookEventType } from "./events";
2
+ export type WebhookEndpoint = {
3
+ id: string;
4
+ workspaceRef: string;
5
+ url: string;
6
+ events: WebhookEventType[];
7
+ secret: string;
8
+ /** The previous secret stays valid until `previousUntil` (rotation window). */
9
+ previousSecret?: string;
10
+ previousUntil?: number;
11
+ };
12
+ export type Delivery = {
13
+ id: string;
14
+ endpointId: string;
15
+ event: WebhookEvent;
16
+ status: "pending" | "succeeded" | "failed";
17
+ attempts: number;
18
+ firstAttemptAt?: number;
19
+ lastAttemptAt?: number;
20
+ lastStatus?: number;
21
+ nextAttemptAt: number;
22
+ };
23
+ export interface WebhookStore {
24
+ endpoints(workspaceRef?: string): WebhookEndpoint[];
25
+ putEndpoint(e: WebhookEndpoint): void;
26
+ deleteEndpoint(id: string): void;
27
+ deliveries(endpointId?: string): Delivery[];
28
+ putDelivery(d: Delivery): void;
29
+ }
30
+ export declare class MemoryWebhookStore implements WebhookStore {
31
+ private e;
32
+ private d;
33
+ endpoints(workspaceRef?: string): WebhookEndpoint[];
34
+ putEndpoint(x: WebhookEndpoint): void;
35
+ deleteEndpoint(id: string): void;
36
+ deliveries(endpointId?: string): Delivery[];
37
+ putDelivery(x: Delivery): void;
38
+ }
39
+ export declare class WebhookError extends Error {
40
+ }
41
+ /**
42
+ * The id of an event's delivery to one endpoint. It depends only on the two, so the same event
43
+ * fanned out again (a bus message redelivered after a replica died, or published twice) finds
44
+ * the delivery it already made instead of making a second one (ST-2).
45
+ */
46
+ export declare function deliveryId(eventId: string, endpointId: string): string;
47
+ export type DispatcherOptions = {
48
+ store: WebhookStore;
49
+ /** Per-attempt timeout (design OQ3 proposal: 10 s). */
50
+ attemptTimeoutMs?: number;
51
+ /** First retry delay; each next one doubles, capped at maxDelayMs (OQ2: from 10 s). */
52
+ firstDelayMs?: number;
53
+ maxDelayMs?: number;
54
+ /** Retries stop this long after the first attempt (OQ2: 24 h). */
55
+ retryPeriodMs?: number;
56
+ /** Lets tests use plain http:// receivers. Never in production. */
57
+ allowInsecureUrls?: boolean;
58
+ /**
59
+ * Private destinations (host names, IPs, CIDRs) endpoints may use; defaults to
60
+ * FONOSTER_ALLOWED_DESTINATIONS. Everything else must be public (RV-B2).
61
+ */
62
+ allowedDestinations?: string[];
63
+ now?: () => number;
64
+ };
65
+ export declare class WebhookDispatcher {
66
+ private readonly options;
67
+ constructor(options: DispatcherOptions);
68
+ private policy;
69
+ private now;
70
+ /** Registers an endpoint; the returned secret is the only time it is shown. */
71
+ createEndpoint(input: {
72
+ workspaceRef: string;
73
+ url: string;
74
+ events: WebhookEventType[];
75
+ }): WebhookEndpoint;
76
+ /** New secret; the old one keeps verifying for `windowMs`. Returns the new secret. */
77
+ rotateSecret(endpointId: string, windowMs?: number): string;
78
+ /** The deliveries an event makes, one per subscribed endpoint of its workspace (not stored). */
79
+ plan(event: WebhookEvent): Delivery[];
80
+ /** Fans an event out to every endpoint of its workspace subscribed to its type. */
81
+ publish(event: WebhookEvent): Delivery[];
82
+ /** Attempts every pending delivery that is due; returns how many were attempted. */
83
+ runDue(): Promise<number>;
84
+ /** Sends a delivery again now, with the original event id and a fresh signature. */
85
+ resend(deliveryId: string): Promise<Delivery>;
86
+ private attempt;
87
+ }
@@ -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,68 @@
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 Roomr nodes publish a call's events (`roomr.events.<call_ref>`; room events have one more token). */
21
+ export declare const ROOMR_CALL_EVENTS_SUBJECT = "roomr.events.*";
22
+ /**
23
+ * The `call.failed` reasons an unanswered outbound call can end with (the node's `cause`); any
24
+ * other cause is reported as `node_error`.
25
+ */
26
+ export declare const UNANSWERED_CALL_REASONS: readonly ["busy", "no_answer", "rejected", "removed", "node_error"];
27
+ /** An outbound call the callee never answered, as its node's `call.ended` tells it. */
28
+ export type UnansweredCall = {
29
+ callRef: string;
30
+ appRef: string;
31
+ from: string;
32
+ to: string;
33
+ reason: (typeof UNANSWERED_CALL_REASONS)[number];
34
+ sipStatus?: number;
35
+ nodeRef: string;
36
+ };
37
+ /**
38
+ * The unanswered outbound call a Roomr node's event on `subject` (`roomr.events.<call_ref>`)
39
+ * reports, or undefined: only a `call.ended` with `answered: false` of a call the control plane
40
+ * placed (its `metadata.app_ref`, passed through by the node) for the subject's own call. The
41
+ * parties come from the metadata the control plane set; the node never chooses the workspace
42
+ * (left for the fan-out to resolve from the app), the event type or extra data (M6).
43
+ */
44
+ export declare function unansweredOutboundCall(subject: string, event: Record<string, unknown> | undefined): UnansweredCall | undefined;
45
+ /**
46
+ * The `call.failed` event for an unanswered outbound call. Its id depends on the call only, so
47
+ * every replica that sees the node's `call.ended` builds the same event and each endpoint gets one
48
+ * delivery.
49
+ */
50
+ export declare function unansweredCallFailed(call: UnansweredCall, now?: Date): WebhookEvent;
51
+ export type FanoutOptions = {
52
+ /** Usually `WebhookDispatcher.publish`: stores one pending delivery per subscribed endpoint. */
53
+ publish(event: WebhookEvent): Delivery[];
54
+ /**
55
+ * The workspace of an app, for events whose producer did not know it (empty or `default`
56
+ * workspaceRef with a `data.appRef`).
57
+ */
58
+ workspaceOfApp?(appRef: string): Promise<string | undefined>;
59
+ /** Called after an event was fanned out (e.g. to attempt the new deliveries at once). */
60
+ onPublished?(event: WebhookEvent, deliveries: Delivery[]): void;
61
+ onError?(e: Error): void;
62
+ /** Queue group: one replica handles each event. */
63
+ queue?: string;
64
+ };
65
+ /** Fills in a missing workspace from the app; undefined when it cannot be known. */
66
+ export declare function resolveWorkspace(event: WebhookEvent, workspaceOfApp?: FanoutOptions["workspaceOfApp"]): Promise<WebhookEvent | undefined>;
67
+ /** Subscribes to the bus and fans every event out; returns the unsubscribe function. */
68
+ export declare function subscribeWebhookFanout(nc: Pick<BusConnection, "subscribe">, options: FanoutOptions): () => void;
@@ -0,0 +1,178 @@
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.UNANSWERED_CALL_REASONS = exports.ROOMR_CALL_EVENTS_SUBJECT = exports.WEBHOOK_EVENTS_SUBJECT = void 0;
13
+ exports.busWebhookPublisher = busWebhookPublisher;
14
+ exports.parseBusEvent = parseBusEvent;
15
+ exports.unansweredOutboundCall = unansweredOutboundCall;
16
+ exports.unansweredCallFailed = unansweredCallFailed;
17
+ exports.resolveWorkspace = resolveWorkspace;
18
+ exports.subscribeWebhookFanout = subscribeWebhookFanout;
19
+ /**
20
+ * Copyright (C) 2025 by Fonoster Inc (https://fonoster.com)
21
+ * http://github.com/fonoster/fonoster
22
+ *
23
+ * This file is part of Fonoster
24
+ *
25
+ * Licensed under the MIT License (the "License");
26
+ * you may not use this file except in compliance with
27
+ * the License. You may obtain a copy of the License at
28
+ *
29
+ * https://opensource.org/licenses/MIT
30
+ *
31
+ * Unless required by applicable law or agreed to in writing, software
32
+ * distributed under the License is distributed on an "AS IS" BASIS,
33
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
34
+ * See the License for the specific language governing permissions and
35
+ * limitations under the License.
36
+ */
37
+ const crypto_1 = require("crypto");
38
+ const util_1 = require("util");
39
+ const events_1 = require("./events");
40
+ // Event fan-out from the event bus (add-workspace-webhooks task 2.2, build log LO-B1). Producers
41
+ // that have no database (Roomr nodes, appbridges in bus mode, other regions) publish events on
42
+ // `webhooks.events`; one control-plane replica per event (queue group) fans each event out to the
43
+ // workspace's subscribed endpoints. A producer that does not know the workspace leaves
44
+ // `workspaceRef` empty and the fan-out resolves it from `data.appRef`.
45
+ //
46
+ // Roomr nodes may not publish `webhooks.events` (M6: a node could otherwise forge any event for
47
+ // any workspace). An outbound call that is never answered reaches no AppBridge; the control plane
48
+ // builds its call.failed from the node's own `call.ended` on `roomr.events.<call_ref>`
49
+ // (unansweredOutboundCall), with the workspace taken from the app and nothing else trusted.
50
+ exports.WEBHOOK_EVENTS_SUBJECT = "webhooks.events";
51
+ const encoder = new util_1.TextEncoder();
52
+ const decoder = new util_1.TextDecoder();
53
+ /** A producer that publishes events on the bus instead of delivering them itself. */
54
+ function busWebhookPublisher(nc) {
55
+ return (event) => nc.publish(exports.WEBHOOK_EVENTS_SUBJECT, encoder.encode(JSON.stringify(event)));
56
+ }
57
+ /** Parses a bus message; undefined for anything that is not a v1 webhook event. */
58
+ function parseBusEvent(data) {
59
+ let e;
60
+ try {
61
+ e = JSON.parse(decoder.decode(data));
62
+ }
63
+ catch (_a) {
64
+ return undefined;
65
+ }
66
+ if (!e ||
67
+ typeof e.id !== "string" ||
68
+ !/^evt_[\w-]+$/.test(e.id) ||
69
+ !events_1.WEBHOOK_EVENT_TYPES.includes(e.type) ||
70
+ typeof e.data !== "object" ||
71
+ e.data === null)
72
+ return undefined;
73
+ return e;
74
+ }
75
+ /** Where Roomr nodes publish a call's events (`roomr.events.<call_ref>`; room events have one more token). */
76
+ exports.ROOMR_CALL_EVENTS_SUBJECT = "roomr.events.*";
77
+ /**
78
+ * The `call.failed` reasons an unanswered outbound call can end with (the node's `cause`); any
79
+ * other cause is reported as `node_error`.
80
+ */
81
+ exports.UNANSWERED_CALL_REASONS = [
82
+ "busy",
83
+ "no_answer",
84
+ "rejected",
85
+ "removed",
86
+ "node_error"
87
+ ];
88
+ const MAX_FIELD = 256;
89
+ const field = (v, required = false) => typeof v === "string" && v.length <= MAX_FIELD && (!required || v.length > 0)
90
+ ? v
91
+ : undefined;
92
+ /**
93
+ * The unanswered outbound call a Roomr node's event on `subject` (`roomr.events.<call_ref>`)
94
+ * reports, or undefined: only a `call.ended` with `answered: false` of a call the control plane
95
+ * placed (its `metadata.app_ref`, passed through by the node) for the subject's own call. The
96
+ * parties come from the metadata the control plane set; the node never chooses the workspace
97
+ * (left for the fan-out to resolve from the app), the event type or extra data (M6).
98
+ */
99
+ function unansweredOutboundCall(subject, event) {
100
+ var _a, _b, _c, _d;
101
+ const tokens = subject.split(".");
102
+ if (tokens.length !== 3 || tokens[0] !== "roomr" || tokens[1] !== "events")
103
+ return undefined;
104
+ if (!event || typeof event !== "object" || event.type !== "call.ended")
105
+ return undefined;
106
+ if (event.answered !== false)
107
+ return undefined;
108
+ const callRef = field(event.call_ref, true);
109
+ if (!callRef || callRef !== tokens[2])
110
+ return undefined;
111
+ const metadata = (event.metadata && typeof event.metadata === "object" ? event.metadata : {});
112
+ const appRef = field(metadata.app_ref, true);
113
+ if (!appRef)
114
+ return undefined;
115
+ const cause = String((_a = event.cause) !== null && _a !== void 0 ? _a : "");
116
+ const reason = exports.UNANSWERED_CALL_REASONS.includes(cause)
117
+ ? cause
118
+ : "node_error";
119
+ const sipStatus = Number(event.sip_status);
120
+ return Object.assign(Object.assign({ callRef,
121
+ appRef, from: (_b = field(metadata.from)) !== null && _b !== void 0 ? _b : "", to: (_c = field(metadata.to)) !== null && _c !== void 0 ? _c : "", reason }, (Number.isInteger(sipStatus) && sipStatus > 0 ? { sipStatus } : {})), { nodeRef: (_d = field(event.node_ref)) !== null && _d !== void 0 ? _d : "" });
122
+ }
123
+ /**
124
+ * The `call.failed` event for an unanswered outbound call. Its id depends on the call only, so
125
+ * every replica that sees the node's `call.ended` builds the same event and each endpoint gets one
126
+ * delivery.
127
+ */
128
+ function unansweredCallFailed(call, now = new Date()) {
129
+ const event = (0, events_1.callFailed)("", {
130
+ callRef: call.callRef,
131
+ appRef: call.appRef,
132
+ from: call.from,
133
+ to: call.to,
134
+ direction: "outbound",
135
+ reason: call.reason
136
+ });
137
+ const digest = (0, crypto_1.createHash)("sha256")
138
+ .update(`call.failed\n${call.callRef}`)
139
+ .digest("hex")
140
+ .slice(0, 32);
141
+ return Object.assign(Object.assign({}, event), { id: `evt_node_${digest}`, createdAt: now.toISOString() });
142
+ }
143
+ /** Fills in a missing workspace from the app; undefined when it cannot be known. */
144
+ function resolveWorkspace(event, workspaceOfApp) {
145
+ return __awaiter(this, void 0, void 0, function* () {
146
+ if (event.workspaceRef && event.workspaceRef !== "default")
147
+ return event;
148
+ const appRef = event.data.appRef;
149
+ if (typeof appRef !== "string" || !appRef || !workspaceOfApp)
150
+ return undefined;
151
+ const workspaceRef = yield workspaceOfApp(appRef);
152
+ return workspaceRef ? Object.assign(Object.assign({}, event), { workspaceRef }) : undefined;
153
+ });
154
+ }
155
+ /** Subscribes to the bus and fans every event out; returns the unsubscribe function. */
156
+ function subscribeWebhookFanout(nc, options) {
157
+ var _a;
158
+ const sub = nc.subscribe(exports.WEBHOOK_EVENTS_SUBJECT, {
159
+ queue: (_a = options.queue) !== null && _a !== void 0 ? _a : "webhooks-fanout",
160
+ callback: (err, msg) => {
161
+ if (err)
162
+ return;
163
+ const event = parseBusEvent(msg.data);
164
+ if (!event)
165
+ return;
166
+ resolveWorkspace(event, options.workspaceOfApp)
167
+ .then((resolved) => {
168
+ var _a;
169
+ if (!resolved)
170
+ return;
171
+ const deliveries = options.publish(resolved);
172
+ (_a = options.onPublished) === null || _a === void 0 ? void 0 : _a.call(options, resolved, deliveries);
173
+ })
174
+ .catch((e) => { var _a; return (_a = options.onError) === null || _a === void 0 ? void 0 : _a.call(options, e); });
175
+ }
176
+ });
177
+ return () => sub.unsubscribe();
178
+ }