@msg91comm/m91 0.0.0-stage → 1.2.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 (100) hide show
  1. package/CHANGELOG.md +94 -0
  2. package/LICENSE +21 -0
  3. package/README.md +647 -2
  4. package/dist/src/client.d.ts +165 -0
  5. package/dist/src/client.d.ts.map +1 -0
  6. package/dist/src/client.js +221 -0
  7. package/dist/src/client.js.map +1 -0
  8. package/dist/src/exceptions.d.ts +114 -0
  9. package/dist/src/exceptions.d.ts.map +1 -0
  10. package/dist/src/exceptions.js +199 -0
  11. package/dist/src/exceptions.js.map +1 -0
  12. package/dist/src/http.d.ts +71 -0
  13. package/dist/src/http.d.ts.map +1 -0
  14. package/dist/src/http.js +150 -0
  15. package/dist/src/http.js.map +1 -0
  16. package/dist/src/index.d.ts +49 -0
  17. package/dist/src/index.d.ts.map +1 -0
  18. package/dist/src/index.js +45 -0
  19. package/dist/src/index.js.map +1 -0
  20. package/dist/src/integrations/claude-agent-sdk.d.ts +63 -0
  21. package/dist/src/integrations/claude-agent-sdk.d.ts.map +1 -0
  22. package/dist/src/integrations/claude-agent-sdk.js +109 -0
  23. package/dist/src/integrations/claude-agent-sdk.js.map +1 -0
  24. package/dist/src/integrations/google-adk.d.ts +47 -0
  25. package/dist/src/integrations/google-adk.d.ts.map +1 -0
  26. package/dist/src/integrations/google-adk.js +84 -0
  27. package/dist/src/integrations/google-adk.js.map +1 -0
  28. package/dist/src/integrations/index.d.ts +26 -0
  29. package/dist/src/integrations/index.d.ts.map +1 -0
  30. package/dist/src/integrations/index.js +25 -0
  31. package/dist/src/integrations/index.js.map +1 -0
  32. package/dist/src/integrations/langchain.d.ts +94 -0
  33. package/dist/src/integrations/langchain.d.ts.map +1 -0
  34. package/dist/src/integrations/langchain.js +129 -0
  35. package/dist/src/integrations/langchain.js.map +1 -0
  36. package/dist/src/integrations/langgraph.d.ts +79 -0
  37. package/dist/src/integrations/langgraph.d.ts.map +1 -0
  38. package/dist/src/integrations/langgraph.js +114 -0
  39. package/dist/src/integrations/langgraph.js.map +1 -0
  40. package/dist/src/integrations/mastra.d.ts +75 -0
  41. package/dist/src/integrations/mastra.d.ts.map +1 -0
  42. package/dist/src/integrations/mastra.js +109 -0
  43. package/dist/src/integrations/mastra.js.map +1 -0
  44. package/dist/src/integrations/openai-agents.d.ts +61 -0
  45. package/dist/src/integrations/openai-agents.d.ts.map +1 -0
  46. package/dist/src/integrations/openai-agents.js +127 -0
  47. package/dist/src/integrations/openai-agents.js.map +1 -0
  48. package/dist/src/integrations/specs.d.ts +173 -0
  49. package/dist/src/integrations/specs.d.ts.map +1 -0
  50. package/dist/src/integrations/specs.js +217 -0
  51. package/dist/src/integrations/specs.js.map +1 -0
  52. package/dist/src/integrations/vercel-ai.d.ts +74 -0
  53. package/dist/src/integrations/vercel-ai.d.ts.map +1 -0
  54. package/dist/src/integrations/vercel-ai.js +102 -0
  55. package/dist/src/integrations/vercel-ai.js.map +1 -0
  56. package/dist/src/integrations/zod-shapes.d.ts +19 -0
  57. package/dist/src/integrations/zod-shapes.d.ts.map +1 -0
  58. package/dist/src/integrations/zod-shapes.js +44 -0
  59. package/dist/src/integrations/zod-shapes.js.map +1 -0
  60. package/dist/src/models.d.ts +274 -0
  61. package/dist/src/models.d.ts.map +1 -0
  62. package/dist/src/models.js +383 -0
  63. package/dist/src/models.js.map +1 -0
  64. package/dist/src/platform.d.ts +176 -0
  65. package/dist/src/platform.d.ts.map +1 -0
  66. package/dist/src/platform.js +286 -0
  67. package/dist/src/platform.js.map +1 -0
  68. package/dist/src/url-builder.d.ts +65 -0
  69. package/dist/src/url-builder.d.ts.map +1 -0
  70. package/dist/src/url-builder.js +167 -0
  71. package/dist/src/url-builder.js.map +1 -0
  72. package/examples/approval-gate.ts +35 -0
  73. package/examples/basic-alert.ts +53 -0
  74. package/examples/claude-agent-sdk.ts +23 -0
  75. package/examples/deep-agents.ts +31 -0
  76. package/examples/google-adk.ts +39 -0
  77. package/examples/langchain-agent.ts +65 -0
  78. package/examples/langgraph-hitl.ts +70 -0
  79. package/examples/mastra.ts +41 -0
  80. package/examples/openai-agents.ts +33 -0
  81. package/examples/vercel-ai-agent.ts +34 -0
  82. package/examples/webhook-urls.ts +44 -0
  83. package/package.json +135 -4
  84. package/src/client.ts +324 -0
  85. package/src/exceptions.ts +229 -0
  86. package/src/http.ts +174 -0
  87. package/src/index.ts +91 -0
  88. package/src/integrations/claude-agent-sdk.ts +158 -0
  89. package/src/integrations/google-adk.ts +122 -0
  90. package/src/integrations/index.ts +27 -0
  91. package/src/integrations/langchain.ts +207 -0
  92. package/src/integrations/langgraph.ts +142 -0
  93. package/src/integrations/mastra.ts +178 -0
  94. package/src/integrations/openai-agents.ts +173 -0
  95. package/src/integrations/specs.ts +328 -0
  96. package/src/integrations/vercel-ai.ts +151 -0
  97. package/src/integrations/zod-shapes.ts +55 -0
  98. package/src/models.ts +660 -0
  99. package/src/platform.ts +343 -0
  100. package/src/url-builder.ts +225 -0
@@ -0,0 +1,286 @@
1
+ /**
2
+ * The API-key client: alerting **one person, by phone number**.
3
+ *
4
+ * The narrowness is the point, and worth understanding before reaching for
5
+ * this. A team is already solved — somebody creates the channel in the app and
6
+ * the integration posts to its send link. What a link cannot do is address a
7
+ * person a platform is discovering at runtime: a CRM signing up its
8
+ * five-thousandth salesman cannot mint and store five thousand bearer
9
+ * credentials, each able to wake somebody through Do Not Disturb, sitting in a
10
+ * user table with no rotation. So for one person, the secret is one header and
11
+ * the address is a phone number.
12
+ *
13
+ * **An API key cannot alert a team channel.** It can *provision* one — create
14
+ * it, staff it, read its send link — because that is the same authority its
15
+ * owner has in the app. Raising the alert is still the send link's job. A
16
+ * leaked key can rename or restaff a channel; it cannot ring a single phone in
17
+ * it, and that split is the entire reason the two capabilities sit on two
18
+ * credentials.
19
+ */
20
+ import { M91ForbiddenError, M91NotFoundError, M91ValidationError } from './exceptions.js';
21
+ import { Transport } from './http.js';
22
+ import { CHANNEL_NAME_MAX, MAX_MEMBERS_PER_CALL, buildAlertPayload, collapse, parseAlertDetail, parseAlertResponse, parseApiKeyInfo, parseInviteResult, parseProvisionedChannel, parseRecipient, validatePhone, } from './models.js';
23
+ import { checkDelivery } from './client.js';
24
+ export const DEFAULT_BASE_URL = 'https://m91-api.msg91.com';
25
+ export const API_KEY_PREFIX = 'm91sk_';
26
+ /** Invite people, alert them one at a time, and provision team channels. */
27
+ export class M91Platform {
28
+ baseUrl;
29
+ throwOnZeroDelivery;
30
+ apiKey;
31
+ http;
32
+ constructor(apiKey, options = {}) {
33
+ this.apiKey = checkApiKey(apiKey);
34
+ this.throwOnZeroDelivery = options.throwOnZeroDelivery !== false;
35
+ this.baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, '');
36
+ this.http = new Transport({
37
+ ...options,
38
+ headers: { authorization: `Bearer ${this.apiKey}`, ...options.headers },
39
+ });
40
+ }
41
+ /** The key is a live credential that can wake people. Only its prefix. */
42
+ toString() {
43
+ return `M91Platform(${this.apiKey.slice(0, 10)}…, ${this.baseUrl})`;
44
+ }
45
+ url(path) {
46
+ return `${this.baseUrl}/api/v1${path}`;
47
+ }
48
+ /**
49
+ * Check the key works, and see which key it is. **Alerts nobody.**
50
+ *
51
+ * The only route on this surface that is safe to call just to find out
52
+ * whether a credential works — everything else writes something or needs an
53
+ * id that already exists. On an alerting product, "test the connection" must
54
+ * never mean "wake somebody", so do not verify a key by sending an alert.
55
+ *
56
+ * A revoked key never reaches this route, so a success means *live*, not
57
+ * merely well-formed.
58
+ *
59
+ * Note what it does **not** prove: a perfectly good key with nobody accepted
60
+ * behind it still alerts nobody. Invite a real number and check the state
61
+ * before you believe the integration works.
62
+ */
63
+ async verify() {
64
+ return parseApiKeyInfo(await this.http.request({ method: 'GET', url: this.url('/me') }));
65
+ }
66
+ /**
67
+ * Invite somebody to accept direct alerts. **Idempotent.**
68
+ *
69
+ * Keyed on the phone number, so calling it on every login is safe and is the
70
+ * intended usage — retries cannot produce a second channel.
71
+ *
72
+ * The returned `state` needs different handling in each case, and the
73
+ * important one is that only `accepted` can actually be woken. A `pending`
74
+ * recipient has been asked and has not answered: nag them in your own UI, do
75
+ * not re-invite them in a loop. `declined` is terminal — no API re-invites a
76
+ * declined number, and only they can undo it from their own app.
77
+ *
78
+ * Inviting is deliberately separate from alerting: **an alert must never be
79
+ * what invites somebody**, because an invited person is not an M91 user yet
80
+ * and the urgent thing would arrive as an ordinary message that does not
81
+ * pierce Do Not Disturb, does not repeat and does not escalate.
82
+ *
83
+ * `label` is **the name the person sees on the alert**, not a name for the
84
+ * person. It is the only thing on their screen saying where the alert came
85
+ * from, so it should be your product — "Acme CRM", not "Priya" and not an id.
86
+ * Woken at 3am, "Acme CRM" answers the question they have and their own name
87
+ * does not.
88
+ *
89
+ * Two things follow, and both bite: it defaults to `"Direct alerts"` when
90
+ * omitted, and **every invite overwrites it**. Since this call belongs in
91
+ * your login handler, omitting the label on the second login resets a good
92
+ * name back to the default. Pass the same constant every time.
93
+ */
94
+ async inviteRecipient(phone, label) {
95
+ const body = { phone: validatePhone(phone) };
96
+ if (label !== undefined && label !== null && collapse(label))
97
+ body.label = collapse(label);
98
+ return parseRecipient(await this.http.request({ method: 'POST', url: this.url('/recipients'), body }));
99
+ }
100
+ /**
101
+ * Stop alerting somebody. `true` when they are no longer a recipient.
102
+ *
103
+ * Worth wiring to whatever your system calls offboarding. Nothing in an
104
+ * integration is as routinely forgotten, and forgetting it means somebody who
105
+ * left the company is still woken at 3am.
106
+ */
107
+ async removeRecipient(phone) {
108
+ return this.tolerateMissing({
109
+ method: 'DELETE',
110
+ url: this.url(`/recipients/${encodeURIComponent(validatePhone(phone))}`),
111
+ });
112
+ }
113
+ /**
114
+ * Wake one person. They must have been invited **and have accepted**.
115
+ *
116
+ * Alerting a number nobody invited throws `M91NotFoundError`, never a silent
117
+ * invitation.
118
+ *
119
+ * A direct alert *is* a channel with one member, so responses, escalation,
120
+ * deduplication and the consent gate behave exactly as they do for a team —
121
+ * there is no second implementation to keep in step.
122
+ */
123
+ async alertPerson(phone, title, options = {}) {
124
+ const body = buildAlertPayload(title, options);
125
+ body.phone = validatePhone(phone);
126
+ const alert = parseAlertResponse(await this.http.request({ method: 'POST', url: this.url('/alerts'), body }));
127
+ // The same guard the send link gets. A person who reinstalled their phone
128
+ // still has an account and still shows as an accepted recipient, so this
129
+ // path answers 201 with delivered: 0 and looks exactly like success — and
130
+ // with one recipient there is nobody else it reached.
131
+ checkDelivery(alert, this.throwOnZeroDelivery);
132
+ return alert;
133
+ }
134
+ /**
135
+ * Read back what one person decided. `null` if there is no such alert.
136
+ * `alertId` is the `customId` you invented or the `_id` a send returned.
137
+ */
138
+ async getAlert(alertId, phone) {
139
+ try {
140
+ const raw = await this.http.request({
141
+ method: 'GET',
142
+ url: this.url(`/alerts/${encodeURIComponent(String(alertId))}`),
143
+ query: { phone: validatePhone(phone) },
144
+ });
145
+ return parseAlertDetail(raw);
146
+ }
147
+ catch (error) {
148
+ if (error instanceof M91NotFoundError)
149
+ return null;
150
+ throw error;
151
+ }
152
+ }
153
+ /**
154
+ * Close one person's alert.
155
+ *
156
+ * **404 and 403 are success**, as on the send link: "no such alert" and
157
+ * "already resolved" both mean it is not open, which is what was asked for.
158
+ */
159
+ async closeAlert(phone, options) {
160
+ const { customId, alertId } = options ?? {};
161
+ if (Boolean(customId) === Boolean(alertId)) {
162
+ throw new M91ValidationError(`closeAlert() needs exactly one of customId or alertId — got ${customId ? 'both' : 'neither'}`);
163
+ }
164
+ try {
165
+ await this.http.request({
166
+ method: 'POST',
167
+ url: this.url('/alerts/close'),
168
+ body: { phone: validatePhone(phone), ...(customId ? { customId } : { alertId }) },
169
+ });
170
+ }
171
+ catch (error) {
172
+ // 403 as well as 404, matching M91Client.close(). Production answers 403
173
+ // `already_resolved` when the alert is closed already, and "it is closed"
174
+ // is the state the caller asked for. Only close gets this: on a REMOVE a
175
+ // 403 means the channel is not yours, which must not be swallowed.
176
+ if (error instanceof M91NotFoundError || error instanceof M91ForbiddenError)
177
+ return true;
178
+ throw error;
179
+ }
180
+ return true;
181
+ }
182
+ /**
183
+ * Create a team channel and get back the send link that alerts it.
184
+ *
185
+ * **Not idempotent, and deliberately takes no id of your own to make it so.**
186
+ * Calling this twice makes two channels. Store the returned `id` and
187
+ * `sendLink` against your own team the moment you get them — a lost response
188
+ * is an orphaned channel, not something to retry.
189
+ *
190
+ * `alertOwner: false` skips adding the key's owner as a member: a platform
191
+ * minting one channel per customer team has no reason for its own account to
192
+ * be paged for every one of them. They stay the owner either way.
193
+ */
194
+ /**
195
+ * Every team channel this key can raise an alert on.
196
+ *
197
+ * The missing half of {@link createChannel}, which is **not idempotent**: a
198
+ * platform that lost the response to a create had an orphaned channel and no
199
+ * way to find it. Now it can read back what it already has.
200
+ *
201
+ * **Scoped by send access, not by ownership.** Every row carries a
202
+ * `sendLink`, so the list answers "what can I alert?" — which includes a
203
+ * channel whose owner granted this person send access, not only the ones they
204
+ * created. Creating a channel grants its owner send access too, so owned
205
+ * channels are a subset of this rather than a separate case.
206
+ *
207
+ * Channels they merely joined stay absent: being a member means you get
208
+ * woken, not that you may wake others. Archived channels are left out, since
209
+ * a link that cannot alert anybody is worse than no link.
210
+ */
211
+ async listChannels() {
212
+ const rows = await this.http.request({ method: 'GET', url: this.url('/channels') });
213
+ return (Array.isArray(rows) ? rows : []).map(parseProvisionedChannel);
214
+ }
215
+ async createChannel(name, alertOwner = true) {
216
+ const clean = collapse(name ?? '');
217
+ if (!clean)
218
+ throw new M91ValidationError('channel name is required');
219
+ if (clean.length > CHANNEL_NAME_MAX) {
220
+ throw new M91ValidationError(`channel name must be ${CHANNEL_NAME_MAX} characters or fewer, got ${clean.length}`);
221
+ }
222
+ return parseProvisionedChannel(await this.http.request({
223
+ method: 'POST',
224
+ url: this.url('/channels'),
225
+ body: { name: clean, alertOwner: alertOwner !== false },
226
+ }));
227
+ }
228
+ /**
229
+ * Invite people into a team channel. Up to 500 per call.
230
+ *
231
+ * Adding somebody to the roster is not the same as being able to alert them:
232
+ * they are invited, and an invitation is theirs to accept.
233
+ */
234
+ async addMembers(channelId, phones) {
235
+ return parseInviteResult(await this.http.request({
236
+ method: 'POST',
237
+ url: this.url(`/channels/${encodeURIComponent(String(channelId))}/members`),
238
+ body: { phones: checkPhones(phones) },
239
+ }));
240
+ }
241
+ /** Take somebody off a team channel's roster. */
242
+ async removeMember(channelId, phone) {
243
+ return this.tolerateMissing({
244
+ method: 'DELETE',
245
+ url: this.url(`/channels/${encodeURIComponent(String(channelId))}/members/${encodeURIComponent(validatePhone(phone))}`),
246
+ });
247
+ }
248
+ /** Already-not-there is the state the caller asked for. */
249
+ async tolerateMissing(request) {
250
+ try {
251
+ await this.http.request(request);
252
+ }
253
+ catch (error) {
254
+ if (error instanceof M91NotFoundError)
255
+ return true;
256
+ throw error;
257
+ }
258
+ return true;
259
+ }
260
+ }
261
+ /**
262
+ * An API key is told apart from a mobile JWT by its prefix, server-side. So a
263
+ * key without it never reaches the key verifier at all, and comes back as a
264
+ * bare 401 that looks like revocation. Checked here to say what is actually
265
+ * wrong.
266
+ */
267
+ export function checkApiKey(apiKey) {
268
+ const key = String(apiKey ?? '').trim();
269
+ if (!key)
270
+ throw new M91ValidationError('apiKey is required');
271
+ if (!key.startsWith(API_KEY_PREFIX)) {
272
+ throw new M91ValidationError(`apiKey must start with '${API_KEY_PREFIX}' — mint one in the M91 app. ` +
273
+ 'A mobile JWT will not work here.');
274
+ }
275
+ return key;
276
+ }
277
+ export function checkPhones(phones) {
278
+ if (!phones?.length)
279
+ throw new M91ValidationError('phones needs at least one number');
280
+ if (phones.length > MAX_MEMBERS_PER_CALL) {
281
+ throw new M91ValidationError(`cannot invite more than ${MAX_MEMBERS_PER_CALL} numbers at once, got ${phones.length}. ` +
282
+ 'Send them in batches.');
283
+ }
284
+ return phones.map(validatePhone);
285
+ }
286
+ //# sourceMappingURL=platform.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"platform.js","sourceRoot":"","sources":["../../src/platform.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,65 @@
1
+ /**
2
+ * The GET form of a send: an alert spelled as a URL.
3
+ *
4
+ * This is the integration surface that needs no client at all. It goes in a
5
+ * browser bar, a phone shortcut, an `<img>` tag, a cron `curl`, a smart button,
6
+ * or any field in any tool labelled "URL to call". Somebody who has never made
7
+ * an HTTP request in their life can raise an alert by tapping a bookmark, and
8
+ * that is the difference between M91 being wired into something today and being
9
+ * wired in never.
10
+ *
11
+ * **A bare send link raises nothing.** It is the connection test: it answers
12
+ * with the channel's name so whoever pasted it can see they picked the right
13
+ * one. Only a URL with `?title=` fires. That is not a default nobody got round
14
+ * to — the token is in the path, so link previews, crawlers and mail scanners
15
+ * fetch these routinely, and `title` having no default is the only thing
16
+ * between a pasted link and a woken team.
17
+ */
18
+ import { type ResponseOption, type Severity } from './models.js';
19
+ /**
20
+ * Validate a send link and strip anything after the token.
21
+ *
22
+ * Callers paste these out of the app, out of a create-channel response, or out
23
+ * of a colleague's message, so a trailing slash, a stray query string or an
24
+ * already-built alert URL all arrive here. All of them normalise to the bare
25
+ * link.
26
+ */
27
+ export declare function normalizeSendLink(sendLink: string): string;
28
+ /**
29
+ * Render `respond=` from labels or response options.
30
+ *
31
+ * The compact syntax exists because `[{label, effect}]` cannot survive a query
32
+ * string without becoming unreadable. So: a comma-separated list where the
33
+ * **first label is SHARED and the rest are PERSONAL** — the shape almost every
34
+ * real alert has, one button that takes the job for the whole group and some
35
+ * number of "seen" buttons that do not. A trailing `!` forces SHARED on any
36
+ * label, for the caller who wants two people able to claim.
37
+ *
38
+ * Two things a link cannot express, both of which throw here rather than
39
+ * producing a URL that quietly does something else: a **PERSONAL first option**,
40
+ * and a label that genuinely **ends in `!`**. Both are reachable with a POST.
41
+ */
42
+ export declare function buildRespond(labels: Array<ResponseOption | string>): string;
43
+ export interface BuildUrlOptions {
44
+ description?: string;
45
+ severity?: Severity;
46
+ customId?: string;
47
+ /** First label is SHARED, the rest PERSONAL. Up to 6. */
48
+ respond?: Array<ResponseOption | string>;
49
+ isAutoClose?: boolean;
50
+ }
51
+ export interface BuildManySpec extends BuildUrlOptions {
52
+ title: string;
53
+ }
54
+ /** Builds ready-to-paste alert URLs for tools whose only field is "URL to call". */
55
+ export declare class M91URLBuilder {
56
+ readonly sendLink: string;
57
+ constructor(sendLink: string);
58
+ /** The bare link. Answers with the channel name and **raises nothing**. */
59
+ testUrl(): string;
60
+ /** A full URL that raises this alert when anything opens it. */
61
+ build(title: string, options?: BuildUrlOptions): string;
62
+ /** One URL per spec. Each takes the same options as `build`. */
63
+ buildMany(alerts: BuildManySpec[]): string[];
64
+ }
65
+ //# sourceMappingURL=url-builder.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"url-builder.d.ts","sourceRoot":"","sources":["../../src/url-builder.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAGH,OAAO,EAKL,KAAK,cAAc,EACnB,KAAK,QAAQ,EAKd,MAAM,aAAa,CAAC;AAErB;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CA+B1D;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,KAAK,CAAC,cAAc,GAAG,MAAM,CAAC,GAAG,MAAM,CAoD3E;AAED,MAAM,WAAW,eAAe;IAC9B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,yDAAyD;IACzD,OAAO,CAAC,EAAE,KAAK,CAAC,cAAc,GAAG,MAAM,CAAC,CAAC;IACzC,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAED,MAAM,WAAW,aAAc,SAAQ,eAAe;IACpD,KAAK,EAAE,MAAM,CAAC;CACf;AAED,oFAAoF;AACpF,qBAAa,aAAa;IACxB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;gBAEd,QAAQ,EAAE,MAAM;IAI5B,2EAA2E;IAC3E,OAAO,IAAI,MAAM;IAIjB,gEAAgE;IAChE,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,GAAE,eAAoB,GAAG,MAAM;IA+C3D,gEAAgE;IAChE,SAAS,CAAC,MAAM,EAAE,aAAa,EAAE,GAAG,MAAM,EAAE;CAQ7C"}
@@ -0,0 +1,167 @@
1
+ /**
2
+ * The GET form of a send: an alert spelled as a URL.
3
+ *
4
+ * This is the integration surface that needs no client at all. It goes in a
5
+ * browser bar, a phone shortcut, an `<img>` tag, a cron `curl`, a smart button,
6
+ * or any field in any tool labelled "URL to call". Somebody who has never made
7
+ * an HTTP request in their life can raise an alert by tapping a bookmark, and
8
+ * that is the difference between M91 being wired into something today and being
9
+ * wired in never.
10
+ *
11
+ * **A bare send link raises nothing.** It is the connection test: it answers
12
+ * with the channel's name so whoever pasted it can see they picked the right
13
+ * one. Only a URL with `?title=` fires. That is not a default nobody got round
14
+ * to — the token is in the path, so link previews, crawlers and mail scanners
15
+ * fetch these routinely, and `title` having no default is the only thing
16
+ * between a pasted link and a woken team.
17
+ */
18
+ import { M91ValidationError } from './exceptions.js';
19
+ import { CUSTOM_ID_MAX, DESCRIPTION_MAX, LABEL_MAX, MAX_LINK_RESPONSE_OPTIONS, TITLE_MAX, TITLE_MIN, checkSeverity, collapse, } from './models.js';
20
+ /**
21
+ * Validate a send link and strip anything after the token.
22
+ *
23
+ * Callers paste these out of the app, out of a create-channel response, or out
24
+ * of a colleague's message, so a trailing slash, a stray query string or an
25
+ * already-built alert URL all arrive here. All of them normalise to the bare
26
+ * link.
27
+ */
28
+ export function normalizeSendLink(sendLink) {
29
+ if (!sendLink || typeof sendLink !== 'string') {
30
+ throw new M91ValidationError('sendLink is required — the full https://…/s/<token> URL');
31
+ }
32
+ let parsed;
33
+ try {
34
+ parsed = new URL(sendLink.trim());
35
+ }
36
+ catch {
37
+ throw new M91ValidationError(`sendLink must be a full URL beginning http:// or https://, got '${sendLink}'`);
38
+ }
39
+ if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
40
+ throw new M91ValidationError(`sendLink must be a full URL beginning http:// or https://, got '${sendLink}'`);
41
+ }
42
+ const segments = parsed.pathname.split('/').filter(Boolean);
43
+ // Checked because the failure it prevents is silent: a URL without a token
44
+ // authenticates as nothing and every call 401s, which reads like a revoked
45
+ // link rather than a mistyped one.
46
+ if (segments.length < 2 || segments[segments.length - 2] !== 's') {
47
+ throw new M91ValidationError(`sendLink must look like https://m91-api.msg91.com/s/<token> — ` +
48
+ `no /s/<token> found in '${parsed.pathname}'`);
49
+ }
50
+ return `${parsed.origin}/${segments.join('/')}`;
51
+ }
52
+ /**
53
+ * Render `respond=` from labels or response options.
54
+ *
55
+ * The compact syntax exists because `[{label, effect}]` cannot survive a query
56
+ * string without becoming unreadable. So: a comma-separated list where the
57
+ * **first label is SHARED and the rest are PERSONAL** — the shape almost every
58
+ * real alert has, one button that takes the job for the whole group and some
59
+ * number of "seen" buttons that do not. A trailing `!` forces SHARED on any
60
+ * label, for the caller who wants two people able to claim.
61
+ *
62
+ * Two things a link cannot express, both of which throw here rather than
63
+ * producing a URL that quietly does something else: a **PERSONAL first option**,
64
+ * and a label that genuinely **ends in `!`**. Both are reachable with a POST.
65
+ */
66
+ export function buildRespond(labels) {
67
+ const rendered = labels.map((entry, index) => {
68
+ let label;
69
+ let marker = '';
70
+ if (typeof entry === 'string') {
71
+ label = collapse(entry);
72
+ }
73
+ else {
74
+ label = collapse(entry?.label ?? '');
75
+ const shared = (entry?.effect ?? 'SHARED') === 'SHARED';
76
+ if (index === 0 && !shared) {
77
+ throw new M91ValidationError(`the first response option in a link is always SHARED — '${label}' cannot be ` +
78
+ 'PERSONAL here. Use POST with responseOptions.');
79
+ }
80
+ marker = shared && index > 0 ? '!' : '';
81
+ }
82
+ if (!label)
83
+ throw new M91ValidationError('respond labels cannot be empty');
84
+ if (label.includes(',')) {
85
+ // The separator is a comma, so a label containing one would silently
86
+ // become two buttons — an alert offering "Restart" and "then page Ana"
87
+ // as separate answers.
88
+ throw new M91ValidationError(`a respond label cannot contain a comma — it is the separator. ` +
89
+ `Use POST with responseOptions for '${label}'.`);
90
+ }
91
+ const bare = label.replace(/!+$/, '').trim();
92
+ if (bare.length > LABEL_MAX) {
93
+ throw new M91ValidationError(`response label must be ${LABEL_MAX} characters or fewer, got '${label}'`);
94
+ }
95
+ return { rendered: label + marker, key: bare.toLowerCase() };
96
+ });
97
+ if (rendered.length > MAX_LINK_RESPONSE_OPTIONS) {
98
+ throw new M91ValidationError(`a link can carry at most ${MAX_LINK_RESPONSE_OPTIONS} response labels, ` +
99
+ `got ${rendered.length}. Use POST for more.`);
100
+ }
101
+ const seen = new Set();
102
+ for (const { rendered: text, key } of rendered) {
103
+ if (seen.has(key)) {
104
+ throw new M91ValidationError(`cannot repeat the same response label twice: '${text}'`);
105
+ }
106
+ seen.add(key);
107
+ }
108
+ return rendered.map((r) => r.rendered).join(',');
109
+ }
110
+ /** Builds ready-to-paste alert URLs for tools whose only field is "URL to call". */
111
+ export class M91URLBuilder {
112
+ sendLink;
113
+ constructor(sendLink) {
114
+ this.sendLink = normalizeSendLink(sendLink);
115
+ }
116
+ /** The bare link. Answers with the channel name and **raises nothing**. */
117
+ testUrl() {
118
+ return this.sendLink;
119
+ }
120
+ /** A full URL that raises this alert when anything opens it. */
121
+ build(title, options = {}) {
122
+ const cleanTitle = collapse(title ?? '');
123
+ if (!cleanTitle || cleanTitle.length < TITLE_MIN) {
124
+ throw new M91ValidationError(`title must be at least ${TITLE_MIN} characters, got '${title}'`);
125
+ }
126
+ if (cleanTitle.length > TITLE_MAX) {
127
+ throw new M91ValidationError(`title must be ${TITLE_MAX} characters or fewer, got ${cleanTitle.length}`);
128
+ }
129
+ const severity = checkSeverity(options.severity ?? 'MEDIUM');
130
+ const query = new URLSearchParams();
131
+ query.set('title', cleanTitle);
132
+ if (options.description !== undefined && options.description !== null) {
133
+ const description = collapse(options.description);
134
+ if (description.length > DESCRIPTION_MAX) {
135
+ throw new M91ValidationError(`description must be ${DESCRIPTION_MAX} characters or fewer, got ${description.length}`);
136
+ }
137
+ query.set('description', description);
138
+ }
139
+ // MEDIUM is the server's default, so spelling it costs URL length and buys
140
+ // nothing. These get pasted into text boxes and retyped by hand.
141
+ if (severity !== 'MEDIUM')
142
+ query.set('severity', severity);
143
+ if (options.customId !== undefined && options.customId !== null) {
144
+ const customId = collapse(String(options.customId));
145
+ if (customId.length > CUSTOM_ID_MAX) {
146
+ throw new M91ValidationError(`customId must be ${CUSTOM_ID_MAX} characters or fewer, got ${customId.length}`);
147
+ }
148
+ query.set('customId', customId);
149
+ }
150
+ if (options.respond?.length)
151
+ query.set('respond', buildRespond(options.respond));
152
+ if (options.isAutoClose !== undefined) {
153
+ query.set('isAutoClose', options.isAutoClose ? 'true' : 'false');
154
+ }
155
+ return `${this.sendLink}?${query.toString()}`;
156
+ }
157
+ /** One URL per spec. Each takes the same options as `build`. */
158
+ buildMany(alerts) {
159
+ return alerts.map(({ title, ...options }) => {
160
+ if (title === undefined) {
161
+ throw new M91ValidationError("every alert in buildMany needs a 'title'");
162
+ }
163
+ return this.build(title, options);
164
+ });
165
+ }
166
+ }
167
+ //# sourceMappingURL=url-builder.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"url-builder.js","sourceRoot":"","sources":["../../src/url-builder.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,35 @@
1
+ /**
2
+ * The approval gate: ask a human, on their phone, and wait.
3
+ *
4
+ * This is the shape of every human-in-the-loop decision — raise the question,
5
+ * block, act on the answer. One call does all of it, including closing the
6
+ * alert afterwards so nobody is left with a live question that has been
7
+ * answered.
8
+ */
9
+
10
+ import { M91Client } from '@msg91comm/m91';
11
+
12
+ const client = new M91Client(process.env.M91_SEND_LINK!);
13
+
14
+ const approved = await client.requestApproval(
15
+ 'Agent wants to process a refund of ₹48,000 for order ORD-40122',
16
+ {
17
+ description:
18
+ 'Customer says the item never arrived. The courier\'s tracking shows delivered, ' +
19
+ "signed for by 'R. Kumar'. The customer's name is Rahul Kumar. " +
20
+ 'Approving pays out immediately and cannot be reversed.',
21
+ customId: 'refund-ORD-40122', // stable: the order id, never a timestamp
22
+ severity: 'HIGH',
23
+ approveLabel: 'Refund',
24
+ rejectLabel: 'Hold',
25
+ timeoutMs: 300_000,
26
+ },
27
+ );
28
+
29
+ if (approved) {
30
+ console.log('A human approved it. Processing the refund.');
31
+ } else {
32
+ // Rejected, or nobody answered in five minutes. Both mean: do not pay out.
33
+ // There is no configuration that makes silence mean yes.
34
+ console.log('Not approved. Leaving the refund for the morning queue.');
35
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Raise an alert, then close it when the condition heals.
3
+ *
4
+ * export M91_SEND_LINK="https://m91-api.msg91.com/s/your_token"
5
+ * npx tsx examples/basic-alert.ts
6
+ *
7
+ * The send link is the whole credential. It goes in the environment, never in
8
+ * the repository — the token is in the URL path, so the URL *is* the password.
9
+ */
10
+
11
+ import { M91Client, M91DeliveryError } from '@msg91comm/m91';
12
+
13
+ const client = new M91Client(process.env.M91_SEND_LINK!);
14
+
15
+ // Raises nothing. Worth doing on startup: a mistyped link otherwise looks fine
16
+ // until the night it has to work.
17
+ const channel = await client.verify();
18
+ console.log(`Connected to '${channel.name}' (${channel.visibility})`);
19
+
20
+ const alert = await client.send('Database disk 90% full on node-7', {
21
+ description: '/var/lib/postgresql at 90%. Growing ~2GB/hour. ~5 hours left.',
22
+ severity: 'HIGH', // HIGH also places a voice call after 45s of silence
23
+ // Stable: the SAME string every time this condition is true. A check running
24
+ // every minute then raises one alert, not sixty — and this is what closes it.
25
+ customId: 'disk-full-node7',
26
+ responseOptions: [
27
+ { label: 'On it', effect: 'SHARED' }, // takes it on, clears for everyone
28
+ { label: 'Seen', effect: 'PERSONAL' }, // acknowledges, stays with the team
29
+ ],
30
+ });
31
+
32
+ // Not "did the request succeed" — `delivered` is phones actually reached.
33
+ console.log(`Delivered to ${alert.delivered} phones (alert ${alert.id})`);
34
+ if (alert.deduplicated) {
35
+ console.log('An alert for this was already open — nobody was woken a second time.');
36
+ }
37
+
38
+ // ...later, when the disk has been cleared:
39
+ await client.close({ customId: 'disk-full-node7' });
40
+
41
+ // The failure worth catching by name: a 2xx where nobody's phone rang.
42
+ try {
43
+ await client.send('Heartbeat check from the example', { customId: 'example-heartbeat' });
44
+ } catch (error) {
45
+ if (error instanceof M91DeliveryError) {
46
+ console.error(
47
+ `Nobody was reachable: ${error.unregistered.length} unregistered, ` +
48
+ `${error.noDevice} without a device`,
49
+ );
50
+ } else {
51
+ throw error;
52
+ }
53
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * A Claude agent that can read anything, but rings a phone before it runs a
3
+ * shell command or writes a file.
4
+ *
5
+ * M91_SEND_LINK=... ANTHROPIC_API_KEY=... npx tsx examples/claude-agent-sdk.ts
6
+ */
7
+
8
+ import { query } from '@anthropic-ai/claude-agent-sdk';
9
+
10
+ import { M91Client } from '@msg91comm/m91';
11
+ import { createM91CanUseTool, createM91McpServer } from '@msg91comm/m91/integrations/claude-agent-sdk';
12
+
13
+ const client = new M91Client(process.env.M91_SEND_LINK!);
14
+
15
+ for await (const message of query({
16
+ prompt: 'Find the feature branches merged more than 30 days ago and delete them.',
17
+ options: {
18
+ canUseTool: createM91CanUseTool(client, { tools: ['Bash', 'Write', 'Edit'] }),
19
+ mcpServers: { m91: await createM91McpServer(client) },
20
+ },
21
+ })) {
22
+ if (message.type === 'result') console.log(message);
23
+ }