nonotify 0.1.4 → 0.1.5

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.
package/README.md CHANGED
@@ -116,3 +116,41 @@ nnt send "some message for user" --profile=important-profile
116
116
  ```bash
117
117
  nnt "Task finished: migrations applied and tests passed"
118
118
  ```
119
+
120
+ ## Node.js API
121
+
122
+ `Notifier` loads config using `EnvConfigLoader` by default.
123
+
124
+ ```ts
125
+ import { Notifier } from "nonotify";
126
+
127
+ const notifier = new Notifier();
128
+
129
+ await notifier.send({
130
+ message: "Build finished successfully",
131
+ });
132
+ ```
133
+
134
+ Also you can pass profile data directly.
135
+
136
+ ```ts
137
+ import { Notifier } from "nonotify";
138
+
139
+ const notifier = new Notifier({
140
+ defaultProfile: "dev",
141
+ profiles: [
142
+ {
143
+ name: "dev",
144
+ botToken: process.env.TELEGRAM_BOT_TOKEN!,
145
+ chatId: process.env.TELEGRAM_CHAT_ID!,
146
+ },
147
+ ],
148
+ });
149
+
150
+ await notifier.send({
151
+ profile: "dev",
152
+ message: "Task completed",
153
+ });
154
+
155
+ console.log(notifier.profiles);
156
+ ```
package/dist/cli.d.ts ADDED
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env node
2
+ import { Cli } from "incur";
3
+ declare const cli: Cli.Cli<{
4
+ send: {
5
+ args: {
6
+ message: string;
7
+ };
8
+ options: {
9
+ profile?: string | undefined;
10
+ };
11
+ };
12
+ } & {}, undefined, undefined>;
13
+ export default cli;
package/dist/cli.js CHANGED
@@ -4,6 +4,7 @@ import { askConfirm, askRequired, askRequiredWithInitial, askSelect, } from "./p
4
4
  import { getConfigPath, loadConfig, saveConfig } from "./config.js";
5
5
  import { printKeyValueTable, printProfilesTable } from "./display.js";
6
6
  import { getLatestUpdateOffset, sendTelegramMessage, waitForChatId, } from "./telegram.js";
7
+ import { Notifier } from "./notifier.js";
7
8
  const profileCli = Cli.create("profile", {
8
9
  description: "Manage notification profiles",
9
10
  });
@@ -335,21 +336,11 @@ const cli = Cli.create("nnt", {
335
336
  profile: "p",
336
337
  },
337
338
  async run(c) {
338
- const config = await loadConfig();
339
- const profileName = c.options.profile ?? config.defaultProfile;
340
- if (!profileName) {
341
- throw new Error("No default profile found. Run `nnt profile add` first.");
342
- }
343
- const profile = config.profiles[profileName];
344
- if (!profile) {
345
- throw new Error(`Profile "${profileName}" not found.`);
346
- }
347
- await sendTelegramMessage(profile.botToken, profile.chatId, c.args.message);
348
- return {
349
- sent: true,
350
- profile: profileName,
351
- provider: profile.type,
352
- };
339
+ const notifier = new Notifier();
340
+ return notifier.send({
341
+ message: c.args.message,
342
+ profile: c.options.profile,
343
+ });
353
344
  },
354
345
  })
355
346
  .command(profileCli);
@@ -0,0 +1,15 @@
1
+ export type TelegramProfile = {
2
+ type: "telegram";
3
+ name: string;
4
+ botToken: string;
5
+ chatId: string;
6
+ createdAt: string;
7
+ };
8
+ export type NntConfig = {
9
+ defaultProfile: string | null;
10
+ profiles: Record<string, TelegramProfile>;
11
+ };
12
+ export declare function getConfigDir(): string;
13
+ export declare function getConfigPath(): string;
14
+ export declare function loadConfig(): Promise<NntConfig>;
15
+ export declare function saveConfig(config: NntConfig): Promise<void>;
@@ -0,0 +1,11 @@
1
+ type ProfileRow = {
2
+ name: string;
3
+ provider: string;
4
+ isDefault: boolean;
5
+ };
6
+ export declare function printProfilesTable(rows: ProfileRow[]): void;
7
+ export declare function printKeyValueTable(title: string, rows: Array<{
8
+ key: string;
9
+ value: string;
10
+ }>): void;
11
+ export {};
@@ -0,0 +1,2 @@
1
+ export { EnvConfigLoader, NoProfilesConfiguredError, Notifier, NotifierError, ProfileNotFoundError, } from "./notifier.js";
2
+ export type { NotifierConfig, NotifierConfigLoader, NotifierProfile, NotifierProfileInput, SendInput, SendResult, } from "./notifier.js";
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ export { EnvConfigLoader, NoProfilesConfiguredError, Notifier, NotifierError, ProfileNotFoundError, } from "./notifier.js";
@@ -0,0 +1,49 @@
1
+ export type NotifierProfile = {
2
+ type: "telegram";
3
+ name: string;
4
+ botToken: string;
5
+ chatId: string;
6
+ };
7
+ export type NotifierProfileInput = {
8
+ type?: "telegram";
9
+ name: string;
10
+ botToken: string;
11
+ chatId: string;
12
+ };
13
+ export type NotifierConfig = {
14
+ profiles: NotifierProfileInput[];
15
+ defaultProfile?: string | null;
16
+ };
17
+ export interface NotifierConfigLoader {
18
+ load(): NotifierConfig;
19
+ }
20
+ export type SendInput = {
21
+ message: string;
22
+ profile?: string;
23
+ };
24
+ export type SendResult = {
25
+ sent: true;
26
+ profile: string;
27
+ provider: "telegram";
28
+ };
29
+ export declare class NotifierError extends Error {
30
+ constructor(message: string);
31
+ }
32
+ export declare class ProfileNotFoundError extends NotifierError {
33
+ readonly profile: string;
34
+ constructor(profile: string);
35
+ }
36
+ export declare class NoProfilesConfiguredError extends NotifierError {
37
+ constructor();
38
+ }
39
+ export declare class EnvConfigLoader implements NotifierConfigLoader {
40
+ load(): NotifierConfig;
41
+ }
42
+ export declare class Notifier {
43
+ readonly profiles: readonly Readonly<NotifierProfile>[];
44
+ private readonly defaultProfile;
45
+ private readonly profilesByName;
46
+ constructor(source?: NotifierConfig | NotifierConfigLoader);
47
+ send(input: SendInput): Promise<SendResult>;
48
+ private resolveProfile;
49
+ }
@@ -0,0 +1,169 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { getConfigPath } from "./config.js";
3
+ import { sendTelegramMessage } from "./telegram.js";
4
+ export class NotifierError extends Error {
5
+ constructor(message) {
6
+ super(message);
7
+ this.name = "NotifierError";
8
+ }
9
+ }
10
+ export class ProfileNotFoundError extends NotifierError {
11
+ profile;
12
+ constructor(profile) {
13
+ super(`Profile "${profile}" not found.`);
14
+ this.name = "ProfileNotFoundError";
15
+ this.profile = profile;
16
+ }
17
+ }
18
+ export class NoProfilesConfiguredError extends NotifierError {
19
+ constructor() {
20
+ super("No profiles configured. Run `nnt profile add` first.");
21
+ this.name = "NoProfilesConfiguredError";
22
+ }
23
+ }
24
+ export class EnvConfigLoader {
25
+ load() {
26
+ const configPath = getConfigPath();
27
+ let rawConfig;
28
+ try {
29
+ const raw = readFileSync(configPath, "utf8");
30
+ rawConfig = JSON.parse(raw);
31
+ }
32
+ catch (error) {
33
+ if (isNodeError(error) && error.code === "ENOENT") {
34
+ return {
35
+ profiles: [],
36
+ defaultProfile: null,
37
+ };
38
+ }
39
+ if (error instanceof SyntaxError) {
40
+ throw new NotifierError(`Invalid JSON in config file at ${configPath}: ${error.message}`);
41
+ }
42
+ throw error;
43
+ }
44
+ return normalizeEnvConfig(rawConfig);
45
+ }
46
+ }
47
+ export class Notifier {
48
+ profiles;
49
+ defaultProfile;
50
+ profilesByName;
51
+ constructor(source = new EnvConfigLoader()) {
52
+ const config = isConfigLoader(source) ? source.load() : source;
53
+ const normalized = normalizeNotifierConfig(config);
54
+ const frozenProfiles = normalized.profiles.map((profile) => Object.freeze({ ...profile }));
55
+ this.profiles = Object.freeze(frozenProfiles);
56
+ this.defaultProfile = normalized.defaultProfile;
57
+ this.profilesByName = new Map(frozenProfiles.map((profile) => [profile.name, profile]));
58
+ }
59
+ async send(input) {
60
+ const message = input.message.trim();
61
+ if (message === "") {
62
+ throw new NotifierError("Message cannot be empty.");
63
+ }
64
+ const selectedProfile = this.resolveProfile(input.profile);
65
+ await sendTelegramMessage(selectedProfile.botToken, selectedProfile.chatId, message);
66
+ return {
67
+ sent: true,
68
+ profile: selectedProfile.name,
69
+ provider: selectedProfile.type,
70
+ };
71
+ }
72
+ resolveProfile(profileName) {
73
+ if (profileName) {
74
+ const profile = this.profilesByName.get(profileName);
75
+ if (!profile) {
76
+ throw new ProfileNotFoundError(profileName);
77
+ }
78
+ return profile;
79
+ }
80
+ if (this.defaultProfile) {
81
+ const defaultProfile = this.profilesByName.get(this.defaultProfile);
82
+ if (defaultProfile) {
83
+ return defaultProfile;
84
+ }
85
+ }
86
+ const firstProfile = this.profiles[0];
87
+ if (!firstProfile) {
88
+ throw new NoProfilesConfiguredError();
89
+ }
90
+ return firstProfile;
91
+ }
92
+ }
93
+ function normalizeEnvConfig(raw) {
94
+ return {
95
+ defaultProfile: typeof raw.defaultProfile === "string" ? raw.defaultProfile : null,
96
+ profiles: normalizeProfilesFromUnknown(raw.profiles),
97
+ };
98
+ }
99
+ function normalizeProfilesFromUnknown(rawProfiles) {
100
+ if (!rawProfiles) {
101
+ return [];
102
+ }
103
+ if (Array.isArray(rawProfiles)) {
104
+ return rawProfiles.map((profile, index) => normalizeSingleProfile(profile, `profiles[${index}]`));
105
+ }
106
+ if (typeof rawProfiles === "object") {
107
+ const entries = Object.entries(rawProfiles);
108
+ return entries.map(([name, profile]) => normalizeRecordProfile(name, profile, `profiles.${name}`));
109
+ }
110
+ throw new NotifierError("Invalid config format: `profiles` must be an array or object.");
111
+ }
112
+ function normalizeNotifierConfig(input) {
113
+ if (!Array.isArray(input.profiles)) {
114
+ throw new NotifierError("Invalid Notifier config: `profiles` must be an array.");
115
+ }
116
+ const profiles = input.profiles.map((profile, index) => normalizeSingleProfile(profile, `profiles[${index}]`));
117
+ const names = new Set();
118
+ for (const profile of profiles) {
119
+ if (names.has(profile.name)) {
120
+ throw new NotifierError(`Invalid Notifier config: duplicate profile name "${profile.name}".`);
121
+ }
122
+ names.add(profile.name);
123
+ }
124
+ return {
125
+ profiles,
126
+ defaultProfile: typeof input.defaultProfile === "string" ? input.defaultProfile : null,
127
+ };
128
+ }
129
+ function normalizeRecordProfile(profileName, rawProfile, sourceLabel) {
130
+ if (!rawProfile || typeof rawProfile !== "object") {
131
+ throw new NotifierError(`Invalid profile at ${sourceLabel}: expected an object.`);
132
+ }
133
+ return {
134
+ type: "telegram",
135
+ name: profileName,
136
+ botToken: requireNonEmptyString(rawProfile.botToken, `${sourceLabel}.botToken`),
137
+ chatId: requireNonEmptyString(rawProfile.chatId, `${sourceLabel}.chatId`),
138
+ };
139
+ }
140
+ function normalizeSingleProfile(rawProfile, sourceLabel) {
141
+ if (!rawProfile || typeof rawProfile !== "object") {
142
+ throw new NotifierError(`Invalid profile at ${sourceLabel}: expected an object.`);
143
+ }
144
+ const profile = rawProfile;
145
+ if (profile.type !== undefined && profile.type !== "telegram") {
146
+ throw new NotifierError(`Invalid profile at ${sourceLabel}.type: only "telegram" is supported.`);
147
+ }
148
+ return {
149
+ type: "telegram",
150
+ name: requireNonEmptyString(profile.name, `${sourceLabel}.name`),
151
+ botToken: requireNonEmptyString(profile.botToken, `${sourceLabel}.botToken`),
152
+ chatId: requireNonEmptyString(profile.chatId, `${sourceLabel}.chatId`),
153
+ };
154
+ }
155
+ function requireNonEmptyString(value, label) {
156
+ if (typeof value !== "string" || value.trim() === "") {
157
+ throw new NotifierError(`Invalid value for ${label}: expected non-empty string.`);
158
+ }
159
+ return value.trim();
160
+ }
161
+ function isConfigLoader(value) {
162
+ return (typeof value === "object" &&
163
+ value !== null &&
164
+ "load" in value &&
165
+ typeof value.load === "function");
166
+ }
167
+ function isNodeError(error) {
168
+ return typeof error === "object" && error !== null && "code" in error;
169
+ }
@@ -0,0 +1,11 @@
1
+ export declare function askRequired(question: string): Promise<string>;
2
+ export declare function askRequiredWithInitial(question: string, initialValue?: string): Promise<string>;
3
+ export declare function askConfirm(question: string, initialValue?: boolean): Promise<boolean>;
4
+ type SelectOption = {
5
+ value: string;
6
+ label: string;
7
+ hint?: string;
8
+ disabled?: boolean;
9
+ };
10
+ export declare function askSelect(question: string, options: SelectOption[]): Promise<string>;
11
+ export {};
@@ -0,0 +1,7 @@
1
+ export type TelegramConnection = {
2
+ chatId: string;
3
+ username: string | null;
4
+ };
5
+ export declare function getLatestUpdateOffset(botToken: string): Promise<number>;
6
+ export declare function waitForChatId(botToken: string, offset: number, timeoutSeconds?: number): Promise<TelegramConnection>;
7
+ export declare function sendTelegramMessage(botToken: string, chatId: string, text: string): Promise<void>;
package/package.json CHANGED
@@ -1,8 +1,17 @@
1
1
  {
2
2
  "name": "nonotify",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "nnt CLI for Telegram notifications",
5
5
  "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ },
13
+ "./package.json": "./package.json"
14
+ },
6
15
  "bin": {
7
16
  "nnt": "./dist/cli.js"
8
17
  },
package/src/cli.ts CHANGED
@@ -14,6 +14,7 @@ import {
14
14
  sendTelegramMessage,
15
15
  waitForChatId,
16
16
  } from "./telegram.js";
17
+ import { Notifier } from "./notifier.js";
17
18
 
18
19
  const profileCli = Cli.create("profile", {
19
20
  description: "Manage notification profiles",
@@ -442,32 +443,12 @@ const cli = Cli.create("nnt", {
442
443
  profile: "p",
443
444
  },
444
445
  async run(c) {
445
- const config = await loadConfig();
446
- const profileName = c.options.profile ?? config.defaultProfile;
446
+ const notifier = new Notifier();
447
447
 
448
- if (!profileName) {
449
- throw new Error(
450
- "No default profile found. Run `nnt profile add` first.",
451
- );
452
- }
453
-
454
- const profile = config.profiles[profileName];
455
-
456
- if (!profile) {
457
- throw new Error(`Profile "${profileName}" not found.`);
458
- }
459
-
460
- await sendTelegramMessage(
461
- profile.botToken,
462
- profile.chatId,
463
- c.args.message,
464
- );
465
-
466
- return {
467
- sent: true,
468
- profile: profileName,
469
- provider: profile.type,
470
- };
448
+ return notifier.send({
449
+ message: c.args.message,
450
+ profile: c.options.profile,
451
+ });
471
452
  },
472
453
  })
473
454
  .command(profileCli);
package/src/index.ts ADDED
@@ -0,0 +1,15 @@
1
+ export {
2
+ EnvConfigLoader,
3
+ NoProfilesConfiguredError,
4
+ Notifier,
5
+ NotifierError,
6
+ ProfileNotFoundError,
7
+ } from "./notifier.js";
8
+ export type {
9
+ NotifierConfig,
10
+ NotifierConfigLoader,
11
+ NotifierProfile,
12
+ NotifierProfileInput,
13
+ SendInput,
14
+ SendResult,
15
+ } from "./notifier.js";
@@ -0,0 +1,323 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { getConfigPath } from "./config.js";
3
+ import { sendTelegramMessage } from "./telegram.js";
4
+
5
+ export type NotifierProfile = {
6
+ type: "telegram";
7
+ name: string;
8
+ botToken: string;
9
+ chatId: string;
10
+ };
11
+
12
+ export type NotifierProfileInput = {
13
+ type?: "telegram";
14
+ name: string;
15
+ botToken: string;
16
+ chatId: string;
17
+ };
18
+
19
+ export type NotifierConfig = {
20
+ profiles: NotifierProfileInput[];
21
+ defaultProfile?: string | null;
22
+ };
23
+
24
+ export interface NotifierConfigLoader {
25
+ load(): NotifierConfig;
26
+ }
27
+
28
+ export type SendInput = {
29
+ message: string;
30
+ profile?: string;
31
+ };
32
+
33
+ export type SendResult = {
34
+ sent: true;
35
+ profile: string;
36
+ provider: "telegram";
37
+ };
38
+
39
+ export class NotifierError extends Error {
40
+ constructor(message: string) {
41
+ super(message);
42
+ this.name = "NotifierError";
43
+ }
44
+ }
45
+
46
+ export class ProfileNotFoundError extends NotifierError {
47
+ readonly profile: string;
48
+
49
+ constructor(profile: string) {
50
+ super(`Profile "${profile}" not found.`);
51
+ this.name = "ProfileNotFoundError";
52
+ this.profile = profile;
53
+ }
54
+ }
55
+
56
+ export class NoProfilesConfiguredError extends NotifierError {
57
+ constructor() {
58
+ super("No profiles configured. Run `nnt profile add` first.");
59
+ this.name = "NoProfilesConfiguredError";
60
+ }
61
+ }
62
+
63
+ type RawEnvConfig = {
64
+ defaultProfile?: unknown;
65
+ profiles?: unknown;
66
+ };
67
+
68
+ type RawRecordProfile = {
69
+ type?: unknown;
70
+ name?: unknown;
71
+ botToken?: unknown;
72
+ chatId?: unknown;
73
+ };
74
+
75
+ export class EnvConfigLoader implements NotifierConfigLoader {
76
+ load(): NotifierConfig {
77
+ const configPath = getConfigPath();
78
+
79
+ let rawConfig: RawEnvConfig;
80
+
81
+ try {
82
+ const raw = readFileSync(configPath, "utf8");
83
+ rawConfig = JSON.parse(raw) as RawEnvConfig;
84
+ } catch (error) {
85
+ if (isNodeError(error) && error.code === "ENOENT") {
86
+ return {
87
+ profiles: [],
88
+ defaultProfile: null,
89
+ };
90
+ }
91
+
92
+ if (error instanceof SyntaxError) {
93
+ throw new NotifierError(
94
+ `Invalid JSON in config file at ${configPath}: ${error.message}`,
95
+ );
96
+ }
97
+
98
+ throw error;
99
+ }
100
+
101
+ return normalizeEnvConfig(rawConfig);
102
+ }
103
+ }
104
+
105
+ export class Notifier {
106
+ readonly profiles: readonly Readonly<NotifierProfile>[];
107
+ private readonly defaultProfile: string | null;
108
+ private readonly profilesByName: ReadonlyMap<
109
+ string,
110
+ Readonly<NotifierProfile>
111
+ >;
112
+
113
+ constructor(
114
+ source: NotifierConfig | NotifierConfigLoader = new EnvConfigLoader(),
115
+ ) {
116
+ const config = isConfigLoader(source) ? source.load() : source;
117
+ const normalized = normalizeNotifierConfig(config);
118
+ const frozenProfiles = normalized.profiles.map((profile) =>
119
+ Object.freeze({ ...profile }),
120
+ );
121
+
122
+ this.profiles = Object.freeze(frozenProfiles);
123
+ this.defaultProfile = normalized.defaultProfile;
124
+ this.profilesByName = new Map(
125
+ frozenProfiles.map((profile) => [profile.name, profile]),
126
+ );
127
+ }
128
+
129
+ async send(input: SendInput): Promise<SendResult> {
130
+ const message = input.message.trim();
131
+
132
+ if (message === "") {
133
+ throw new NotifierError("Message cannot be empty.");
134
+ }
135
+
136
+ const selectedProfile = this.resolveProfile(input.profile);
137
+
138
+ await sendTelegramMessage(
139
+ selectedProfile.botToken,
140
+ selectedProfile.chatId,
141
+ message,
142
+ );
143
+
144
+ return {
145
+ sent: true,
146
+ profile: selectedProfile.name,
147
+ provider: selectedProfile.type,
148
+ };
149
+ }
150
+
151
+ private resolveProfile(profileName?: string): Readonly<NotifierProfile> {
152
+ if (profileName) {
153
+ const profile = this.profilesByName.get(profileName);
154
+
155
+ if (!profile) {
156
+ throw new ProfileNotFoundError(profileName);
157
+ }
158
+
159
+ return profile;
160
+ }
161
+
162
+ if (this.defaultProfile) {
163
+ const defaultProfile = this.profilesByName.get(this.defaultProfile);
164
+ if (defaultProfile) {
165
+ return defaultProfile;
166
+ }
167
+ }
168
+
169
+ const firstProfile = this.profiles[0];
170
+
171
+ if (!firstProfile) {
172
+ throw new NoProfilesConfiguredError();
173
+ }
174
+
175
+ return firstProfile;
176
+ }
177
+ }
178
+
179
+ function normalizeEnvConfig(raw: RawEnvConfig): NotifierConfig {
180
+ return {
181
+ defaultProfile:
182
+ typeof raw.defaultProfile === "string" ? raw.defaultProfile : null,
183
+ profiles: normalizeProfilesFromUnknown(raw.profiles),
184
+ };
185
+ }
186
+
187
+ function normalizeProfilesFromUnknown(rawProfiles: unknown): NotifierProfile[] {
188
+ if (!rawProfiles) {
189
+ return [];
190
+ }
191
+
192
+ if (Array.isArray(rawProfiles)) {
193
+ return rawProfiles.map((profile, index) =>
194
+ normalizeSingleProfile(profile, `profiles[${index}]`),
195
+ );
196
+ }
197
+
198
+ if (typeof rawProfiles === "object") {
199
+ const entries = Object.entries(
200
+ rawProfiles as Record<string, RawRecordProfile>,
201
+ );
202
+
203
+ return entries.map(([name, profile]) =>
204
+ normalizeRecordProfile(name, profile, `profiles.${name}`),
205
+ );
206
+ }
207
+
208
+ throw new NotifierError(
209
+ "Invalid config format: `profiles` must be an array or object.",
210
+ );
211
+ }
212
+
213
+ function normalizeNotifierConfig(input: NotifierConfig): {
214
+ profiles: NotifierProfile[];
215
+ defaultProfile: string | null;
216
+ } {
217
+ if (!Array.isArray(input.profiles)) {
218
+ throw new NotifierError(
219
+ "Invalid Notifier config: `profiles` must be an array.",
220
+ );
221
+ }
222
+
223
+ const profiles = input.profiles.map((profile, index) =>
224
+ normalizeSingleProfile(profile, `profiles[${index}]`),
225
+ );
226
+
227
+ const names = new Set<string>();
228
+
229
+ for (const profile of profiles) {
230
+ if (names.has(profile.name)) {
231
+ throw new NotifierError(
232
+ `Invalid Notifier config: duplicate profile name "${profile.name}".`,
233
+ );
234
+ }
235
+
236
+ names.add(profile.name);
237
+ }
238
+
239
+ return {
240
+ profiles,
241
+ defaultProfile:
242
+ typeof input.defaultProfile === "string" ? input.defaultProfile : null,
243
+ };
244
+ }
245
+
246
+ function normalizeRecordProfile(
247
+ profileName: string,
248
+ rawProfile: RawRecordProfile,
249
+ sourceLabel: string,
250
+ ): NotifierProfile {
251
+ if (!rawProfile || typeof rawProfile !== "object") {
252
+ throw new NotifierError(
253
+ `Invalid profile at ${sourceLabel}: expected an object.`,
254
+ );
255
+ }
256
+
257
+ return {
258
+ type: "telegram",
259
+ name: profileName,
260
+ botToken: requireNonEmptyString(
261
+ rawProfile.botToken,
262
+ `${sourceLabel}.botToken`,
263
+ ),
264
+ chatId: requireNonEmptyString(rawProfile.chatId, `${sourceLabel}.chatId`),
265
+ };
266
+ }
267
+
268
+ function normalizeSingleProfile(
269
+ rawProfile: unknown,
270
+ sourceLabel: string,
271
+ ): NotifierProfile {
272
+ if (!rawProfile || typeof rawProfile !== "object") {
273
+ throw new NotifierError(
274
+ `Invalid profile at ${sourceLabel}: expected an object.`,
275
+ );
276
+ }
277
+
278
+ const profile = rawProfile as {
279
+ type?: unknown;
280
+ name?: unknown;
281
+ botToken?: unknown;
282
+ chatId?: unknown;
283
+ };
284
+
285
+ if (profile.type !== undefined && profile.type !== "telegram") {
286
+ throw new NotifierError(
287
+ `Invalid profile at ${sourceLabel}.type: only "telegram" is supported.`,
288
+ );
289
+ }
290
+
291
+ return {
292
+ type: "telegram",
293
+ name: requireNonEmptyString(profile.name, `${sourceLabel}.name`),
294
+ botToken: requireNonEmptyString(
295
+ profile.botToken,
296
+ `${sourceLabel}.botToken`,
297
+ ),
298
+ chatId: requireNonEmptyString(profile.chatId, `${sourceLabel}.chatId`),
299
+ };
300
+ }
301
+
302
+ function requireNonEmptyString(value: unknown, label: string): string {
303
+ if (typeof value !== "string" || value.trim() === "") {
304
+ throw new NotifierError(
305
+ `Invalid value for ${label}: expected non-empty string.`,
306
+ );
307
+ }
308
+
309
+ return value.trim();
310
+ }
311
+
312
+ function isConfigLoader(value: unknown): value is NotifierConfigLoader {
313
+ return (
314
+ typeof value === "object" &&
315
+ value !== null &&
316
+ "load" in value &&
317
+ typeof (value as { load: unknown }).load === "function"
318
+ );
319
+ }
320
+
321
+ function isNodeError(error: unknown): error is NodeJS.ErrnoException {
322
+ return typeof error === "object" && error !== null && "code" in error;
323
+ }
package/tsconfig.json CHANGED
@@ -8,6 +8,7 @@
8
8
  "resolveJsonModule": true,
9
9
  "esModuleInterop": true,
10
10
  "forceConsistentCasingInFileNames": true,
11
+ "declaration": true,
11
12
  "outDir": "dist",
12
13
  "rootDir": "src"
13
14
  },