@assemora/notifications 0.1.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.
package/dist/jobs.js ADDED
@@ -0,0 +1,59 @@
1
+ /**
2
+ * The sending itself (SPEC.md §82).
3
+ *
4
+ * One job per delivery, dispatched by `notifications.send` and handed to the queue
5
+ * only once the outermost transaction commits (ADR-0023) — so a rollback tells nobody,
6
+ * and the network call happens after the request that caused it has answered.
7
+ *
8
+ * The handler sends and then executes a command to write down what happened, rather
9
+ * than writing the row itself: a job is not a mutation, and the rows it changes go
10
+ * through the Command Bus like every other row (SPEC.md §14). It executes it by name
11
+ * so that this file does not import the file that dispatches this job.
12
+ */
13
+ import { job } from '@assemora/core';
14
+ import { uuid } from '@assemora/schema';
15
+ import { channelNamed, isRejection } from './channel.js';
16
+ import { NotificationDelivery } from './models.js';
17
+ export const DeliverNotification = job('notifications.deliver', {
18
+ description: 'Sends one notification over its channel and records what came back',
19
+ input: { deliveryId: uuid() },
20
+ handle: async ({ deliveryId }, context) => {
21
+ const delivery = await NotificationDelivery.find(deliveryId);
22
+ // The row is gone, or somebody has already sent it. A queue delivers at least
23
+ // once, so a worker killed between the send and the record hands this to another
24
+ // worker — and the second one must not send the kitchen a duplicate.
25
+ if (delivery === null || delivery.status === 'sent')
26
+ return;
27
+ const channel = channelNamed(delivery.channel);
28
+ if (channel === undefined) {
29
+ await context.commands.execute('notifications.record', {
30
+ deliveryId,
31
+ outcome: 'failed',
32
+ error: `No channel named "${delivery.channel}" is configured`,
33
+ });
34
+ return;
35
+ }
36
+ try {
37
+ await channel.send(delivery.address, { text: delivery.body });
38
+ }
39
+ catch (error) {
40
+ const reason = error instanceof Error ? error.message : String(error);
41
+ await context.commands.execute('notifications.record', {
42
+ deliveryId,
43
+ outcome: 'failed',
44
+ error: reason,
45
+ });
46
+ // A rejection is the channel's final answer — a chat that does not exist will
47
+ // not start existing on the third attempt, and a queue retrying it spends the
48
+ // afternoon on a typo. Everything else is a moment that has passed by the time
49
+ // the queue tries again, so it is rethrown and the adapter decides when
50
+ // (ADR-0023).
51
+ if (isRejection(error))
52
+ return;
53
+ throw error;
54
+ }
55
+ await context.commands.execute('notifications.record', { deliveryId, outcome: 'sent' });
56
+ },
57
+ });
58
+ export const notificationJobs = [DeliverNotification];
59
+ //# sourceMappingURL=jobs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jobs.js","sourceRoot":"","sources":["../src/jobs.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,EAAE,GAAG,EAAE,MAAM,gBAAgB,CAAA;AACpC,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAA;AAEvC,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AACxD,OAAO,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAA;AAElD,MAAM,CAAC,MAAM,mBAAmB,GAAG,GAAG,CAAC,uBAAuB,EAAE;IAC9D,WAAW,EAAE,oEAAoE;IACjF,KAAK,EAAE,EAAE,UAAU,EAAE,IAAI,EAAE,EAAE;IAC7B,MAAM,EAAE,KAAK,EAAE,EAAE,UAAU,EAAE,EAAE,OAAO,EAAE,EAAE;QACxC,MAAM,QAAQ,GAAG,MAAM,oBAAoB,CAAC,IAAI,CAAC,UAAU,CAAC,CAAA;QAE5D,8EAA8E;QAC9E,iFAAiF;QACjF,qEAAqE;QACrE,IAAI,QAAQ,KAAK,IAAI,IAAI,QAAQ,CAAC,MAAM,KAAK,MAAM;YAAE,OAAM;QAE3D,MAAM,OAAO,GAAG,YAAY,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAA;QAE9C,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;YAC1B,MAAM,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC,sBAAsB,EAAE;gBACrD,UAAU;gBACV,OAAO,EAAE,QAAQ;gBACjB,KAAK,EAAE,qBAAqB,QAAQ,CAAC,OAAO,iBAAiB;aAC9D,CAAC,CAAA;YAEF,OAAM;QACR,CAAC;QAED,IAAI,CAAC;YACH,MAAM,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,EAAE,EAAE,IAAI,EAAE,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAA;QAC/D,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,MAAM,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;YAErE,MAAM,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC,sBAAsB,EAAE;gBACrD,UAAU;gBACV,OAAO,EAAE,QAAQ;gBACjB,KAAK,EAAE,MAAM;aACd,CAAC,CAAA;YAEF,8EAA8E;YAC9E,8EAA8E;YAC9E,+EAA+E;YAC/E,wEAAwE;YACxE,cAAc;YACd,IAAI,WAAW,CAAC,KAAK,CAAC;gBAAE,OAAM;YAE9B,MAAM,KAAK,CAAA;QACb,CAAC;QAED,MAAM,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC,sBAAsB,EAAE,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,CAAA;IACzF,CAAC;CACF,CAAC,CAAA;AAEF,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,mBAAmB,CAAU,CAAA"}
@@ -0,0 +1,91 @@
1
+ /** What a delivery can be. `pending` is a row nobody has tried to send yet. */
2
+ export declare const DELIVERY_STATUSES: readonly ['pending', 'sent', 'failed'];
3
+ export type DeliveryStatus = (typeof DELIVERY_STATUSES)[number];
4
+ export declare const NotificationRecipient: import("@assemora/data").Model<{
5
+ id: import("@assemora/data").UuidColumnBuilder;
6
+ /** What a person calls this address: "Kitchen", "Ada's phone". */
7
+ label: import("@assemora/data").ColumnBuilder<string>;
8
+ /** The channel's own name, as the driver registered it. */
9
+ channel: import("@assemora/data").ColumnBuilder<string>;
10
+ /** A Telegram chat id today; an address, a number or a URL when a channel grows. */
11
+ address: import("@assemora/data").ColumnBuilder<string>;
12
+ /**
13
+ * The topics this recipient asked for. Empty means all of them.
14
+ *
15
+ * Empty-is-everything because the first recipient anybody adds is the staff chat
16
+ * that wants the lot, and making them tick every box would mean a new topic is one
17
+ * nobody is told about until somebody remembers to go back and tick it.
18
+ */
19
+ topics: import("@assemora/data").JsonColumnBuilder<string[]>;
20
+ /**
21
+ * Off rather than deleted, so a chat can be silenced for an evening without losing
22
+ * what it is and which topics it had.
23
+ */
24
+ active: import("@assemora/data").ColumnBuilder<boolean>;
25
+ createdAt: import("@assemora/data").TimestampColumnBuilder;
26
+ updatedAt: import("@assemora/data").TimestampColumnBuilder;
27
+ }, never, Readonly<Record<never, never>>>;
28
+ export declare const NotificationDelivery: import("@assemora/data").Model<{
29
+ id: import("@assemora/data").UuidColumnBuilder;
30
+ topic: import("@assemora/data").ColumnBuilder<string>;
31
+ channel: import("@assemora/data").ColumnBuilder<string>;
32
+ address: import("@assemora/data").ColumnBuilder<string>;
33
+ /**
34
+ * Null when the recipient row has been deleted since. The delivery is history and
35
+ * outlives the address book.
36
+ */
37
+ recipientId: import("@assemora/data").ColumnBuilder<string | null>;
38
+ /** What was rendered, so the log shows what was actually said, not what it would say now. */
39
+ body: import("@assemora/data").ColumnBuilder<string>;
40
+ status: import("@assemora/data").ColumnBuilder<"failed" | "pending" | "sent">;
41
+ attempts: import("@assemora/data").ColumnBuilder<number>;
42
+ /** The channel's refusal, in its own words. Null while nothing has gone wrong. */
43
+ error: import("@assemora/data").ColumnBuilder<string | null>;
44
+ sentAt: import("@assemora/data").ColumnBuilder<Date | null>;
45
+ createdAt: import("@assemora/data").TimestampColumnBuilder;
46
+ updatedAt: import("@assemora/data").TimestampColumnBuilder;
47
+ }, never, Readonly<Record<never, never>>>;
48
+ export declare const notificationModels: readonly [import("@assemora/data").Model<{
49
+ id: import("@assemora/data").UuidColumnBuilder;
50
+ /** What a person calls this address: "Kitchen", "Ada's phone". */
51
+ label: import("@assemora/data").ColumnBuilder<string>;
52
+ /** The channel's own name, as the driver registered it. */
53
+ channel: import("@assemora/data").ColumnBuilder<string>;
54
+ /** A Telegram chat id today; an address, a number or a URL when a channel grows. */
55
+ address: import("@assemora/data").ColumnBuilder<string>;
56
+ /**
57
+ * The topics this recipient asked for. Empty means all of them.
58
+ *
59
+ * Empty-is-everything because the first recipient anybody adds is the staff chat
60
+ * that wants the lot, and making them tick every box would mean a new topic is one
61
+ * nobody is told about until somebody remembers to go back and tick it.
62
+ */
63
+ topics: import("@assemora/data").JsonColumnBuilder<string[]>;
64
+ /**
65
+ * Off rather than deleted, so a chat can be silenced for an evening without losing
66
+ * what it is and which topics it had.
67
+ */
68
+ active: import("@assemora/data").ColumnBuilder<boolean>;
69
+ createdAt: import("@assemora/data").TimestampColumnBuilder;
70
+ updatedAt: import("@assemora/data").TimestampColumnBuilder;
71
+ }, never, Readonly<Record<never, never>>>, import("@assemora/data").Model<{
72
+ id: import("@assemora/data").UuidColumnBuilder;
73
+ topic: import("@assemora/data").ColumnBuilder<string>;
74
+ channel: import("@assemora/data").ColumnBuilder<string>;
75
+ address: import("@assemora/data").ColumnBuilder<string>;
76
+ /**
77
+ * Null when the recipient row has been deleted since. The delivery is history and
78
+ * outlives the address book.
79
+ */
80
+ recipientId: import("@assemora/data").ColumnBuilder<string | null>;
81
+ /** What was rendered, so the log shows what was actually said, not what it would say now. */
82
+ body: import("@assemora/data").ColumnBuilder<string>;
83
+ status: import("@assemora/data").ColumnBuilder<"failed" | "pending" | "sent">;
84
+ attempts: import("@assemora/data").ColumnBuilder<number>;
85
+ /** The channel's refusal, in its own words. Null while nothing has gone wrong. */
86
+ error: import("@assemora/data").ColumnBuilder<string | null>;
87
+ sentAt: import("@assemora/data").ColumnBuilder<Date | null>;
88
+ createdAt: import("@assemora/data").TimestampColumnBuilder;
89
+ updatedAt: import("@assemora/data").TimestampColumnBuilder;
90
+ }, never, Readonly<Record<never, never>>>];
91
+ //# sourceMappingURL=models.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"models.d.ts","sourceRoot":"","sources":["../src/models.ts"],"names":[],"mappings":"AAsBA,+EAA+E;AAC/E,eAAO,MAAM,iBAAiB,YAAI,SAAS,EAAE,MAAM,EAAE,QAAQ,CAAU,CAAA;AAEvE,MAAM,MAAM,cAAc,GAAG,CAAC,OAAO,iBAAiB,CAAC,CAAC,MAAM,CAAC,CAAA;AAE/D,eAAO,MAAM,qBAAqB;;IAEhC,kEAAkE;;IAElE,2DAA2D;;IAE3D,oFAAoF;;IAEpF;;;;;;OAMG;;IAEH;;;OAGG;;;;yCAIH,CAAA;AAEF,eAAO,MAAM,oBAAoB;;;;;IAK/B;;;OAGG;;IAEH,6FAA6F;;;;IAM7F,kFAAkF;;;;;yCAKlF,CAAA;AAEF,eAAO,MAAM,kBAAkB;;IA9C7B,kEAAkE;;IAElE,2DAA2D;;IAE3D,oFAAoF;;IAEpF;;;;;;OAMG;;IAEH;;;OAGG;;;;;;;;;IAWH;;;OAGG;;IAEH,6FAA6F;;;;IAM7F,kFAAkF;;;;;0CAOI,CAAA"}
package/dist/models.js ADDED
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Who is told, and what was told to them (SPEC.md §81).
3
+ *
4
+ * Two tables and no more. A recipient is an address somebody put in Studio; a
5
+ * delivery is one attempt to reach one address with one message, kept whether it
6
+ * worked or not — a notification that quietly did not arrive is the failure mode of
7
+ * every notification system, and a row that says `failed` with the channel's own
8
+ * words in it is the whole difference between "the kitchen was not told" and "nobody
9
+ * knows whether the kitchen was told".
10
+ */
11
+ import { boolean, enumOf, integer, json, model, string, text, timestamp, uuid, } from '@assemora/data';
12
+ /** What a delivery can be. `pending` is a row nobody has tried to send yet. */
13
+ export const DELIVERY_STATUSES = ['pending', 'sent', 'failed'];
14
+ export const NotificationRecipient = model('assemora_notification_recipients', {
15
+ id: uuid().primary().defaultRandom(),
16
+ /** What a person calls this address: "Kitchen", "Ada's phone". */
17
+ label: string(),
18
+ /** The channel's own name, as the driver registered it. */
19
+ channel: string(),
20
+ /** A Telegram chat id today; an address, a number or a URL when a channel grows. */
21
+ address: string(),
22
+ /**
23
+ * The topics this recipient asked for. Empty means all of them.
24
+ *
25
+ * Empty-is-everything because the first recipient anybody adds is the staff chat
26
+ * that wants the lot, and making them tick every box would mean a new topic is one
27
+ * nobody is told about until somebody remembers to go back and tick it.
28
+ */
29
+ topics: json().default([]),
30
+ /**
31
+ * Off rather than deleted, so a chat can be silenced for an evening without losing
32
+ * what it is and which topics it had.
33
+ */
34
+ active: boolean().default(true),
35
+ createdAt: timestamp().created(),
36
+ updatedAt: timestamp().updated(),
37
+ });
38
+ export const NotificationDelivery = model('assemora_notification_deliveries', {
39
+ id: uuid().primary().defaultRandom(),
40
+ topic: string().index(),
41
+ channel: string(),
42
+ address: string(),
43
+ /**
44
+ * Null when the recipient row has been deleted since. The delivery is history and
45
+ * outlives the address book.
46
+ */
47
+ recipientId: uuid().nullable(),
48
+ /** What was rendered, so the log shows what was actually said, not what it would say now. */
49
+ body: text(),
50
+ status: enumOf(...DELIVERY_STATUSES)
51
+ .default('pending')
52
+ .index(),
53
+ attempts: integer().default(0),
54
+ /** The channel's refusal, in its own words. Null while nothing has gone wrong. */
55
+ error: text().nullable(),
56
+ sentAt: timestamp().nullable(),
57
+ createdAt: timestamp().created(),
58
+ updatedAt: timestamp().updated(),
59
+ });
60
+ export const notificationModels = [NotificationRecipient, NotificationDelivery];
61
+ //# sourceMappingURL=models.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"models.js","sourceRoot":"","sources":["../src/models.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EACL,OAAO,EACP,MAAM,EACN,OAAO,EACP,IAAI,EACJ,KAAK,EACL,MAAM,EACN,IAAI,EACJ,SAAS,EACT,IAAI,GACL,MAAM,gBAAgB,CAAA;AAEvB,+EAA+E;AAC/E,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,CAAU,CAAA;AAIvE,MAAM,CAAC,MAAM,qBAAqB,GAAG,KAAK,CAAC,kCAAkC,EAAE;IAC7E,EAAE,EAAE,IAAI,EAAE,CAAC,OAAO,EAAE,CAAC,aAAa,EAAE;IACpC,kEAAkE;IAClE,KAAK,EAAE,MAAM,EAAE;IACf,2DAA2D;IAC3D,OAAO,EAAE,MAAM,EAAE;IACjB,oFAAoF;IACpF,OAAO,EAAE,MAAM,EAAE;IACjB;;;;;;OAMG;IACH,MAAM,EAAE,IAAI,EAAY,CAAC,OAAO,CAAC,EAAE,CAAC;IACpC;;;OAGG;IACH,MAAM,EAAE,OAAO,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC;IAC/B,SAAS,EAAE,SAAS,EAAE,CAAC,OAAO,EAAE;IAChC,SAAS,EAAE,SAAS,EAAE,CAAC,OAAO,EAAE;CACjC,CAAC,CAAA;AAEF,MAAM,CAAC,MAAM,oBAAoB,GAAG,KAAK,CAAC,kCAAkC,EAAE;IAC5E,EAAE,EAAE,IAAI,EAAE,CAAC,OAAO,EAAE,CAAC,aAAa,EAAE;IACpC,KAAK,EAAE,MAAM,EAAE,CAAC,KAAK,EAAE;IACvB,OAAO,EAAE,MAAM,EAAE;IACjB,OAAO,EAAE,MAAM,EAAE;IACjB;;;OAGG;IACH,WAAW,EAAE,IAAI,EAAE,CAAC,QAAQ,EAAE;IAC9B,6FAA6F;IAC7F,IAAI,EAAE,IAAI,EAAE;IACZ,MAAM,EAAE,MAAM,CAAC,GAAG,iBAAiB,CAAC;SACjC,OAAO,CAAC,SAAS,CAAC;SAClB,KAAK,EAAE;IACV,QAAQ,EAAE,OAAO,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC;IAC9B,kFAAkF;IAClF,KAAK,EAAE,IAAI,EAAE,CAAC,QAAQ,EAAE;IACxB,MAAM,EAAE,SAAS,EAAE,CAAC,QAAQ,EAAE;IAC9B,SAAS,EAAE,SAAS,EAAE,CAAC,OAAO,EAAE;IAChC,SAAS,EAAE,SAAS,EAAE,CAAC,OAAO,EAAE;CACjC,CAAC,CAAA;AAEF,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,qBAAqB,EAAE,oBAAoB,CAAU,CAAA"}
@@ -0,0 +1,31 @@
1
+ /**
2
+ * The `notifications()` module (SPEC.md §13, §81).
3
+ *
4
+ * ```ts
5
+ * assemora({
6
+ * modules: [
7
+ * notifications({
8
+ * channels: [telegram({ token: process.env.TELEGRAM_BOT_TOKEN ?? '' })],
9
+ * topics: [OrderPlaced],
10
+ * }),
11
+ * ],
12
+ * })
13
+ * ```
14
+ *
15
+ * The two lists are configuration and declaration: a channel is a driver this
16
+ * deployment was given, a topic is something this application can announce. Both are
17
+ * passed rather than discovered by import, for the reason `pages({ blocks })` takes
18
+ * its blocks that way — a registration that happens at import time is in the
19
+ * application whether the module was switched on or not.
20
+ */
21
+ import { type ModuleBuilder } from '@assemora/core';
22
+ import { type NotificationChannel } from './channel.js';
23
+ import { type AnyNotification } from './notification.js';
24
+ export type NotificationsOptions = {
25
+ /** The drivers this deployment can send over. None means nothing can be delivered. */
26
+ readonly channels?: readonly NotificationChannel[];
27
+ /** What this application can announce, declared with `notification()`. */
28
+ readonly topics?: readonly AnyNotification[];
29
+ };
30
+ export declare const notifications: (options?: NotificationsOptions) => ModuleBuilder;
31
+ //# sourceMappingURL=module.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"module.d.ts","sourceRoot":"","sources":["../src/module.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,OAAO,EAAE,KAAK,aAAa,EAAU,MAAM,gBAAgB,CAAA;AAE3D,OAAO,EAAE,KAAK,mBAAmB,EAAe,MAAM,cAAc,CAAA;AAIpE,OAAO,EAAE,KAAK,eAAe,EAAoB,MAAM,mBAAmB,CAAA;AAG1E,MAAM,MAAM,oBAAoB,GAAG;IACjC,sFAAsF;IACtF,QAAQ,CAAC,QAAQ,CAAC,EAAE,SAAS,mBAAmB,EAAE,CAAA;IAClD,0EAA0E;IAC1E,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,eAAe,EAAE,CAAA;CAC7C,CAAA;AAED,eAAO,MAAM,aAAa,aAAa,oBAAoB,KAAQ,aA0ClE,CAAA"}
package/dist/module.js ADDED
@@ -0,0 +1,63 @@
1
+ /**
2
+ * The `notifications()` module (SPEC.md §13, §81).
3
+ *
4
+ * ```ts
5
+ * assemora({
6
+ * modules: [
7
+ * notifications({
8
+ * channels: [telegram({ token: process.env.TELEGRAM_BOT_TOKEN ?? '' })],
9
+ * topics: [OrderPlaced],
10
+ * }),
11
+ * ],
12
+ * })
13
+ * ```
14
+ *
15
+ * The two lists are configuration and declaration: a channel is a driver this
16
+ * deployment was given, a topic is something this application can announce. Both are
17
+ * passed rather than discovered by import, for the reason `pages({ blocks })` takes
18
+ * its blocks that way — a registration that happens at import time is in the
19
+ * application whether the module was switched on or not.
20
+ */
21
+ import { module } from '@assemora/core';
22
+ import { useChannels } from './channel.js';
23
+ import { notificationCommands } from './commands.js';
24
+ import { notificationJobs } from './jobs.js';
25
+ import { notificationModels } from './models.js';
26
+ import { useNotifications } from './notification.js';
27
+ import { notificationResources } from './resources.js';
28
+ export const notifications = (options = {}) => {
29
+ const channels = options.channels ?? [];
30
+ const topics = options.topics ?? [];
31
+ const { recipients, deliveries } = notificationResources({
32
+ channels: channels.map((channel) => channel.name),
33
+ topics: topics.map((topic) => topic.topic),
34
+ });
35
+ return module('notifications')
36
+ .models(...notificationModels)
37
+ .resources(recipients, deliveries)
38
+ .commands(...notificationCommands)
39
+ .jobs(...notificationJobs)
40
+ .boot((context) => {
41
+ useChannels(channels);
42
+ useNotifications(topics);
43
+ for (const topic of topics) {
44
+ context.registry.register('notifications', {
45
+ name: topic.topic,
46
+ ...(topic.description === undefined ? {} : { description: topic.description }),
47
+ input: topic.input.toJsonSchema(),
48
+ module: context.module,
49
+ });
50
+ }
51
+ // Said rather than refused. An application configured with no channel still
52
+ // boots, still shows the address book and still records what it would have
53
+ // sent — which is what a project looks like the week before somebody creates
54
+ // the bot. Silence here is how it stays that way for a month.
55
+ if (channels.length === 0) {
56
+ context.logger.warn('The notifications module has no channel, so nothing can be delivered. Pass channels: [telegram({ token })].');
57
+ }
58
+ if (topics.length === 0) {
59
+ context.logger.warn('The notifications module declares no topics, so there is nothing to announce. Pass topics: [ ... ] built with notification().');
60
+ }
61
+ });
62
+ };
63
+ //# sourceMappingURL=module.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"module.js","sourceRoot":"","sources":["../src/module.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,OAAO,EAAsB,MAAM,EAAE,MAAM,gBAAgB,CAAA;AAE3D,OAAO,EAA4B,WAAW,EAAE,MAAM,cAAc,CAAA;AACpE,OAAO,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAA;AACpD,OAAO,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAA;AAC5C,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAA;AAChD,OAAO,EAAwB,gBAAgB,EAAE,MAAM,mBAAmB,CAAA;AAC1E,OAAO,EAAE,qBAAqB,EAAE,MAAM,gBAAgB,CAAA;AAStD,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,OAAO,GAAyB,EAAE,EAAiB,EAAE;IACjF,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,IAAI,EAAE,CAAA;IACvC,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,EAAE,CAAA;IACnC,MAAM,EAAE,UAAU,EAAE,UAAU,EAAE,GAAG,qBAAqB,CAAC;QACvD,QAAQ,EAAE,QAAQ,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC;QACjD,MAAM,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC;KAC3C,CAAC,CAAA;IAEF,OAAO,MAAM,CAAC,eAAe,CAAC;SAC3B,MAAM,CAAC,GAAG,kBAAkB,CAAC;SAC7B,SAAS,CAAC,UAAU,EAAE,UAAU,CAAC;SACjC,QAAQ,CAAC,GAAG,oBAAoB,CAAC;SACjC,IAAI,CAAC,GAAG,gBAAgB,CAAC;SACzB,IAAI,CAAC,CAAC,OAAO,EAAE,EAAE;QAChB,WAAW,CAAC,QAAQ,CAAC,CAAA;QACrB,gBAAgB,CAAC,MAAM,CAAC,CAAA;QAExB,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;YAC3B,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC,eAAe,EAAE;gBACzC,IAAI,EAAE,KAAK,CAAC,KAAK;gBACjB,GAAG,CAAC,KAAK,CAAC,WAAW,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,KAAK,CAAC,WAAW,EAAE,CAAC;gBAC9E,KAAK,EAAE,KAAK,CAAC,KAAK,CAAC,YAAY,EAAE;gBACjC,MAAM,EAAE,OAAO,CAAC,MAAM;aACvB,CAAC,CAAA;QACJ,CAAC;QAED,4EAA4E;QAC5E,2EAA2E;QAC3E,6EAA6E;QAC7E,8DAA8D;QAC9D,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC1B,OAAO,CAAC,MAAM,CAAC,IAAI,CACjB,6GAA6G,CAC9G,CAAA;QACH,CAAC;QAED,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACxB,OAAO,CAAC,MAAM,CAAC,IAAI,CACjB,+HAA+H,CAChI,CAAA;QACH,CAAC;IACH,CAAC,CAAC,CAAA;AACN,CAAC,CAAA"}
@@ -0,0 +1,69 @@
1
+ /**
2
+ * What an application can announce (SPEC.md §81).
3
+ *
4
+ * ```ts
5
+ * export const OrderPlaced = notification('orders.placed', {
6
+ * description: 'A new order is waiting for the kitchen',
7
+ * input: { code: string(), total: integer(), phone: string() },
8
+ * render: (order) => `Order ${order.code}\n${order.total} · ${order.phone}`,
9
+ * })
10
+ * ```
11
+ *
12
+ * The fourth member of the family beside `command()`, `query()` and `job()`, and
13
+ * declared the same way: a name, an input schema and a function. The schema is what
14
+ * makes `notifications.send` a typed act rather than a text box — the payload is
15
+ * validated before anybody is told anything, so a topic cannot be sent with a field
16
+ * missing and a chat cannot be reached with arbitrary prose.
17
+ *
18
+ * `render` is a function and therefore lives on the server only. The descriptor a
19
+ * generator reads carries the name, the description and the input schema, which is
20
+ * everything a form or an agent needs to *ask* for a notification, and nothing that
21
+ * would have to survive `JSON.stringify` (ADR-0027).
22
+ */
23
+ import type { InferShape, Schema, Shape } from '@assemora/schema';
24
+ import type { NotificationMessage } from './channel.js';
25
+ export type NotificationDefinition<S extends Shape> = {
26
+ readonly node: 'notification';
27
+ readonly topic: string;
28
+ readonly description: string | undefined;
29
+ readonly input: Schema<InferShape<S>>;
30
+ /** A string is the message; the object form is there for a channel that grows one. */
31
+ render(payload: InferShape<S>): NotificationMessage | string;
32
+ };
33
+ /** A notification of any shape, as the module stores it. */
34
+ export type AnyNotification = {
35
+ readonly node: 'notification';
36
+ readonly topic: string;
37
+ readonly description: string | undefined;
38
+ readonly input: Schema<unknown>;
39
+ render(payload: never): NotificationMessage | string;
40
+ };
41
+ /** How a topic describes itself in the Schema Registry (ADR-0002). */
42
+ export type NotificationDescriptor = {
43
+ readonly name: string;
44
+ readonly description?: string;
45
+ readonly input: ReturnType<Schema<unknown>['toJsonSchema']>;
46
+ readonly module?: string;
47
+ };
48
+ declare module '@assemora/core' {
49
+ interface RegistrySections {
50
+ notifications: NotificationDescriptor;
51
+ }
52
+ }
53
+ export declare const notification: <S extends Shape>(topic: string, definition: {
54
+ readonly input: S;
55
+ readonly description?: string;
56
+ render(payload: InferShape<S>): NotificationMessage | string;
57
+ }) => NotificationDefinition<S>;
58
+ export declare const useNotifications: (topics: readonly AnyNotification[]) => void;
59
+ export declare const notificationTopics: () => readonly string[];
60
+ export declare const notificationFor: (topic: string) => AnyNotification | undefined;
61
+ export declare const clearNotifications: () => void;
62
+ /**
63
+ * The text a recipient reads, from a payload that has already been validated.
64
+ *
65
+ * Trimmed, because a template literal that spans lines in a source file arrives with
66
+ * the indentation of the file it was written in.
67
+ */
68
+ export declare const renderMessage: (definition: AnyNotification, payload: unknown) => NotificationMessage;
69
+ //# sourceMappingURL=notification.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"notification.d.ts","sourceRoot":"","sources":["../src/notification.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAA;AAGjE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAA;AAEvD,MAAM,MAAM,sBAAsB,CAAC,CAAC,SAAS,KAAK,IAAI;IACpD,QAAQ,CAAC,IAAI,EAAE,cAAc,CAAA;IAC7B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,WAAW,EAAE,MAAM,GAAG,SAAS,CAAA;IACxC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAA;IACrC,sFAAsF;IACtF,MAAM,CAAC,OAAO,EAAE,UAAU,CAAC,CAAC,CAAC,GAAG,mBAAmB,GAAG,MAAM,CAAA;CAC7D,CAAA;AAED,4DAA4D;AAC5D,MAAM,MAAM,eAAe,GAAG;IAC5B,QAAQ,CAAC,IAAI,EAAE,cAAc,CAAA;IAC7B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,WAAW,EAAE,MAAM,GAAG,SAAS,CAAA;IACxC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,OAAO,CAAC,CAAA;IAC/B,MAAM,CAAC,OAAO,EAAE,KAAK,GAAG,mBAAmB,GAAG,MAAM,CAAA;CACrD,CAAA;AAED,sEAAsE;AACtE,MAAM,MAAM,sBAAsB,GAAG;IACnC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;IAC7B,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,cAAc,CAAC,CAAC,CAAA;IAC3D,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CACzB,CAAA;AAED,OAAO,QAAQ,gBAAgB,CAAC;IAC9B,UAAU,gBAAgB;QACxB,aAAa,EAAE,sBAAsB,CAAA;KACtC;CACF;AAED,eAAO,MAAM,YAAY,GAAI,CAAC,SAAS,KAAK,SACnC,MAAM,cACD;IACV,QAAQ,CAAC,KAAK,EAAE,CAAC,CAAA;IACjB,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;IAC7B,MAAM,CAAC,OAAO,EAAE,UAAU,CAAC,CAAC,CAAC,GAAG,mBAAmB,GAAG,MAAM,CAAA;CAC7D,KACA,sBAAsB,CAAC,CAAC,CAMzB,CAAA;AAYF,eAAO,MAAM,gBAAgB,WAAY,SAAS,eAAe,EAAE,KAAG,IAErE,CAAA;AAED,eAAO,MAAM,kBAAkB,QAAO,SAAS,MAAM,EAA0B,CAAA;AAE/E,eAAO,MAAM,eAAe,UAAW,MAAM,KAAG,eAAe,GAAG,SAAgC,CAAA;AAElG,eAAO,MAAM,kBAAkB,QAAO,IAErC,CAAA;AAED;;;;;GAKG;AACH,eAAO,MAAM,aAAa,eACZ,eAAe,WAClB,OAAO,KACf,mBAMF,CAAA"}
@@ -0,0 +1,36 @@
1
+ import { object } from '@assemora/schema';
2
+ export const notification = (topic, definition) => ({
3
+ node: 'notification',
4
+ topic,
5
+ description: definition.description,
6
+ input: object(definition.input),
7
+ render: definition.render,
8
+ });
9
+ /**
10
+ * The topics this application declared.
11
+ *
12
+ * Filled by `notifications({ topics })` rather than by importing a file, for the
13
+ * reason `pages({ blocks })` takes its blocks that way: a declaration that registers
14
+ * itself at import time is in the application whether the module was switched on or
15
+ * not, and the list is also what the recipient form's checkboxes are built from.
16
+ */
17
+ let declared = new Map();
18
+ export const useNotifications = (topics) => {
19
+ declared = new Map(topics.map((topic) => [topic.topic, topic]));
20
+ };
21
+ export const notificationTopics = () => [...declared.keys()];
22
+ export const notificationFor = (topic) => declared.get(topic);
23
+ export const clearNotifications = () => {
24
+ declared = new Map();
25
+ };
26
+ /**
27
+ * The text a recipient reads, from a payload that has already been validated.
28
+ *
29
+ * Trimmed, because a template literal that spans lines in a source file arrives with
30
+ * the indentation of the file it was written in.
31
+ */
32
+ export const renderMessage = (definition, payload) => {
33
+ const rendered = definition.render(payload);
34
+ return typeof rendered === 'string' ? { text: rendered.trim() } : { text: rendered.text.trim() };
35
+ };
36
+ //# sourceMappingURL=notification.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"notification.js","sourceRoot":"","sources":["../src/notification.ts"],"names":[],"mappings":"AAuBA,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAA;AAoCzC,MAAM,CAAC,MAAM,YAAY,GAAG,CAC1B,KAAa,EACb,UAIC,EAC0B,EAAE,CAAC,CAAC;IAC/B,IAAI,EAAE,cAAc;IACpB,KAAK;IACL,WAAW,EAAE,UAAU,CAAC,WAAW;IACnC,KAAK,EAAE,MAAM,CAAC,UAAU,CAAC,KAAK,CAAC;IAC/B,MAAM,EAAE,UAAU,CAAC,MAAM;CAC1B,CAAC,CAAA;AAEF;;;;;;;GAOG;AACH,IAAI,QAAQ,GAAyC,IAAI,GAAG,EAAE,CAAA;AAE9D,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,MAAkC,EAAQ,EAAE;IAC3E,QAAQ,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC,CAAA;AACjE,CAAC,CAAA;AAED,MAAM,CAAC,MAAM,kBAAkB,GAAG,GAAsB,EAAE,CAAC,CAAC,GAAG,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAA;AAE/E,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,KAAa,EAA+B,EAAE,CAAC,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,CAAA;AAElG,MAAM,CAAC,MAAM,kBAAkB,GAAG,GAAS,EAAE;IAC3C,QAAQ,GAAG,IAAI,GAAG,EAAE,CAAA;AACtB,CAAC,CAAA;AAED;;;;;GAKG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,CAC3B,UAA2B,EAC3B,OAAgB,EACK,EAAE;IACvB,MAAM,QAAQ,GAAI,UAAU,CAAC,MAA6D,CACxF,OAAO,CACR,CAAA;IAED,OAAO,OAAO,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,QAAQ,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,QAAQ,CAAC,IAAI,CAAC,IAAI,EAAE,EAAE,CAAA;AAClG,CAAC,CAAA"}
@@ -0,0 +1,49 @@
1
+ export declare const RECIPIENTS = "notificationRecipients";
2
+ export declare const DELIVERIES = "notificationDeliveries";
3
+ export type NotificationResourceOptions = {
4
+ readonly channels: readonly string[];
5
+ readonly topics: readonly string[];
6
+ };
7
+ export declare const notificationResources: (options: NotificationResourceOptions) => {
8
+ recipients: import("@assemora/resources").Resource<{
9
+ id: import("@assemora/data").UuidColumnBuilder;
10
+ label: import("@assemora/data").ColumnBuilder<string>;
11
+ channel: import("@assemora/data").ColumnBuilder<string>;
12
+ address: import("@assemora/data").ColumnBuilder<string>;
13
+ topics: import("@assemora/data").JsonColumnBuilder<string[]>;
14
+ active: import("@assemora/data").ColumnBuilder<boolean>;
15
+ createdAt: import("@assemora/data").TimestampColumnBuilder;
16
+ updatedAt: import("@assemora/data").TimestampColumnBuilder;
17
+ }, never, Readonly<Record<never, never>>, {
18
+ label: import("@assemora/resources").FieldBuilder<string>;
19
+ channel: import("@assemora/resources").FieldBuilder<string>;
20
+ address: import("@assemora/resources").FieldBuilder<string>;
21
+ topics: import("@assemora/resources").FieldBuilder<string[]>;
22
+ active: import("@assemora/resources").FieldBuilder<boolean>;
23
+ }>;
24
+ deliveries: import("@assemora/resources").Resource<{
25
+ id: import("@assemora/data").UuidColumnBuilder;
26
+ topic: import("@assemora/data").ColumnBuilder<string>;
27
+ channel: import("@assemora/data").ColumnBuilder<string>;
28
+ address: import("@assemora/data").ColumnBuilder<string>;
29
+ recipientId: import("@assemora/data").ColumnBuilder<string | null>;
30
+ body: import("@assemora/data").ColumnBuilder<string>;
31
+ status: import("@assemora/data").ColumnBuilder<"failed" | "pending" | "sent">;
32
+ attempts: import("@assemora/data").ColumnBuilder<number>;
33
+ error: import("@assemora/data").ColumnBuilder<string | null>;
34
+ sentAt: import("@assemora/data").ColumnBuilder<Date | null>;
35
+ createdAt: import("@assemora/data").TimestampColumnBuilder;
36
+ updatedAt: import("@assemora/data").TimestampColumnBuilder;
37
+ }, never, Readonly<Record<never, never>>, {
38
+ topic: import("@assemora/resources").FieldBuilder<string>;
39
+ channel: import("@assemora/resources").FieldBuilder<string>;
40
+ address: import("@assemora/resources").FieldBuilder<string>;
41
+ status: import("@assemora/resources").FieldBuilder<"failed" | "pending" | "sent">;
42
+ attempts: import("@assemora/resources").FieldBuilder<number>;
43
+ error: import("@assemora/resources").FieldBuilder<string>;
44
+ body: import("@assemora/resources").FieldBuilder<string>;
45
+ sentAt: import("@assemora/resources").FieldBuilder<Date>;
46
+ createdAt: import("@assemora/resources").FieldBuilder<Date>;
47
+ }>;
48
+ };
49
+ //# sourceMappingURL=resources.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resources.d.ts","sourceRoot":"","sources":["../src/resources.ts"],"names":[],"mappings":"AA0BA,eAAO,MAAM,UAAU,2BAA2B,CAAA;AAClD,eAAO,MAAM,UAAU,2BAA2B,CAAA;AASlD,MAAM,MAAM,2BAA2B,GAAG;IACxC,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAA;IACpC,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAA;CACnC,CAAA;AAED,eAAO,MAAM,qBAAqB,YAAa,2BAA2B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAsEzE,CAAA"}
@@ -0,0 +1,79 @@
1
+ /**
2
+ * The address book and the log, as Studio sees them (SPEC.md §35, §58).
3
+ *
4
+ * They are resources rather than screens, so the list, the form, the filters, the REST
5
+ * endpoints, the OpenAPI document, the SDK and the MCP tools all follow from these two
6
+ * declarations and nothing is written twice (ADR-0027).
7
+ *
8
+ * Both are built by a function rather than exported as constants, because two of the
9
+ * fields are choices and the choices are configuration: the channels are the drivers
10
+ * this application was given, and the topics are what it declared. A free-text
11
+ * "channel" column is a typo waiting to become a delivery nobody receives.
12
+ */
13
+ import { checkboxes, datetime, integer, json, resource, select, text, textarea, toggle, } from '@assemora/resources';
14
+ import { DELIVERY_STATUSES, NotificationDelivery, NotificationRecipient } from './models.js';
15
+ export const RECIPIENTS = 'notificationRecipients';
16
+ export const DELIVERIES = 'notificationDeliveries';
17
+ /** Studio's heading for both of them (SPEC.md §58). */
18
+ const GROUP = 'Notifications';
19
+ /** A non-empty tuple, or nothing — which is what `select()` and `checkboxes()` need. */
20
+ const choices = (values) => values.length === 0 ? undefined : values;
21
+ export const notificationResources = (options) => {
22
+ const channels = choices(options.channels);
23
+ const topics = choices(options.topics);
24
+ const recipients = resource(NotificationRecipient, {
25
+ label: text().required().searchable().sortable().label('Name'),
26
+ // A driver that is not configured cannot deliver, so the form offers the ones
27
+ // that are. With none configured the column is still a column, and a text box
28
+ // is a better answer than a form nobody can submit.
29
+ channel: channels === undefined
30
+ ? text().required().label('Channel')
31
+ : select(...channels)
32
+ .required()
33
+ .filterable()
34
+ .label('Channel'),
35
+ address: text().required().label('Address'),
36
+ // Nothing ticked means every topic, which is what the column says and what the
37
+ // hint has to repeat, because an empty box reads as "none" to everybody.
38
+ topics: topics === undefined
39
+ ? json().label('Topics')
40
+ : checkboxes(...topics).label('Topics'),
41
+ active: toggle().filterable().label('Active'),
42
+ }, {
43
+ name: RECIPIENTS,
44
+ label: 'Notification recipients',
45
+ group: GROUP,
46
+ icon: 'contact',
47
+ titleField: 'label',
48
+ defaultSort: 'label',
49
+ });
50
+ const deliveries = resource(NotificationDelivery, {
51
+ topic: text().searchable().filterable().sortable().label('Topic'),
52
+ channel: text().filterable().label('Channel'),
53
+ address: text().searchable().label('Address'),
54
+ status: select(...DELIVERY_STATUSES)
55
+ .filterable()
56
+ .sortable()
57
+ .label('Status'),
58
+ attempts: integer().sortable().label('Attempts'),
59
+ error: textarea().label('Error'),
60
+ body: textarea().label('Message'),
61
+ sentAt: datetime().sortable().label('Sent'),
62
+ createdAt: datetime().sortable().label('Queued'),
63
+ }, {
64
+ name: DELIVERIES,
65
+ label: 'Notification deliveries',
66
+ group: GROUP,
67
+ icon: 'bell',
68
+ titleField: 'topic',
69
+ defaultSort: '-createdAt',
70
+ perPage: 30,
71
+ // A log is written by the sending and read by everybody else. Editing one would
72
+ // be editing what happened, and creating one would be a delivery nobody sent —
73
+ // the flags are what make that true in Studio, over REST, in the SDK and over
74
+ // MCP at once (SPEC.md §43).
75
+ api: { create: false, update: false, delete: false },
76
+ });
77
+ return { recipients, deliveries };
78
+ };
79
+ //# sourceMappingURL=resources.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resources.js","sourceRoot":"","sources":["../src/resources.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,13 @@
1
+ import { type NotificationChannel } from './channel.js';
2
+ export type TelegramOptions = {
3
+ /** From @BotFather. A secret: it never reaches the registry, a log or an answer. */
4
+ readonly token: string;
5
+ /** The Bot API root. Overridden by tests and by a self-hosted Bot API server. */
6
+ readonly api?: string;
7
+ /** How long one send may take. A hung request holds the command that is sending. */
8
+ readonly timeoutMs?: number;
9
+ /** Injected by tests; production uses the platform's. */
10
+ readonly fetch?: typeof globalThis.fetch;
11
+ };
12
+ export declare const telegram: (options: TelegramOptions) => NotificationChannel;
13
+ //# sourceMappingURL=telegram.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"telegram.d.ts","sourceRoot":"","sources":["../src/telegram.ts"],"names":[],"mappings":"AAeA,OAAO,EAAE,KAAK,mBAAmB,EAAyB,MAAM,cAAc,CAAA;AAE9E,MAAM,MAAM,eAAe,GAAG;IAC5B,oFAAoF;IACpF,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,iFAAiF;IACjF,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAA;IACrB,oFAAoF;IACpF,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;IAC3B,yDAAyD;IACzD,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAA;CACzC,CAAA;AAqCD,eAAO,MAAM,QAAQ,YAAa,eAAe,KAAG,mBA8DnD,CAAA"}