@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,79 @@
1
+ import { Capability, CatalogEntry, ItemKind, ModelEntry, ProviderEntry, RegistryStore, Usage, UsageSource, VoiceEntry } from "./types";
2
+ /** Adapters that ship with this version (add-provider-registry, P2). */
3
+ export declare const BUILT_IN_ADAPTERS: string[];
4
+ export declare class RegistryError extends Error {
5
+ }
6
+ export type ImpactLine = {
7
+ mark: "✓" | "✗" | "~";
8
+ text: string;
9
+ count: number;
10
+ };
11
+ export type Impact = {
12
+ lines: ImpactLine[];
13
+ mappable: Usage[];
14
+ unmappable: Usage[];
15
+ };
16
+ export type RegistryOptions = {
17
+ store: RegistryStore;
18
+ usages?: UsageSource;
19
+ actor?: string;
20
+ /** Allows the fake adapter (test deployments only; conformance "Implementations ship fake adapters"). */
21
+ testMode?: boolean;
22
+ now?: () => Date;
23
+ /** The command line that made the change, stored in every audit entry (spec "Every write is audited"). */
24
+ commandLine?: string;
25
+ };
26
+ /** The registry operations behind the `fonoster-registry` utility. */
27
+ export declare class Registry {
28
+ private readonly options;
29
+ private readonly usages;
30
+ constructor(options: RegistryOptions);
31
+ private get store();
32
+ private audit;
33
+ /** Rejects anything that looks like a credential (the catalog never holds a secret). */
34
+ private assertNoSecrets;
35
+ private assertNew;
36
+ private require;
37
+ addProvider(input: {
38
+ id: string;
39
+ adapter: string;
40
+ capabilities: Capability[];
41
+ credentialRef: string;
42
+ } & Record<string, unknown>): ProviderEntry;
43
+ addModel(input: {
44
+ id: string;
45
+ type: Capability;
46
+ features?: string[];
47
+ languages?: string[];
48
+ settings?: string[];
49
+ } & Record<string, unknown>): ModelEntry;
50
+ addVoice(input: {
51
+ id: string;
52
+ models: string[];
53
+ language?: string;
54
+ gender?: string;
55
+ ownerWorkspace?: string;
56
+ } & Record<string, unknown>): VoiceEntry;
57
+ private requireProviderFor;
58
+ /** What moving the usages of `id` to `replacement` would do (shared by deprecate and retire). */
59
+ impact(id: string, replacement: string, voiceMap?: Record<string, string>): Impact;
60
+ deprecate(kind: ItemKind, id: string, replacement: string, retireOn: string): Impact;
61
+ retire(kind: ItemKind, id: string, voiceMap?: Record<string, string>): Impact;
62
+ /** What the gateway does with an id at session start: retired ids resolve to their replacement. */
63
+ resolve(id: string): {
64
+ id: string;
65
+ event?: "provider.deprecated_used" | "provider.retired_used";
66
+ replacement?: string;
67
+ };
68
+ show(id: string): CatalogEntry;
69
+ list(kind?: ItemKind): CatalogEntry[];
70
+ /** Voices the dashboard offers for a model: active or deprecated voices that work on it. */
71
+ voicesFor(modelId: string): VoiceEntry[];
72
+ usage(id: string): Usage[];
73
+ history(id?: string): import("./types").AuditEntry[];
74
+ /** The whole catalog, sorted and without secrets, as YAML. Byte-identical for equal catalogs. */
75
+ export(): string;
76
+ plan(file: string): string[];
77
+ apply(file: string): string[];
78
+ private diff;
79
+ }
@@ -0,0 +1,418 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Registry = exports.RegistryError = exports.BUILT_IN_ADAPTERS = 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 yaml_1 = require("yaml");
23
+ const features_1 = require("./features");
24
+ /** Adapters that ship with this version (add-provider-registry, P2). */
25
+ exports.BUILT_IN_ADAPTERS = [
26
+ "deepgram",
27
+ "elevenlabs",
28
+ "google",
29
+ "azure",
30
+ "openai",
31
+ "anthropic",
32
+ "groq",
33
+ "ollama",
34
+ "fake"
35
+ ];
36
+ // provider/name; the name may have its own slashes, as hosted models do (groq/openai/gpt-oss-120b).
37
+ const CATALOG_ID = /^[a-z0-9-]+(\/[A-Za-z0-9._-]+)+$/;
38
+ const REF = /^[a-z0-9][a-z0-9-]*$/;
39
+ const SECRET_KEY = /(api[-_]?key|secret|token|password|private[-_]?key)/i;
40
+ const SECRET_VALUE = /^(sk-|sk_|xox[abp]-|AKIA|AIza|-----BEGIN)/;
41
+ class RegistryError extends Error {
42
+ }
43
+ exports.RegistryError = RegistryError;
44
+ const noUsages = {
45
+ find: () => [],
46
+ rewrite: () => true,
47
+ notify: () => undefined
48
+ };
49
+ /** The registry operations behind the `fonoster-registry` utility. */
50
+ class Registry {
51
+ constructor(options) {
52
+ var _a;
53
+ this.options = options;
54
+ this.usages = (_a = options.usages) !== null && _a !== void 0 ? _a : noUsages;
55
+ }
56
+ get store() {
57
+ return this.options.store;
58
+ }
59
+ audit(action, target, details) {
60
+ var _a, _b, _c, _d, _e;
61
+ this.store.audit({
62
+ at: ((_c = (_b = (_a = this.options).now) === null || _b === void 0 ? void 0 : _b.call(_a)) !== null && _c !== void 0 ? _c : new Date()).toISOString(),
63
+ actor: (_e = (_d = this.options.actor) !== null && _d !== void 0 ? _d : process.env.USER) !== null && _e !== void 0 ? _e : "operator",
64
+ action,
65
+ target,
66
+ details: this.options.commandLine
67
+ ? Object.assign({ command: this.options.commandLine }, details) : details
68
+ });
69
+ }
70
+ /** Rejects anything that looks like a credential (the catalog never holds a secret). */
71
+ assertNoSecrets(input) {
72
+ for (const [k, v] of Object.entries(input)) {
73
+ if (SECRET_KEY.test(k) ||
74
+ (typeof v === "string" && SECRET_VALUE.test(v))) {
75
+ throw new RegistryError(`secrets cannot be stored in the catalog (${k}); store keys as workspace Secrets or platform credentials and pass --credential-ref`);
76
+ }
77
+ }
78
+ }
79
+ assertNew(id) {
80
+ if (!CATALOG_ID.test(id) && !REF.test(id))
81
+ throw new RegistryError(`invalid catalog id ${id}`);
82
+ const existing = this.store.get(id);
83
+ if (existing)
84
+ throw new RegistryError(`catalog id ${id} already exists (state: ${existing.state})`);
85
+ }
86
+ require(id, kind) {
87
+ const e = this.store.get(id);
88
+ if (!e || e.kind !== kind)
89
+ throw new RegistryError(`no ${kind} ${id}`);
90
+ return e;
91
+ }
92
+ // ---- add --------------------------------------------------------------------------
93
+ addProvider(input) {
94
+ this.assertNoSecrets(input);
95
+ this.assertNew(input.id);
96
+ if (!exports.BUILT_IN_ADAPTERS.includes(input.adapter))
97
+ throw new RegistryError(`unknown adapter: ${input.adapter}`);
98
+ if (input.adapter === "fake" && !this.options.testMode) {
99
+ throw new RegistryError("the fake adapter is only available in test mode");
100
+ }
101
+ if (!REF.test(input.credentialRef))
102
+ throw new RegistryError("credentialRef must be a name such as deepgram-default");
103
+ const entry = {
104
+ kind: "provider",
105
+ id: input.id,
106
+ adapter: input.adapter,
107
+ capabilities: input.capabilities,
108
+ credentialRef: input.credentialRef,
109
+ state: "active"
110
+ };
111
+ this.store.put(entry);
112
+ this.audit("add-provider", entry.id, { entry, before: null, after: entry });
113
+ return entry;
114
+ }
115
+ addModel(input) {
116
+ var _a, _b, _c, _d;
117
+ this.assertNoSecrets(input);
118
+ this.assertNew(input.id);
119
+ this.requireProviderFor(input.id, input.type);
120
+ const unknown = (0, features_1.unknownFeatures)(input.type, (_a = input.features) !== null && _a !== void 0 ? _a : []);
121
+ if (unknown.length)
122
+ throw new RegistryError(`unknown ${input.type} features: ${unknown.join(", ")}`);
123
+ const entry = {
124
+ kind: "model",
125
+ id: input.id,
126
+ type: input.type,
127
+ features: (_b = input.features) !== null && _b !== void 0 ? _b : [],
128
+ languages: (_c = input.languages) !== null && _c !== void 0 ? _c : [],
129
+ settings: (_d = input.settings) !== null && _d !== void 0 ? _d : [],
130
+ state: "active"
131
+ };
132
+ this.store.put(entry);
133
+ this.audit("add-model", entry.id, { entry, before: null, after: entry });
134
+ return entry;
135
+ }
136
+ addVoice(input) {
137
+ this.assertNoSecrets(input);
138
+ this.assertNew(input.id);
139
+ for (const m of input.models) {
140
+ const model = this.require(m, "model");
141
+ if (model.type !== "tts")
142
+ throw new RegistryError(`${m} is not a tts model`);
143
+ }
144
+ const entry = {
145
+ kind: "voice",
146
+ id: input.id,
147
+ models: input.models,
148
+ language: input.language,
149
+ gender: input.gender,
150
+ ownerWorkspace: input.ownerWorkspace,
151
+ state: "active"
152
+ };
153
+ this.store.put(entry);
154
+ this.audit("add-voice", entry.id, { entry, before: null, after: entry });
155
+ return entry;
156
+ }
157
+ requireProviderFor(id, type) {
158
+ const provider = this.store.get(id.split("/")[0]);
159
+ if (!provider || provider.kind !== "provider")
160
+ throw new RegistryError(`no provider ${id.split("/")[0]}`);
161
+ if (!provider.capabilities.includes(type))
162
+ throw new RegistryError(`provider ${provider.id} has no ${type} capability`);
163
+ }
164
+ // ---- impact, deprecate, retire ----------------------------------------------------
165
+ /** What moving the usages of `id` to `replacement` would do (shared by deprecate and retire). */
166
+ impact(id, replacement, voiceMap = {}) {
167
+ var _a;
168
+ const item = this.store.get(id);
169
+ const repl = this.store.get(replacement);
170
+ if (!item || !repl)
171
+ throw new RegistryError(`unknown ${!item ? id : replacement}`);
172
+ const usages = this.usages.find(id);
173
+ const mappable = [];
174
+ const unmappable = [];
175
+ const counts = new Map();
176
+ const bump = (key, line) => {
177
+ var _a;
178
+ const l = (_a = counts.get(key)) !== null && _a !== void 0 ? _a : Object.assign(Object.assign({}, line), { count: 0 });
179
+ l.count++;
180
+ counts.set(key, l);
181
+ };
182
+ for (const u of usages) {
183
+ let ok = true;
184
+ if (item.kind === "model" && repl.kind === "model") {
185
+ const missing = u.requires.filter((f) => features_1.FEATURES[repl.type][f] === "hard" && !repl.features.includes(f));
186
+ if (missing.length) {
187
+ ok = false;
188
+ bump(`req:${missing}`, {
189
+ mark: "✗",
190
+ text: `requires ${missing.join(", ")}, which ${replacement} lacks`
191
+ });
192
+ }
193
+ if (u.voice) {
194
+ const voice = this.store.get(u.voice);
195
+ const name = (_a = u.voice.split("/")[1]) !== null && _a !== void 0 ? _a : u.voice;
196
+ if (voice === null || voice === void 0 ? void 0 : voice.models.includes(replacement))
197
+ bump(`v:${u.voice}`, {
198
+ mark: "✓",
199
+ text: `voice ${name} works on replacement`
200
+ });
201
+ else if (voiceMap[name])
202
+ bump(`vm:${u.voice}`, {
203
+ mark: "✓",
204
+ text: `voice ${name} → ${voiceMap[name]}`
205
+ });
206
+ else {
207
+ ok = false;
208
+ const owned = (voice === null || voice === void 0 ? void 0 : voice.ownerWorkspace)
209
+ ? ` (workspace ${voice.ownerWorkspace}'s own voice)`
210
+ : "";
211
+ bump(`vx:${u.voice}`, {
212
+ mark: "✗",
213
+ text: `voice ${name}${owned} only exists on ${id.split("/")[1]} → needs --voice-map ${name}=<voice>`
214
+ });
215
+ }
216
+ }
217
+ for (const s of u.settings.filter((s) => !repl.settings.includes(s))) {
218
+ bump(`s:${s}`, {
219
+ mark: "~",
220
+ text: `setting "${s}" not supported, will be dropped (soft)`
221
+ });
222
+ }
223
+ }
224
+ else if (item.kind === "voice" && repl.kind === "voice") {
225
+ if (u.model && !repl.models.includes(u.model)) {
226
+ ok = false;
227
+ bump(`m:${u.model}`, {
228
+ mark: "✗",
229
+ text: `${replacement} does not work on ${u.model}`
230
+ });
231
+ }
232
+ else
233
+ bump("ok", { mark: "✓", text: `works on the same model` });
234
+ }
235
+ else
236
+ bump("ok", { mark: "✓", text: "config moves to the replacement" });
237
+ (ok ? mappable : unmappable).push(u);
238
+ }
239
+ return { lines: [...counts.values()], mappable, unmappable };
240
+ }
241
+ deprecate(kind, id, replacement, retireOn) {
242
+ const item = this.require(id, kind);
243
+ const repl = this.store.get(replacement);
244
+ if (!repl ||
245
+ repl.kind !== kind ||
246
+ repl.state !== "active" ||
247
+ (kind === "model" &&
248
+ repl.type !== item.type)) {
249
+ throw new RegistryError(`replacement must be an active ${kind}`);
250
+ }
251
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(retireOn))
252
+ throw new RegistryError("--retire-on must be a date (YYYY-MM-DD)");
253
+ const report = this.impact(id, replacement);
254
+ const after = Object.assign(Object.assign({}, item), { state: "deprecated", replacement,
255
+ retireOn });
256
+ this.store.put(after);
257
+ this.audit(`deprecate-${kind}`, id, {
258
+ replacement,
259
+ retireOn,
260
+ before: item,
261
+ after
262
+ });
263
+ const workspaces = new Set(this.usages.find(id).map((u) => u.workspace));
264
+ // One provider.deprecated notice per workspace (sent as a workspace webhook by the apiserver).
265
+ for (const w of workspaces)
266
+ this.usages.notify(w, {
267
+ type: "provider.deprecated",
268
+ id,
269
+ replacement,
270
+ retireOn
271
+ });
272
+ return report;
273
+ }
274
+ retire(kind, id, voiceMap = {}) {
275
+ var _a;
276
+ const item = this.require(id, kind);
277
+ if (!item.replacement)
278
+ throw new RegistryError(`${id} must be deprecated with a replacement before it is retired`);
279
+ const report = this.impact(id, item.replacement, voiceMap);
280
+ const repl = this.store.get(item.replacement);
281
+ for (const u of report.mappable) {
282
+ const name = (_a = u.voice) === null || _a === void 0 ? void 0 : _a.split("/")[1];
283
+ const voice = kind === "voice"
284
+ ? item.replacement
285
+ : name && voiceMap[name]
286
+ ? `${u.voice.split("/")[0]}/${voiceMap[name]}`
287
+ : undefined;
288
+ const dropSettings = repl.kind === "model"
289
+ ? u.settings.filter((s) => !repl.settings.includes(s))
290
+ : [];
291
+ const change = {
292
+ model: kind === "model" ? item.replacement : undefined,
293
+ voice,
294
+ dropSettings
295
+ };
296
+ if (this.usages.rewrite(u, change))
297
+ this.audit("rewrite-config", id, {
298
+ workspace: u.workspace,
299
+ config: u.config,
300
+ change
301
+ });
302
+ }
303
+ for (const w of new Set([...report.mappable, ...report.unmappable].map((u) => u.workspace))) {
304
+ this.usages.notify(w, {
305
+ type: "provider.retired",
306
+ id,
307
+ replacement: item.replacement
308
+ });
309
+ }
310
+ const after = Object.assign(Object.assign({}, item), { state: "retired" });
311
+ this.store.put(after);
312
+ this.audit(`retire-${kind}`, id, {
313
+ before: item,
314
+ after,
315
+ replacement: item.replacement,
316
+ unmappable: report.unmappable.length
317
+ });
318
+ return report;
319
+ }
320
+ /** What the gateway does with an id at session start: retired ids resolve to their replacement. */
321
+ resolve(id) {
322
+ const e = this.store.get(id);
323
+ if (!e)
324
+ return { id };
325
+ if (e.state === "retired" && e.replacement)
326
+ return Object.assign(Object.assign({}, this.resolve(e.replacement)), { event: "provider.retired_used", replacement: e.replacement });
327
+ if (e.state === "deprecated")
328
+ return {
329
+ id,
330
+ event: "provider.deprecated_used",
331
+ replacement: e.replacement
332
+ };
333
+ return { id };
334
+ }
335
+ // ---- read ---------------------------------------------------------------------------
336
+ show(id) {
337
+ const e = this.store.get(id);
338
+ if (!e)
339
+ throw new RegistryError(`no catalog entry ${id}`);
340
+ return e;
341
+ }
342
+ list(kind) {
343
+ return this.store.all().filter((e) => !kind || e.kind === kind);
344
+ }
345
+ /** Voices the dashboard offers for a model: active or deprecated voices that work on it. */
346
+ voicesFor(modelId) {
347
+ return this.store
348
+ .all()
349
+ .filter((e) => e.kind === "voice" &&
350
+ e.state !== "retired" &&
351
+ e.models.includes(modelId));
352
+ }
353
+ usage(id) {
354
+ return this.usages.find(id);
355
+ }
356
+ history(id) {
357
+ return this.store.history(id);
358
+ }
359
+ // ---- export, plan, apply ----------------------------------------------------------
360
+ /** The whole catalog, sorted and without secrets, as YAML. Byte-identical for equal catalogs. */
361
+ export() {
362
+ return (0, yaml_1.stringify)({
363
+ format: "fonoster.catalog.v1",
364
+ entries: this.store.all().map(canonical)
365
+ });
366
+ }
367
+ plan(file) {
368
+ return this.diff(file).map((d) => d.line);
369
+ }
370
+ apply(file) {
371
+ var _a;
372
+ const changes = this.diff(file);
373
+ for (const c of changes) {
374
+ const before = (_a = this.store.get(c.entry.id)) !== null && _a !== void 0 ? _a : null;
375
+ this.store.put(c.entry);
376
+ this.audit("apply", c.entry.id, {
377
+ change: c.line,
378
+ before,
379
+ after: c.entry
380
+ });
381
+ }
382
+ return changes.length ? changes.map((c) => c.line) : ["no changes"];
383
+ }
384
+ diff(file) {
385
+ const target = (0, yaml_1.parse)(file).entries;
386
+ const out = [];
387
+ for (const want of target) {
388
+ this.assertNoSecrets(want);
389
+ const have = this.store.get(want.id);
390
+ if (!have)
391
+ out.push({ line: `+ add ${want.kind} ${want.id}`, entry: want });
392
+ else if (JSON.stringify(canonical(have)) !== JSON.stringify(canonical(want))) {
393
+ const verb = have.state !== want.state
394
+ ? want.state === "deprecated"
395
+ ? "deprecate"
396
+ : want.state === "retired"
397
+ ? "retire"
398
+ : "update"
399
+ : "update";
400
+ const to = want.replacement && have.state !== want.state
401
+ ? ` → ${want.replacement}`
402
+ : "";
403
+ out.push({
404
+ line: `~ ${verb} ${want.kind} ${want.id}${to}`,
405
+ entry: want
406
+ });
407
+ }
408
+ }
409
+ return out;
410
+ }
411
+ }
412
+ exports.Registry = Registry;
413
+ /** Keys in a fixed order and no undefined values, so exports compare byte for byte. */
414
+ function canonical(e) {
415
+ return Object.fromEntries(Object.entries(e)
416
+ .filter(([, v]) => v !== undefined)
417
+ .sort(([a], [b]) => a.localeCompare(b)));
418
+ }
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env node
2
+ import { PrismaLike } from "./prisma";
3
+ /** Runs one command against the JSON file store (synchronous; the default store). */
4
+ export declare function run(argv: string[], out?: (s: string) => void): number;
5
+ export declare const usesDatabase: (argv: string[]) => boolean;
6
+ /**
7
+ * Runs one command against the control-plane database: loads the catalog and usages, runs the
8
+ * command, then waits for every write, records skipped rewrites in the audit log and sends due
9
+ * `provider.deprecated` webhooks once (retries are adopted by a running dispatcher).
10
+ */
11
+ export declare function runWithDatabase(argv: string[], out?: (s: string) => void, deps?: {
12
+ prisma?: PrismaLike;
13
+ actor?: string;
14
+ }): Promise<number>;
15
+ /**
16
+ * Runs one command with telemetry (add-platform 2.5): a `registry.command` span and a
17
+ * `fonoster.registry.commands` count by command and outcome, flushed before returning. Off unless
18
+ * an OTLP endpoint is set.
19
+ */
20
+ export declare function runTraced(argv: string[], env?: Record<string, string | undefined>): Promise<number>;