@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,202 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.runCheck = runCheck;
7
+ /**
8
+ * Copyright (C) 2025 by Fonoster Inc (https://fonoster.com)
9
+ * http://github.com/fonoster/fonoster
10
+ *
11
+ * This file is part of Fonoster
12
+ *
13
+ * Licensed under the MIT License (the "License");
14
+ * you may not use this file except in compliance with
15
+ * the License. You may obtain a copy of the License at
16
+ *
17
+ * https://opensource.org/licenses/MIT
18
+ *
19
+ * Unless required by applicable law or agreed to in writing, software
20
+ * distributed under the License is distributed on an "AS IS" BASIS,
21
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
22
+ * See the License for the specific language governing permissions and
23
+ * limitations under the License.
24
+ */
25
+ const fs_1 = require("fs");
26
+ const path_1 = require("path");
27
+ const _2020_1 = __importDefault(require("ajv/dist/2020"));
28
+ const yaml_1 = require("yaml");
29
+ const configHints_1 = require("../configHints");
30
+ const interpolateEnv_1 = require("../interpolateEnv");
31
+ const resolveSecretRefs_1 = require("../resolveSecretRefs");
32
+ const envFile_1 = require("./envFile");
33
+ const exposure_1 = require("./exposure");
34
+ const render_1 = require("./render");
35
+ /**
36
+ * Container paths under which the services mount config/ (compose.yaml): a `secret://file/...`
37
+ * reference to one of them is read from config/ on this machine.
38
+ */
39
+ const MOUNTS = ["/config/", "/opt/fonoster/", "/etc/fonoster/"];
40
+ /** Schema names that validate a config file of another name. */
41
+ const ALIASES = {
42
+ apiserver: "fonoster",
43
+ "autopilot-session": "autopilot"
44
+ };
45
+ const PLACEHOLDER = /\$\{([A-Za-z_][A-Za-z0-9_]*)(:-[^}]*)?\}/g;
46
+ function keyPath(segments) {
47
+ return segments
48
+ .map((s, i) => typeof s === "number" ? `[${s}]` : i === 0 ? String(s) : `.${s}`)
49
+ .join("");
50
+ }
51
+ function pointerSegments(pointer) {
52
+ if (!pointer)
53
+ return [];
54
+ return pointer
55
+ .slice(1)
56
+ .split("/")
57
+ .map((s) => s.replace(/~1/g, "/").replace(/~0/g, "~"))
58
+ .map((s) => (/^\d+$/.test(s) ? Number(s) : s));
59
+ }
60
+ /** Resolves every secret:// reference in place; problems name the key path, never a value. */
61
+ function resolveRefs(input, env, configDir, path, problems) {
62
+ if (typeof input === "string") {
63
+ const raw = input;
64
+ // ${NAME} placeholders resolve from .env, as Compose passes it to the containers.
65
+ if (/\$\{[^}]*\}/.test(raw)) {
66
+ try {
67
+ input = (0, interpolateEnv_1.interpolateEnv)(raw, env);
68
+ }
69
+ catch (_a) {
70
+ const unset = [...raw.matchAll(PLACEHOLDER)]
71
+ .filter((m) => (env[m[1]] === undefined || env[m[1]] === "") && !m[2])
72
+ .map((m) => m[1]);
73
+ problems.push(`${keyPath(path)}: ${unset.map((n) => `\${${n}}`).join(", ")} not set in .env`);
74
+ return input;
75
+ }
76
+ }
77
+ if (typeof input !== "string" || !(0, resolveSecretRefs_1.isSecretRef)(input))
78
+ return input;
79
+ try {
80
+ return (0, resolveSecretRefs_1.resolveSecretRef)(input, {
81
+ env,
82
+ readFile: (p) => {
83
+ const mount = MOUNTS.find((m) => p.startsWith(m));
84
+ const local = mount ? (0, path_1.join)(configDir, p.slice(mount.length)) : p;
85
+ return (0, fs_1.readFileSync)((0, fs_1.existsSync)(local) ? local : p, "utf8");
86
+ }
87
+ });
88
+ }
89
+ catch (e) {
90
+ problems.push(`${keyPath(path)}: ${e.message}`);
91
+ return input;
92
+ }
93
+ }
94
+ if (Array.isArray(input))
95
+ return input.map((v, i) => resolveRefs(v, env, configDir, [...path, i], problems));
96
+ if (input !== null && typeof input === "object")
97
+ return Object.fromEntries(Object.entries(input).map(([k, v]) => [
98
+ k,
99
+ resolveRefs(v, env, configDir, [...path, k], problems)
100
+ ]));
101
+ return input;
102
+ }
103
+ function describe(error) {
104
+ var _a, _b;
105
+ const at = pointerSegments(error.instancePath);
106
+ if (error.keyword === "additionalProperties") {
107
+ const key = error.params
108
+ .additionalProperty;
109
+ const known = Object.keys((_b = (_a = error.parentSchema) === null || _a === void 0 ? void 0 : _a.properties) !== null && _b !== void 0 ? _b : {});
110
+ const hint = (0, configHints_1.suggestKey)(key, known);
111
+ return `${keyPath([...at, key])}: unknown key${hint ? ` (did you mean ${hint}?)` : ""}`;
112
+ }
113
+ if (error.keyword === "required") {
114
+ const missing = error.params
115
+ .missingProperty;
116
+ return `${keyPath([...at, missing])}: missing required value`;
117
+ }
118
+ return `${keyPath(at) || "(root)"}: ${error.message}`;
119
+ }
120
+ function loadSchemas(dir) {
121
+ var _a;
122
+ const schemas = new Map();
123
+ if (!(0, fs_1.existsSync)(dir))
124
+ return schemas;
125
+ for (const file of (0, fs_1.readdirSync)(dir).filter((f) => f.endsWith(".json"))) {
126
+ const name = file.replace(/\.json$/, "");
127
+ schemas.set((_a = ALIASES[name]) !== null && _a !== void 0 ? _a : name, JSON.parse((0, fs_1.readFileSync)((0, path_1.join)(dir, file), "utf8")));
128
+ }
129
+ return schemas;
130
+ }
131
+ /** Validates config/*.yaml, placeholders resolved from .env, against config/schemas/*.json (D5). */
132
+ function runCheck(options, io) {
133
+ var _a, _b;
134
+ const dir = (0, path_1.resolve)(options.dir);
135
+ const envFile = (0, path_1.join)(dir, ".env");
136
+ const configDir = (0, path_1.join)(dir, "config");
137
+ if (!(0, fs_1.existsSync)(envFile) || !(0, fs_1.existsSync)(configDir)) {
138
+ io.err("no .env here: run `npx -p @fonoster/common fonoster-config init` first");
139
+ return 1;
140
+ }
141
+ const env = (0, envFile_1.expandEnv)((0, envFile_1.readEnvFile)(envFile));
142
+ const schemas = loadSchemas((0, path_1.join)(configDir, "schemas"));
143
+ if (!schemas.size)
144
+ io.out("config/schemas/ has no schemas: checking values and secret:// references only");
145
+ let failed = 0;
146
+ const files = (0, render_1.listExamples)(configDir)
147
+ .map((e) => (0, render_1.renderedName)(e))
148
+ .filter((f) => /\.ya?ml$/.test(f));
149
+ for (const file of files) {
150
+ const label = `config/${file}`;
151
+ const path = (0, path_1.join)(configDir, file);
152
+ const problems = [];
153
+ if (!(0, fs_1.existsSync)(path)) {
154
+ io.err(`${label}: missing (run init)`);
155
+ failed++;
156
+ continue;
157
+ }
158
+ let data;
159
+ try {
160
+ data = (_a = (0, yaml_1.parse)((0, fs_1.readFileSync)(path, "utf8"))) !== null && _a !== void 0 ? _a : {};
161
+ }
162
+ catch (e) {
163
+ io.err(`${label}: not valid YAML: ${e.message}`);
164
+ failed++;
165
+ continue;
166
+ }
167
+ const resolved = resolveRefs(data, env, configDir, [], problems);
168
+ const schema = schemas.get(file.replace(/\.ya?ml$/, ""));
169
+ if (schema) {
170
+ // The exported schemas are draft 2020-12 (toJsonSchema).
171
+ // coerceTypes: a placeholder is a string after YAML parsing; services accept "5060" for a
172
+ // port the same way. validateFormats: false: formats (uri, email) only document a value
173
+ // here (the services check them), and Ajv without ajv-formats warned for each one.
174
+ const ajv = new _2020_1.default({
175
+ allErrors: true,
176
+ coerceTypes: true,
177
+ strict: false,
178
+ validateFormats: false,
179
+ verbose: true
180
+ });
181
+ const validate = ajv.compile(schema);
182
+ if (!validate(resolved))
183
+ for (const error of (_b = validate.errors) !== null && _b !== void 0 ? _b : []) {
184
+ const p = describe(error);
185
+ if (!problems.includes(p))
186
+ problems.push(p);
187
+ }
188
+ }
189
+ if (problems.length) {
190
+ failed++;
191
+ problems.forEach((p) => io.err(`${label}: ${p}`));
192
+ }
193
+ else
194
+ io.out(`${label}: valid${schema ? "" : " (no schema; values only)"}`);
195
+ }
196
+ (0, exposure_1.exposureWarnings)(env, dir).forEach((w) => io.err(`warning: ${w}`));
197
+ if (failed) {
198
+ io.err(`${failed} file${failed > 1 ? "s" : ""} with problems`);
199
+ return 1;
200
+ }
201
+ return 0;
202
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Compose-style `.env` files (KEY=VALUE lines, `#` comments). Values may reference earlier names
3
+ * as `${NAME}` or `${NAME:-default}`, which Docker Compose expands too; `expandEnv` does the same
4
+ * for rendering. Quotes around a whole value are removed.
5
+ */
6
+ export type EnvLine = {
7
+ kind: "entry";
8
+ name: string;
9
+ value: string;
10
+ raw: string;
11
+ } | {
12
+ kind: "other";
13
+ raw: string;
14
+ };
15
+ export declare function parseEnvLines(text: string): EnvLine[];
16
+ /** The entries of an env file, by name (the last one wins, as in Compose). */
17
+ export declare function envEntries(lines: EnvLine[]): Map<string, string>;
18
+ export declare function readEnvFile(file: string): Map<string, string>;
19
+ /** Expands `${NAME}` references between values, in order (later values see earlier ones). */
20
+ export declare function expandEnv(entries: Map<string, string>, base?: Record<string, string | undefined>): Record<string, string>;
21
+ /** Writes a file and sets its mode (also when it already existed with another one). */
22
+ export declare function writeWithMode(file: string, content: string, mode: number): void;
23
+ export declare function serializeEnvLines(lines: EnvLine[]): string;
@@ -0,0 +1,81 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseEnvLines = parseEnvLines;
4
+ exports.envEntries = envEntries;
5
+ exports.readEnvFile = readEnvFile;
6
+ exports.expandEnv = expandEnv;
7
+ exports.writeWithMode = writeWithMode;
8
+ exports.serializeEnvLines = serializeEnvLines;
9
+ /**
10
+ * Copyright (C) 2025 by Fonoster Inc (https://fonoster.com)
11
+ * http://github.com/fonoster/fonoster
12
+ *
13
+ * This file is part of Fonoster
14
+ *
15
+ * Licensed under the MIT License (the "License");
16
+ * you may not use this file except in compliance with
17
+ * the License. You may obtain a copy of the License at
18
+ *
19
+ * https://opensource.org/licenses/MIT
20
+ *
21
+ * Unless required by applicable law or agreed to in writing, software
22
+ * distributed under the License is distributed on an "AS IS" BASIS,
23
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
24
+ * See the License for the specific language governing permissions and
25
+ * limitations under the License.
26
+ */
27
+ const fs_1 = require("fs");
28
+ const ENTRY = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=(.*)$/;
29
+ function unquote(value) {
30
+ const v = value.trim();
31
+ if (v.length >= 2 &&
32
+ ((v.startsWith('"') && v.endsWith('"')) ||
33
+ (v.startsWith("'") && v.endsWith("'"))))
34
+ return v.slice(1, -1);
35
+ return v;
36
+ }
37
+ function parseEnvLines(text) {
38
+ const lines = text.split(/\r?\n/);
39
+ if (lines.length && lines[lines.length - 1] === "")
40
+ lines.pop();
41
+ return lines.map((raw) => {
42
+ const m = raw.trimStart().startsWith("#") ? null : ENTRY.exec(raw);
43
+ return m
44
+ ? { kind: "entry", name: m[1], value: unquote(m[2]), raw }
45
+ : { kind: "other", raw };
46
+ });
47
+ }
48
+ /** The entries of an env file, by name (the last one wins, as in Compose). */
49
+ function envEntries(lines) {
50
+ const out = new Map();
51
+ for (const line of lines)
52
+ if (line.kind === "entry")
53
+ out.set(line.name, line.value);
54
+ return out;
55
+ }
56
+ function readEnvFile(file) {
57
+ return envEntries(parseEnvLines((0, fs_1.readFileSync)(file, "utf8")));
58
+ }
59
+ const REF = /\$\{([A-Za-z_][A-Za-z0-9_]*)(:-([^}]*))?\}/g;
60
+ /** Expands `${NAME}` references between values, in order (later values see earlier ones). */
61
+ function expandEnv(entries, base = {}) {
62
+ const out = {};
63
+ for (const [name, value] of entries) {
64
+ out[name] = value.replace(REF, (_m, ref, dflt, fallback) => {
65
+ var _a;
66
+ const v = (_a = out[ref]) !== null && _a !== void 0 ? _a : base[ref];
67
+ if (v !== undefined && v !== "")
68
+ return v;
69
+ return dflt !== undefined ? fallback : "";
70
+ });
71
+ }
72
+ return out;
73
+ }
74
+ /** Writes a file and sets its mode (also when it already existed with another one). */
75
+ function writeWithMode(file, content, mode) {
76
+ (0, fs_1.writeFileSync)(file, content, { mode });
77
+ (0, fs_1.chmodSync)(file, mode);
78
+ }
79
+ function serializeEnvLines(lines) {
80
+ return lines.map((l) => l.raw).join("\n") + "\n";
81
+ }
@@ -0,0 +1,10 @@
1
+ /** The Compose file that turns on HTTPS in Envoy (ops/bundle/compose.tls.yaml). */
2
+ export declare const TLS_COMPOSE_FILE = "compose.tls.yaml";
3
+ export declare function isLocalHost(host: string): boolean;
4
+ /** Whether .env turns on compose.tls.yaml (COMPOSE_FILE, `:`- or `,`-separated). */
5
+ export declare function tlsEnabled(env: Record<string, string | undefined>): boolean;
6
+ /**
7
+ * Warnings about how the dashboard and API are exposed (M10): plain HTTP on an address the
8
+ * internet may reach, or an HTTPS setup that is incomplete. They never stop init or check.
9
+ */
10
+ export declare function exposureWarnings(env: Record<string, string | undefined>, dir: string): string[];
@@ -0,0 +1,93 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TLS_COMPOSE_FILE = void 0;
4
+ exports.isLocalHost = isLocalHost;
5
+ exports.tlsEnabled = tlsEnabled;
6
+ exports.exposureWarnings = exposureWarnings;
7
+ /**
8
+ * Copyright (C) 2025 by Fonoster Inc (https://fonoster.com)
9
+ * http://github.com/fonoster/fonoster
10
+ *
11
+ * This file is part of Fonoster
12
+ *
13
+ * Licensed under the MIT License (the "License");
14
+ * you may not use this file except in compliance with
15
+ * the License. You may obtain a copy of the License at
16
+ *
17
+ * https://opensource.org/licenses/MIT
18
+ *
19
+ * Unless required by applicable law or agreed to in writing, software
20
+ * distributed under the License is distributed on an "AS IS" BASIS,
21
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
22
+ * See the License for the specific language governing permissions and
23
+ * limitations under the License.
24
+ */
25
+ const fs_1 = require("fs");
26
+ const net_1 = require("net");
27
+ const path_1 = require("path");
28
+ const url_1 = require("url");
29
+ /** The Compose file that turns on HTTPS in Envoy (ops/bundle/compose.tls.yaml). */
30
+ exports.TLS_COMPOSE_FILE = "compose.tls.yaml";
31
+ /** Loopback, private and link-local ranges: a URL on them is not reachable from the internet. */
32
+ const LOCAL = new net_1.BlockList();
33
+ LOCAL.addSubnet("127.0.0.0", 8);
34
+ LOCAL.addSubnet("10.0.0.0", 8);
35
+ LOCAL.addSubnet("172.16.0.0", 12);
36
+ LOCAL.addSubnet("192.168.0.0", 16);
37
+ LOCAL.addSubnet("169.254.0.0", 16);
38
+ LOCAL.addSubnet("100.64.0.0", 10);
39
+ LOCAL.addAddress("::1", "ipv6");
40
+ LOCAL.addSubnet("fc00::", 7, "ipv6");
41
+ LOCAL.addSubnet("fe80::", 10, "ipv6");
42
+ function isLocalHost(host) {
43
+ const h = host.replace(/^\[|\]$/g, "").toLowerCase();
44
+ if (h === "localhost" || h.endsWith(".localhost") || h.endsWith(".local"))
45
+ return true;
46
+ const family = (0, net_1.isIP)(h);
47
+ if (family === 0)
48
+ return false;
49
+ return LOCAL.check(h, family === 6 ? "ipv6" : "ipv4");
50
+ }
51
+ /** Whether .env turns on compose.tls.yaml (COMPOSE_FILE, `:`- or `,`-separated). */
52
+ function tlsEnabled(env) {
53
+ var _a;
54
+ return ((_a = env.COMPOSE_FILE) !== null && _a !== void 0 ? _a : "")
55
+ .split(/[:,;]/)
56
+ .some((f) => f.trim().replace(/^\.\//, "") === exports.TLS_COMPOSE_FILE);
57
+ }
58
+ const HOW = "set COMPOSE_FILE=compose.yaml:compose.tls.yaml with TLS_CERT and TLS_KEY in .env (see .env.example), or put a TLS proxy in front with HTTP_BIND=127.0.0.1";
59
+ /**
60
+ * Warnings about how the dashboard and API are exposed (M10): plain HTTP on an address the
61
+ * internet may reach, or an HTTPS setup that is incomplete. They never stop init or check.
62
+ */
63
+ function exposureWarnings(env, dir) {
64
+ var _a;
65
+ const warnings = [];
66
+ const publicUrl = (_a = env.PUBLIC_URL) !== null && _a !== void 0 ? _a : "";
67
+ let url;
68
+ try {
69
+ url = new url_1.URL(publicUrl);
70
+ }
71
+ catch (_b) {
72
+ if (publicUrl)
73
+ warnings.push(`PUBLIC_URL "${publicUrl}" is not a URL (http(s)://host)`);
74
+ }
75
+ const tls = tlsEnabled(env);
76
+ if (tls) {
77
+ ["TLS_CERT", "TLS_KEY"].forEach((name) => {
78
+ const file = env[name];
79
+ if (!file)
80
+ warnings.push(`COMPOSE_FILE names ${exports.TLS_COMPOSE_FILE} but ${name} is not set: Compose will refuse to start`);
81
+ else if (!(0, fs_1.existsSync)((0, path_1.isAbsolute)(file) ? file : (0, path_1.resolve)(dir, file)))
82
+ warnings.push(`${name}: ${file} not found on this machine`);
83
+ });
84
+ if (url && url.protocol !== "https:")
85
+ warnings.push(`HTTPS is on (${exports.TLS_COMPOSE_FILE}) but PUBLIC_URL is ${url.protocol}//: set PUBLIC_URL=https://<the certificate's domain> and run init again`);
86
+ return warnings;
87
+ }
88
+ if (env.TLS_CERT || env.TLS_KEY)
89
+ warnings.push(`TLS_CERT/TLS_KEY are set but COMPOSE_FILE does not name ${exports.TLS_COMPOSE_FILE}: Envoy still serves plain HTTP`);
90
+ if ((url === null || url === void 0 ? void 0 : url.protocol) === "http:" && !isLocalHost(url.hostname))
91
+ warnings.push(`PUBLIC_URL is ${url.origin}: the dashboard and API (passwords, API keys, tokens) go over plain HTTP to an address others may reach. For HTTPS, ${HOW}`);
92
+ return warnings;
93
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -1,6 +1,6 @@
1
+ #!/usr/bin/env node
1
2
  "use strict";
2
3
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.tokenHasAccessKeyId = tokenHasAccessKeyId;
4
4
  /**
5
5
  * Copyright (C) 2025 by Fonoster Inc (https://fonoster.com)
6
6
  * http://github.com/fonoster/fonoster
@@ -19,10 +19,9 @@ exports.tokenHasAccessKeyId = tokenHasAccessKeyId;
19
19
  * See the License for the specific language governing permissions and
20
20
  * limitations under the License.
21
21
  */
22
- const _1 = require(".");
23
- function tokenHasAccessKeyId(token, accessKeyId) {
24
- var _a;
25
- const decodedToken = (0, _1.decodeToken)(token);
26
- const accessKeyIds = (_a = decodedToken.access) === null || _a === void 0 ? void 0 : _a.map((a) => a.accessKeyId);
27
- return accessKeyIds.includes(accessKeyId);
28
- }
22
+ const main_1 = require("./main");
23
+ // fonoster-config (bin): see ./main for the commands.
24
+ (0, main_1.main)(process.argv.slice(2)).then((code) => process.exit(code), (e) => {
25
+ process.stderr.write(`fonoster-config: ${e.message}\n`);
26
+ process.exit(1);
27
+ });
@@ -0,0 +1,6 @@
1
+ import { Io } from "./types";
2
+ export type InfoOptions = {
3
+ dir: string;
4
+ };
5
+ /** How to reach the install, from its local files only (.env). */
6
+ export declare function runInfo(options: InfoOptions, io: Io): number;
@@ -0,0 +1,62 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.runInfo = runInfo;
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 fs_1 = require("fs");
23
+ const path_1 = require("path");
24
+ const url_1 = require("url");
25
+ const envFile_1 = require("./envFile");
26
+ const exposure_1 = require("./exposure");
27
+ /** How to reach the install, from its local files only (.env). */
28
+ function runInfo(options, io) {
29
+ const envFile = (0, path_1.join)(options.dir, ".env");
30
+ if (!(0, fs_1.existsSync)(envFile) || !(0, fs_1.existsSync)((0, path_1.join)(options.dir, "config"))) {
31
+ io.err("no .env here: run `npx -p @fonoster/common fonoster-config init` first");
32
+ return 1;
33
+ }
34
+ const env = (0, envFile_1.expandEnv)((0, envFile_1.readEnvFile)(envFile));
35
+ const host = env.HOST_IP || "localhost";
36
+ const httpPort = env.HTTP_PORT || "8449";
37
+ const sipPort = env.SIP_PORT || "5060";
38
+ const publicUrl = env.PUBLIC_URL || `http://${host}:${httpPort}`;
39
+ // compose.tls.yaml: Envoy serves HTTPS on 443 for PUBLIC_URL's host.
40
+ let tlsHost = host;
41
+ try {
42
+ tlsHost = new url_1.URL(publicUrl).hostname;
43
+ }
44
+ catch (_a) {
45
+ // Not a URL: check and init say so.
46
+ }
47
+ const api = (0, exposure_1.tlsEnabled)(env)
48
+ ? `${tlsHost}:443 (gRPC and gRPC-Web, TLS)`
49
+ : `${host}:${httpPort} (gRPC and gRPC-Web, plain HTTP)`;
50
+ const lines = [
51
+ `Dashboard ${publicUrl}`,
52
+ `Owner ${env.OWNER_EMAIL || "(OWNER_EMAIL not set)"} / ${env.OWNER_PASSWORD || "(OWNER_PASSWORD not set)"}`,
53
+ `API ${api}`,
54
+ `SIP ${host}:${sipPort} (UDP and TCP)`,
55
+ "",
56
+ `Files ${envFile} Compose settings and the generated secrets (back it up)`,
57
+ ` ${(0, path_1.join)(options.dir, "config")}/ service settings (from config/*.example.*)`,
58
+ ` ${(0, path_1.join)(options.dir, "config", "keys")}/ signing keys`
59
+ ];
60
+ lines.forEach((l) => io.out(l));
61
+ return 0;
62
+ }
@@ -0,0 +1,9 @@
1
+ import { Io } from "./types";
2
+ export type InitOptions = {
3
+ /** The bundle folder (compose.yaml, .env.example, config/). */
4
+ dir: string;
5
+ hostIp?: string;
6
+ /** Finds the host address (tests). */
7
+ detectHostIp?: () => Promise<string | undefined>;
8
+ };
9
+ export declare function runInit(options: InitOptions, io: Io): Promise<number>;
@@ -0,0 +1,152 @@
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.runInit = runInit;
13
+ /**
14
+ * Copyright (C) 2025 by Fonoster Inc (https://fonoster.com)
15
+ * http://github.com/fonoster/fonoster
16
+ *
17
+ * This file is part of Fonoster
18
+ *
19
+ * Licensed under the MIT License (the "License");
20
+ * you may not use this file except in compliance with
21
+ * the License. You may obtain a copy of the License at
22
+ *
23
+ * https://opensource.org/licenses/MIT
24
+ *
25
+ * Unless required by applicable law or agreed to in writing, software
26
+ * distributed under the License is distributed on an "AS IS" BASIS,
27
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
28
+ * See the License for the specific language governing permissions and
29
+ * limitations under the License.
30
+ */
31
+ const fs_1 = require("fs");
32
+ const path_1 = require("path");
33
+ const envFile_1 = require("./envFile");
34
+ const exposure_1 = require("./exposure");
35
+ const render_1 = require("./render");
36
+ const secrets_1 = require("./secrets");
37
+ const HOST = /^[A-Za-z0-9]([A-Za-z0-9.-]*[A-Za-z0-9])?$|^[0-9a-fA-F:]+$/;
38
+ /** The .env lines after a run: new values only for missing names or empty secrets. */
39
+ function mergeEnv(example, existing, hostIp) {
40
+ const added = [];
41
+ const generated = [];
42
+ const valueFor = (name, exampleValue) => {
43
+ if (name === "HOST_IP")
44
+ return hostIp;
45
+ if (exampleValue !== "")
46
+ return exampleValue;
47
+ const secret = (0, secrets_1.generateSecret)(name);
48
+ if (secret !== undefined)
49
+ generated.push(name);
50
+ return secret !== null && secret !== void 0 ? secret : "";
51
+ };
52
+ const entry = (name, value) => ({
53
+ kind: "entry",
54
+ name,
55
+ value,
56
+ raw: `${name}=${value}`
57
+ });
58
+ if (!existing) {
59
+ const lines = example.map((l) => l.kind === "entry" && (l.value === "" || l.name === "HOST_IP")
60
+ ? entry(l.name, valueFor(l.name, l.value))
61
+ : l);
62
+ return { lines, added: [], generated };
63
+ }
64
+ // Existing lines stay as they are, except an empty secret (filled) and HOST_IP when
65
+ // --host-ip names another address.
66
+ const lines = existing.map((l) => {
67
+ if (l.kind !== "entry")
68
+ return l;
69
+ if (l.name === "HOST_IP")
70
+ return l.value === hostIp ? l : entry(l.name, hostIp);
71
+ if (l.value === "" && (0, secrets_1.generateSecret)(l.name) !== undefined)
72
+ return entry(l.name, valueFor(l.name, ""));
73
+ return l;
74
+ });
75
+ const have = (0, envFile_1.envEntries)(existing);
76
+ const missing = example.filter((l) => l.kind === "entry" && !have.has(l.name));
77
+ if (missing.length) {
78
+ lines.push({
79
+ kind: "other",
80
+ raw: `# Added by fonoster-config init (${new Date().toISOString().slice(0, 10)})`
81
+ });
82
+ for (const l of missing) {
83
+ lines.push(entry(l.name, valueFor(l.name, l.value)));
84
+ added.push(l.name);
85
+ }
86
+ }
87
+ return { lines, added, generated };
88
+ }
89
+ function runInit(options, io) {
90
+ return __awaiter(this, void 0, void 0, function* () {
91
+ var _a;
92
+ const dir = options.dir;
93
+ const exampleFile = (0, path_1.join)(dir, ".env.example");
94
+ const envFile = (0, path_1.join)(dir, ".env");
95
+ const configDir = (0, path_1.join)(dir, "config");
96
+ if (!(0, fs_1.existsSync)(exampleFile) || !(0, fs_1.existsSync)(configDir)) {
97
+ io.err(`${dir} has no .env.example or config/: run init in the unpacked fonoster folder, or pass --dir <folder>`);
98
+ return 1;
99
+ }
100
+ if (options.hostIp !== undefined && !HOST.test(options.hostIp)) {
101
+ io.err(`--host-ip: "${options.hostIp}" is not an address`);
102
+ return 1;
103
+ }
104
+ const example = (0, envFile_1.parseEnvLines)((0, fs_1.readFileSync)(exampleFile, "utf8"));
105
+ const existing = (0, fs_1.existsSync)(envFile)
106
+ ? (0, envFile_1.parseEnvLines)((0, fs_1.readFileSync)(envFile, "utf8"))
107
+ : undefined;
108
+ const kept = existing ? (0, envFile_1.envEntries)(existing).get("HOST_IP") : undefined;
109
+ const hostIp = options.hostIp ||
110
+ kept ||
111
+ (yield ((_a = options.detectHostIp) !== null && _a !== void 0 ? _a : (() => (0, secrets_1.detectHostIp)()))());
112
+ if (!hostIp) {
113
+ io.err("could not detect this machine's address: pass --host-ip <address> (the address softphones and browsers reach it at)");
114
+ return 1;
115
+ }
116
+ const { lines, added, generated } = mergeEnv(example, existing, hostIp);
117
+ (0, envFile_1.writeWithMode)(envFile, (0, envFile_1.serializeEnvLines)(lines), 0o600);
118
+ // config/ holds secrets: only the owner may list it. Files inside are readable by the
119
+ // containers, which run as another user and reach them through their bind mounts.
120
+ (0, fs_1.chmodSync)(configDir, 0o700);
121
+ const keys = (0, secrets_1.ensureKeys)(configDir);
122
+ const env = (0, envFile_1.expandEnv)((0, envFile_1.envEntries)(lines));
123
+ let rendered;
124
+ try {
125
+ rendered = (0, render_1.renderConfigDir)({ configDir, env });
126
+ }
127
+ catch (e) {
128
+ if (!(e instanceof render_1.RenderError))
129
+ throw e;
130
+ e.problems.forEach((p) => io.err(p));
131
+ io.err("fix .env (or the example) and run init again");
132
+ return 1;
133
+ }
134
+ const rel = (p) => (0, path_1.relative)(dir, p) || ".";
135
+ io.out(existing
136
+ ? `.env kept${added.length ? `; added ${added.join(", ")}` : ""}`
137
+ : `.env written (${generated.length} secrets generated)`);
138
+ const filled = generated.filter((n) => !added.includes(n));
139
+ if (existing && filled.length)
140
+ io.out(`generated ${filled.join(", ")}`);
141
+ io.out(keys.length
142
+ ? `keys: ${keys.length} files created in ${rel((0, path_1.join)(configDir, "keys"))}`
143
+ : "keys: kept");
144
+ io.out(`config: ${rendered.map((f) => `config/${f}`).join(", ")}`);
145
+ io.out(`host address: ${hostIp}`);
146
+ (0, exposure_1.exposureWarnings)(env, dir).forEach((w) => io.err(`warning: ${w}`));
147
+ io.out("");
148
+ io.out("Next: docker compose up -d");
149
+ io.out("Then: npx -p @fonoster/common fonoster-config info (dashboard URL and owner login)");
150
+ return 0;
151
+ });
152
+ }