@msg91comm/m91 0.0.0-stage → 1.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/CHANGELOG.md +59 -0
- package/LICENSE +21 -0
- package/README.md +425 -2
- package/dist/src/client.d.ts +165 -0
- package/dist/src/client.d.ts.map +1 -0
- package/dist/src/client.js +221 -0
- package/dist/src/client.js.map +1 -0
- package/dist/src/exceptions.d.ts +114 -0
- package/dist/src/exceptions.d.ts.map +1 -0
- package/dist/src/exceptions.js +199 -0
- package/dist/src/exceptions.js.map +1 -0
- package/dist/src/http.d.ts +71 -0
- package/dist/src/http.d.ts.map +1 -0
- package/dist/src/http.js +150 -0
- package/dist/src/http.js.map +1 -0
- package/dist/src/index.d.ts +49 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +45 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/integrations/index.d.ts +22 -0
- package/dist/src/integrations/index.d.ts.map +1 -0
- package/dist/src/integrations/index.js +21 -0
- package/dist/src/integrations/index.js.map +1 -0
- package/dist/src/integrations/langchain.d.ts +43 -0
- package/dist/src/integrations/langchain.d.ts.map +1 -0
- package/dist/src/integrations/langchain.js +75 -0
- package/dist/src/integrations/langchain.js.map +1 -0
- package/dist/src/integrations/langgraph.d.ts +79 -0
- package/dist/src/integrations/langgraph.d.ts.map +1 -0
- package/dist/src/integrations/langgraph.js +114 -0
- package/dist/src/integrations/langgraph.js.map +1 -0
- package/dist/src/integrations/specs.d.ts +131 -0
- package/dist/src/integrations/specs.d.ts.map +1 -0
- package/dist/src/integrations/specs.js +156 -0
- package/dist/src/integrations/specs.js.map +1 -0
- package/dist/src/integrations/vercel-ai.d.ts +36 -0
- package/dist/src/integrations/vercel-ai.d.ts.map +1 -0
- package/dist/src/integrations/vercel-ai.js +67 -0
- package/dist/src/integrations/vercel-ai.js.map +1 -0
- package/dist/src/models.d.ts +274 -0
- package/dist/src/models.d.ts.map +1 -0
- package/dist/src/models.js +383 -0
- package/dist/src/models.js.map +1 -0
- package/dist/src/platform.d.ts +176 -0
- package/dist/src/platform.d.ts.map +1 -0
- package/dist/src/platform.js +286 -0
- package/dist/src/platform.js.map +1 -0
- package/dist/src/url-builder.d.ts +65 -0
- package/dist/src/url-builder.d.ts.map +1 -0
- package/dist/src/url-builder.js +167 -0
- package/dist/src/url-builder.js.map +1 -0
- package/examples/approval-gate.ts +35 -0
- package/examples/basic-alert.ts +53 -0
- package/examples/langchain-agent.ts +65 -0
- package/examples/langgraph-hitl.ts +70 -0
- package/examples/vercel-ai-agent.ts +34 -0
- package/examples/webhook-urls.ts +44 -0
- package/package.json +83 -4
- package/src/client.ts +324 -0
- package/src/exceptions.ts +229 -0
- package/src/http.ts +174 -0
- package/src/index.ts +91 -0
- package/src/integrations/index.ts +23 -0
- package/src/integrations/langchain.ts +104 -0
- package/src/integrations/langgraph.ts +142 -0
- package/src/integrations/specs.ts +224 -0
- package/src/integrations/vercel-ai.ts +99 -0
- package/src/models.ts +660 -0
- package/src/platform.ts +343 -0
- package/src/url-builder.ts +225 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 1.1.0
|
|
4
|
+
|
|
5
|
+
Three gaps where the server had grown a field and the SDK had not, so the only
|
|
6
|
+
route to a value you had explicitly asked for was the raw body.
|
|
7
|
+
|
|
8
|
+
**Nothing breaks.** `AlertDetail.responses` holds typed objects now, and they
|
|
9
|
+
still answer to `r["label"]` and `r.get("note")` — the spelling callers already
|
|
10
|
+
use — so upgrading changes nothing you have written. (Python; in Node they were
|
|
11
|
+
`unknown[]`, which required a cast either way.)
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- `send({ attachments: [...] })` — https links to files the alert is about. URLs,
|
|
16
|
+
never bytes, because every automation a send link is pasted into has a URL
|
|
17
|
+
field and little else. Validated locally so a typo costs no round trip.
|
|
18
|
+
- `send({ callbackUrl: ... })` — where the outcome is POSTed when somebody answers.
|
|
19
|
+
- `Response.note` and `Response.attachments`, the two things a sender can
|
|
20
|
+
require before an option may be chosen.
|
|
21
|
+
- `AlertDetail.attachments` and `AlertDetail.pending`.
|
|
22
|
+
- `Attachment` and `Response` types.
|
|
23
|
+
- `M91Platform.listChannels()` — the
|
|
24
|
+
missing half of the non-idempotent `create_channel`. A platform that lost the
|
|
25
|
+
response to a create had an orphaned channel and no way to find it.
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
## 1.0.0
|
|
29
|
+
|
|
30
|
+
First release.
|
|
31
|
+
|
|
32
|
+
- `M91Client` — verify, send, getAlert, waitForDecision, close, requestApproval,
|
|
33
|
+
against a channel send link.
|
|
34
|
+
- `M91Platform` — verify the key, invite and alert one person by phone number,
|
|
35
|
+
and provision team channels, with an API key.
|
|
36
|
+
- `M91URLBuilder` — the GET form, for anything whose only field is "URL to call".
|
|
37
|
+
- Integrations: LangChain, LangGraph and the Vercel AI SDK.
|
|
38
|
+
|
|
39
|
+
Zero runtime dependencies: native `fetch`, Node 18+. Every framework adapter
|
|
40
|
+
imports its framework lazily, so installing this package pulls in none of them.
|
|
41
|
+
|
|
42
|
+
Four behaviours worth calling out, because they are the ones that differ from
|
|
43
|
+
what a hand-rolled wrapper around `fetch` usually does:
|
|
44
|
+
|
|
45
|
+
- **A send that reaches nobody throws `M91DeliveryError`.** `delivered` counts
|
|
46
|
+
phones actually sent to, not the size of the roster, so a channel whose device
|
|
47
|
+
tokens are all stale answers 201 and wakes nobody. This applies to
|
|
48
|
+
`alertPerson` as well as `send`, and matters more there: a direct alert has a
|
|
49
|
+
roster of one. Pass `{ throwOnZeroDelivery: false }` to opt out.
|
|
50
|
+
- **`close()` treats 404 and 403 as success.** Nothing left to close is the
|
|
51
|
+
state the caller asked for.
|
|
52
|
+
- **`waitForDecision()` reads `decision` only.** A PERSONAL response ("Seen")
|
|
53
|
+
never ends the wait, because "Seen" must never read as "Approved".
|
|
54
|
+
- **`verify()` on both clients tests a credential without waking anybody.**
|
|
55
|
+
`M91Client.verify()` pings the send link; `M91Platform.verify()` calls
|
|
56
|
+
`GET /api/v1/me` and names the key and its owner.
|
|
57
|
+
- **The send token is redacted from every string the SDK produces** — `toString`,
|
|
58
|
+
error messages and tool results — including text written by the runtime, which
|
|
59
|
+
can name the URL a request failed on.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 MSG91
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,426 @@
|
|
|
1
|
-
#
|
|
1
|
+
# m91
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**Wake a human when software cannot carry on alone.** An M91 alert rings every
|
|
4
|
+
phone in a channel at full volume — through silent mode and Do Not Disturb — and
|
|
5
|
+
keeps ringing until somebody answers. HIGH and CRITICAL alerts place a voice
|
|
6
|
+
call if 45 seconds pass in silence. Whoever takes the alert on is recorded, and
|
|
7
|
+
it clears for everyone else.
|
|
8
|
+
|
|
9
|
+
Email, SMS and chat arrive quietly and wait to be read. That is the right
|
|
10
|
+
behaviour for almost everything, and the wrong behaviour when somebody has to
|
|
11
|
+
act now.
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install @msg91comm/m91
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Node 18+. **Zero dependencies** — native `fetch`, nothing else.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
The package itself needs nothing. The framework adapters use optional peer
|
|
24
|
+
dependencies, imported only when you call one:
|
|
25
|
+
|
|
26
|
+
| For | Install alongside |
|
|
27
|
+
|---|---|
|
|
28
|
+
| `@msg91comm/m91` — client, platform, URL builder | nothing |
|
|
29
|
+
| `@msg91comm/m91/integrations/langchain` | `@langchain/core` |
|
|
30
|
+
| `@msg91comm/m91/integrations/langgraph` | nothing — it wraps the client, not the graph |
|
|
31
|
+
| `@msg91comm/m91/integrations/vercel-ai` | `ai` |
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Quick start
|
|
36
|
+
|
|
37
|
+
You need a **send link** — `https://m91.msg91.com/s/<token>` — from the channel
|
|
38
|
+
in the M91 app. The token is in the URL path, so the link *is* the credential:
|
|
39
|
+
keep it in the environment, never in the repository.
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
import { M91Client } from '@msg91comm/m91';
|
|
43
|
+
|
|
44
|
+
const client = new M91Client(process.env.M91_SEND_LINK!);
|
|
45
|
+
|
|
46
|
+
await client.verify(); // raises nothing; names the channel
|
|
47
|
+
await client.send('Database disk 90% full on node-7', {
|
|
48
|
+
severity: 'HIGH',
|
|
49
|
+
customId: 'disk-full-node7', // stable: the condition, not the moment
|
|
50
|
+
});
|
|
51
|
+
await client.close({ customId: 'disk-full-node7' }); // when the disk has been cleared
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
`verify()` is worth calling on startup. A mistyped link otherwise looks
|
|
55
|
+
perfectly healthy until the night it has to work.
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## The approval gate
|
|
60
|
+
|
|
61
|
+
The reason an agent wants this: a question that blocks until a person answers on
|
|
62
|
+
their phone.
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
const approved = await client.requestApproval(
|
|
66
|
+
'Agent wants to refund ₹48,000 on order ORD-40122',
|
|
67
|
+
{
|
|
68
|
+
description: 'Customer says it never arrived. Courier shows delivered, signed for.',
|
|
69
|
+
customId: 'refund-ORD-40122',
|
|
70
|
+
timeoutMs: 300_000,
|
|
71
|
+
},
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
if (approved) await processRefund();
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
One call raises the alert, waits, and closes it afterwards. It resolves `true`
|
|
78
|
+
only when somebody tapped **Approve**. A rejection, a timeout and an unreachable
|
|
79
|
+
channel all resolve `false` or throw — there is no configuration under which
|
|
80
|
+
silence means yes.
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## Severity
|
|
85
|
+
|
|
86
|
+
| | Delivery | Voice call |
|
|
87
|
+
|---|---|---|
|
|
88
|
+
| `LOW` | Rings through Do Not Disturb, repeats until answered | no |
|
|
89
|
+
| `MEDIUM` — the default | Rings through Do Not Disturb, repeats until answered | no |
|
|
90
|
+
| `HIGH` | The same | after 45s with no answer |
|
|
91
|
+
| `CRITICAL` | The same | after 45s with no answer |
|
|
92
|
+
|
|
93
|
+
MEDIUM is the default because HIGH dials phones, and "I didn't specify" must not
|
|
94
|
+
mean "ring everyone". `requestApproval` defaults to HIGH instead — something is
|
|
95
|
+
waiting on the answer, so a call after 45 seconds of silence is the point.
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## Alert URLs
|
|
100
|
+
|
|
101
|
+
For anything whose only integration field is "URL to call": Grafana, Sentry,
|
|
102
|
+
UptimeRobot, Zapier, a GitHub Action, a cron `curl`, an iOS Shortcut, a browser
|
|
103
|
+
bookmark.
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
import { M91URLBuilder } from '@msg91comm/m91';
|
|
107
|
+
|
|
108
|
+
const builder = new M91URLBuilder(process.env.M91_SEND_LINK!);
|
|
109
|
+
|
|
110
|
+
console.log(builder.build('CPU above 95% on prod-server-3', {
|
|
111
|
+
severity: 'HIGH',
|
|
112
|
+
customId: 'cpu-high-prod3',
|
|
113
|
+
respond: ['On it', 'Escalate'],
|
|
114
|
+
}));
|
|
115
|
+
// https://m91.msg91.com/s/TOKEN?title=CPU+above+95%25+on+prod-server-3&severity=HIGH
|
|
116
|
+
// &customId=cpu-high-prod3&respond=On+it%2CEscalate
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Paste that into Grafana's webhook field and it works — no client, no headers, no
|
|
120
|
+
code.
|
|
121
|
+
|
|
122
|
+
In `respond`, the **first label is SHARED** and the rest are PERSONAL. Up to six
|
|
123
|
+
in a link. To make a later one SHARED too, pass `{ label: 'Also on it' }`
|
|
124
|
+
instead of the bare string, or add a trailing `!`.
|
|
125
|
+
|
|
126
|
+
`builder.testUrl()` returns the bare link. Opening it **raises nothing** and
|
|
127
|
+
answers with the channel's name, which is also why a link preview, a crawler or
|
|
128
|
+
a mail scanner touching a pasted URL wakes nobody.
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## Platform API — alerting one person
|
|
133
|
+
|
|
134
|
+
For a product that discovers its users at runtime, where a per-team send link
|
|
135
|
+
does not fit. Needs an API key (`m91sk_…`), minted from the app.
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
import { M91Platform } from '@msg91comm/m91';
|
|
139
|
+
|
|
140
|
+
const platform = new M91Platform(process.env.M91_API_KEY!);
|
|
141
|
+
|
|
142
|
+
await platform.verify(); // names the key and its owner. Alerts nobody.
|
|
143
|
+
|
|
144
|
+
// Idempotent — this belongs in your login handler, not a migration script.
|
|
145
|
+
// The label is your PRODUCT name: it is what she sees on the alert.
|
|
146
|
+
const recipient = await platform.inviteRecipient('919876543210', 'Acme CRM');
|
|
147
|
+
|
|
148
|
+
if (recipient.state === 'accepted') {
|
|
149
|
+
await platform.alertPerson('919876543210', 'Your nightly export failed', {
|
|
150
|
+
customId: 'export-fail-priya-2026-09',
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
`label` is **what the person sees as the name of the thing alerting them** — your
|
|
156
|
+
product, not their name. It defaults to `"Direct alerts"`, and **every invite
|
|
157
|
+
overwrites it**, so pass the same constant on every login rather than letting a
|
|
158
|
+
later call reset it.
|
|
159
|
+
|
|
160
|
+
Only an **accepted** recipient can be woken. `pending` means they have been
|
|
161
|
+
asked and have not answered — nag them in your own UI rather than re-inviting in
|
|
162
|
+
a loop. `declined` is terminal: no API re-invites a declined number, and only
|
|
163
|
+
they can undo it.
|
|
164
|
+
|
|
165
|
+
An API key can also *provision* a team channel — `createChannel`, `addMembers`,
|
|
166
|
+
`removeMember` — but it **cannot alert one**. Alerting a team is always the send
|
|
167
|
+
link `createChannel` returns. A leaked key can restaff a channel; it cannot ring
|
|
168
|
+
a single phone in it.
|
|
169
|
+
|
|
170
|
+
`alertPerson` throws `M91DeliveryError` on a 2xx that reached nobody, same as
|
|
171
|
+
`send`. It matters more here: a direct alert has a roster of one, so a single
|
|
172
|
+
stale device token is the difference between waking somebody and waking nobody.
|
|
173
|
+
|
|
174
|
+
The cases an API key exists for — per-user alerting, per-tenant channels, roster
|
|
175
|
+
sync — and the much larger set where a send link is the right answer, are worked
|
|
176
|
+
through at **<https://m91.msg91.com/docs>**.
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
## LangChain
|
|
181
|
+
|
|
182
|
+
```ts
|
|
183
|
+
import { M91Client } from '@msg91comm/m91';
|
|
184
|
+
import { createM91ApprovalTool, createM91AlertTool } from '@msg91comm/m91/integrations/langchain';
|
|
185
|
+
|
|
186
|
+
const client = new M91Client(process.env.M91_SEND_LINK!);
|
|
187
|
+
|
|
188
|
+
const tools = [
|
|
189
|
+
await createM91ApprovalTool(client), // blocks for a human
|
|
190
|
+
await createM91AlertTool(client), // wakes somebody, carries on
|
|
191
|
+
...myTools,
|
|
192
|
+
];
|
|
193
|
+
const agent = createReactAgent({ llm, tools, prompt: SYSTEM });
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
The factories are async because `@langchain/core` is imported only when you call
|
|
197
|
+
one — that is what keeps `npm install @msg91comm/m91` at zero dependencies, which matters
|
|
198
|
+
for a package whose job is to work when everything else is on fire.
|
|
199
|
+
|
|
200
|
+
The tool descriptions tell the model when to reach for each one. Your system
|
|
201
|
+
prompt still has to say that approval is **required** before irreversible
|
|
202
|
+
actions, and to list them — see `examples/langchain-agent.ts`, included with the SDK.
|
|
203
|
+
|
|
204
|
+
`createM91ApprovalTool` returns a tool that answers `"approved"`, `"rejected"`
|
|
205
|
+
or `"error: …"`. Nothing but a real approval returns `"approved"`.
|
|
206
|
+
|
|
207
|
+
**Set the defaults where the tool is built, not in the prompt** — how long
|
|
208
|
+
somebody has to wake up is an operations decision, not the model's:
|
|
209
|
+
|
|
210
|
+
```ts
|
|
211
|
+
await createM91ApprovalTool(client, {
|
|
212
|
+
severity: 'CRITICAL',
|
|
213
|
+
approveLabel: 'Deploy',
|
|
214
|
+
rejectLabel: 'Hold release',
|
|
215
|
+
timeoutSeconds: 900,
|
|
216
|
+
});
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
---
|
|
220
|
+
|
|
221
|
+
## LangGraph
|
|
222
|
+
|
|
223
|
+
LangGraph knows how to stop a graph and resume it. What it has no opinion about
|
|
224
|
+
is how the human finds out it stopped. M91 is the part in between.
|
|
225
|
+
|
|
226
|
+
```ts
|
|
227
|
+
import { M91InterruptNotifier } from '@msg91comm/m91/integrations/langgraph';
|
|
228
|
+
|
|
229
|
+
const notifier = new M91InterruptNotifier(client, { timeoutMs: 600_000 });
|
|
230
|
+
|
|
231
|
+
let result = await graph.invoke(state, config);
|
|
232
|
+
if (result.__interrupt__) {
|
|
233
|
+
const data = result.__interrupt__[0].value;
|
|
234
|
+
const decision = await notifier.notify({ title: data.question, customId: data.customId });
|
|
235
|
+
result = await graph.invoke(new Command({ resume: decision }), config);
|
|
236
|
+
}
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
Or let the graph own it:
|
|
240
|
+
|
|
241
|
+
```ts
|
|
242
|
+
import { createM91ApprovalNode } from '@msg91comm/m91/integrations/langgraph';
|
|
243
|
+
|
|
244
|
+
builder.addNode('approveRefund', createM91ApprovalNode(client, {
|
|
245
|
+
approveLabel: 'Process Refund',
|
|
246
|
+
rejectLabel: 'Hold Refund',
|
|
247
|
+
timeoutMs: 600_000,
|
|
248
|
+
}));
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
The node reads `title` (or `question`) and `customId` from state, and writes
|
|
252
|
+
`{ decision, approved }` back for the next edge.
|
|
253
|
+
|
|
254
|
+
---
|
|
255
|
+
|
|
256
|
+
## Vercel AI SDK
|
|
257
|
+
|
|
258
|
+
```ts
|
|
259
|
+
import { createM91ApprovalTool, createM91AlertTool } from '@msg91comm/m91/integrations/vercel-ai';
|
|
260
|
+
|
|
261
|
+
const { text } = await generateText({
|
|
262
|
+
model: openai('gpt-4o'),
|
|
263
|
+
tools: {
|
|
264
|
+
m91_request_approval: await createM91ApprovalTool(client),
|
|
265
|
+
m91_send_alert: await createM91AlertTool(client),
|
|
266
|
+
},
|
|
267
|
+
stopWhen: stepCountIs(10),
|
|
268
|
+
prompt: 'Verify the GDPR deletion request for acct_8821 and action it.',
|
|
269
|
+
});
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
---
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
## Error handling
|
|
276
|
+
|
|
277
|
+
Branch on `.code` — a stable string — never on `.message`, which is written for
|
|
278
|
+
a person and is expected to change.
|
|
279
|
+
|
|
280
|
+
```ts
|
|
281
|
+
import {
|
|
282
|
+
M91AuthError, M91ChannelEmptyError, M91DeliveryError, M91Error,
|
|
283
|
+
M91NetworkError, M91RateLimitError, M91ServerError, M91ValidationError,
|
|
284
|
+
} from '@msg91comm/m91';
|
|
285
|
+
|
|
286
|
+
try {
|
|
287
|
+
await client.send('Payments API is down', { severity: 'CRITICAL', customId: 'payments-down' });
|
|
288
|
+
} catch (error) {
|
|
289
|
+
if (error instanceof M91DeliveryError) {
|
|
290
|
+
// 2xx, and nobody's phone rang. The one failure a status code hides.
|
|
291
|
+
await pageBackupChannel({ unregistered: error.unregistered, noDevice: error.noDevice });
|
|
292
|
+
} else if (error instanceof M91ChannelEmptyError) {
|
|
293
|
+
log.error('M91 channel has no reachable members', error.unregistered);
|
|
294
|
+
} else if (error instanceof M91RateLimitError) {
|
|
295
|
+
// 60 sends a minute per link. Already retried once, at the server's own delay.
|
|
296
|
+
log.warn(`M91 rate limited, retry after ${error.retryAfter}s`);
|
|
297
|
+
} else if (error instanceof M91AuthError) {
|
|
298
|
+
log.error('M91 send link is invalid or has been revoked');
|
|
299
|
+
} else if (error instanceof M91ValidationError) {
|
|
300
|
+
log.error(`M91 rejected the alert: ${error.message}`, error.details);
|
|
301
|
+
} else if (error instanceof M91NetworkError || error instanceof M91ServerError) {
|
|
302
|
+
log.error('M91 unreachable', error);
|
|
303
|
+
} else if (error instanceof M91Error) {
|
|
304
|
+
log.error(`M91 failed: [${error.code}] ${error.message}`);
|
|
305
|
+
} else {
|
|
306
|
+
throw error;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
| Thrown | When |
|
|
312
|
+
|---|---|
|
|
313
|
+
| `M91ValidationError` | a field breaks a limit, locally or at the server (400, 413) |
|
|
314
|
+
| `M91AuthError` | the credential is invalid, expired or revoked (401) |
|
|
315
|
+
| `M91ForbiddenError` | authenticated but not allowed (403) |
|
|
316
|
+
| `M91NotFoundError` | absent — or present and not yours (404) |
|
|
317
|
+
| `M91ChannelEmptyError` | nobody on the channel can receive (409) |
|
|
318
|
+
| `M91RateLimitError` | over 60 sends a minute on this link (429) |
|
|
319
|
+
| `M91ServerError` | M91's fault (5xx), after the retries are spent |
|
|
320
|
+
| `M91NetworkError` | the request never got an answer |
|
|
321
|
+
| `M91DeliveryError` | 2xx, and no phone was reached |
|
|
322
|
+
| `M91TimeoutError` | `throwOnTimeout: true` and nobody answered |
|
|
323
|
+
|
|
324
|
+
**Retries, which happen before any of these reach you:** a 429 is retried once,
|
|
325
|
+
after exactly the delay the server asked for. A 5xx is retried three times, at
|
|
326
|
+
1s, 2s and 4s. A 4xx is never retried — it is not transient. A network failure
|
|
327
|
+
is never retried either: a send that timed out may already have raised the
|
|
328
|
+
alert, and repeating it is how one condition wakes a team four times.
|
|
329
|
+
|
|
330
|
+
---
|
|
331
|
+
|
|
332
|
+
## The rules worth knowing
|
|
333
|
+
|
|
334
|
+
| | |
|
|
335
|
+
|---|---|
|
|
336
|
+
| **`customId` must be stable** | The same condition gets the same string every time — `disk-full-node7`, `refund-ORD-40122`. Built from a timestamp or a UUID it defeats deduplication (a check running every minute raises sixty alerts an hour) and leaves nothing stable to close by. |
|
|
337
|
+
| **`delivered` is not the roster** | It counts phones actually sent to. A channel of four whose device tokens are all stale answers 201 with `delivered: 0`. Assert on `alert.actuallyReachedSomeone`, not on the request succeeding. |
|
|
338
|
+
| **Read `decision`, never `responses[0]`** | Only a SHARED response sets `decision`. A PERSONAL one is an acknowledgement — reading it as an answer would let "Seen" mean "Approved". |
|
|
339
|
+
| **`isAutoClose` defaults to `false`** | The alert stays open until somebody closes it, even after a SHARED response claims it. Set it `true` when the first person taking it on really is the end of the matter. |
|
|
340
|
+
| **A bare send link raises nothing** | `GET /s/<token>` with no `title` is the connection test. That is what stops a link preview or a crawler waking a team, so it is a security boundary rather than a default. |
|
|
341
|
+
| **404 and 403 on `close()` are success** | Nothing left to close is the state you asked for. |
|
|
342
|
+
| **Never log the send link** | The token is in the path. The SDK redacts it in every string, error and tool result; do the same in your own logs. |
|
|
343
|
+
| **An API key cannot alert a team** | It provisions channels. Alerting one is the send link, always. |
|
|
344
|
+
| **Invite before you alert** | Alerting an uninvited number is a 404, never a silent invitation. Alerting somebody you *removed* is a 409 instead — their channel still exists, its roster does not. |
|
|
345
|
+
| **`delivered` is not an answer** | It means phones were sent to. Poll `decision` for what a human actually decided. |
|
|
346
|
+
|
|
347
|
+
---
|
|
348
|
+
|
|
349
|
+
## API reference
|
|
350
|
+
|
|
351
|
+
### `new M91Client(sendLink, options?)`
|
|
352
|
+
|
|
353
|
+
`options`: `{ timeoutMs?, throwOnZeroDelivery?, headers?, fetchImpl?, sleepImpl? }`
|
|
354
|
+
|
|
355
|
+
| Method | |
|
|
356
|
+
|---|---|
|
|
357
|
+
| `verify(): Promise<ChannelInfo>` | Check the link and name the channel. Raises no alert. |
|
|
358
|
+
| `send(title, options?): Promise<AlertResponse>` | Raise an alert. |
|
|
359
|
+
| `getAlert(customIdOrAlertId): Promise<AlertDetail \| null>` | Read one back, by either id. |
|
|
360
|
+
| `waitForDecision(customId, options?): Promise<Decision \| null>` | Block until a SHARED response claims it. |
|
|
361
|
+
| `close({ customId? , alertId? }): Promise<boolean>` | Close it. Exactly one id. |
|
|
362
|
+
| `requestApproval(title, options): Promise<boolean>` | Send, wait, close. |
|
|
363
|
+
|
|
364
|
+
`SendOptions`: `{ description?, severity?, customId?, isAutoClose?, responseOptions? }`
|
|
365
|
+
`WaitOptions`: `{ timeoutMs?, pollIntervalMs?, throwOnTimeout? }`
|
|
366
|
+
`ApprovalOptions`: `{ customId, description?, severity?, approveLabel?, rejectLabel?, timeoutMs?, pollIntervalMs?, timeoutMeansReject? }`
|
|
367
|
+
|
|
368
|
+
### `new M91Platform(apiKey, options?)`
|
|
369
|
+
|
|
370
|
+
| Method | |
|
|
371
|
+
|---|---|
|
|
372
|
+
| `verify(): Promise<ApiKeyInfo>` | Name the key and its owner. Alerts nobody. |
|
|
373
|
+
| `inviteRecipient(phone, label?): Promise<Recipient>` | Invite somebody. Idempotent. `label` is your product name. |
|
|
374
|
+
| `removeRecipient(phone): Promise<boolean>` | Stop alerting them. |
|
|
375
|
+
| `alertPerson(phone, title, options?): Promise<AlertResponse>` | Wake one person. |
|
|
376
|
+
| `getAlert(alertId, phone): Promise<AlertDetail \| null>` | Read what they decided. |
|
|
377
|
+
| `closeAlert(phone, { customId? , alertId? }): Promise<boolean>` | Close their alert. |
|
|
378
|
+
| `createChannel(name, alertOwner?): Promise<ProvisionedChannel>` | **Not idempotent.** Store the result. |
|
|
379
|
+
| `addMembers(channelId, phones): Promise<InviteResult>` | Up to 500 per call. |
|
|
380
|
+
| `removeMember(channelId, phone): Promise<boolean>` | Take them off the roster. |
|
|
381
|
+
|
|
382
|
+
### `new M91URLBuilder(sendLink)`
|
|
383
|
+
|
|
384
|
+
| Method | |
|
|
385
|
+
|---|---|
|
|
386
|
+
| `build(title, options?): string` | One alert URL. |
|
|
387
|
+
| `buildMany(alerts): string[]` | One per spec. |
|
|
388
|
+
| `testUrl(): string` | The bare link. Raises nothing. |
|
|
389
|
+
|
|
390
|
+
### Types
|
|
391
|
+
|
|
392
|
+
`Severity` · `Effect` · `AlertStatus` · `RecipientState` · `ResponseOption` ·
|
|
393
|
+
`Decision` · `AlertResponse` · `AlertDetail` · `ChannelInfo` · `Recipient` ·
|
|
394
|
+
`ProvisionedChannel` · `InviteResult` · `ApiKeyInfo`
|
|
395
|
+
|
|
396
|
+
Helpers: `isApproved(decision, approveLabel?)` · `validatePhone(phone)` ·
|
|
397
|
+
`redact(url)` · `buildAlertPayload(title, fields?)`
|
|
398
|
+
|
|
399
|
+
---
|
|
400
|
+
|
|
401
|
+
## Running the tests
|
|
402
|
+
|
|
403
|
+
```bash
|
|
404
|
+
npm test
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
Compiles with `tsc` and runs the suite on Node's built-in test runner — no test
|
|
408
|
+
framework to install. Every test swaps `fetch`; nothing here makes a network
|
|
409
|
+
call, and nothing wakes anybody.
|
|
410
|
+
|
|
411
|
+
For the SDK against a real socket — a live server, a real poll loop, a real
|
|
412
|
+
timeout — run the demo:
|
|
413
|
+
|
|
414
|
+
```bash
|
|
415
|
+
cd ../demo && python3 run_node_demo.py
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
---
|
|
419
|
+
|
|
420
|
+
## Examples
|
|
421
|
+
|
|
422
|
+
The `examples/` directory included with the SDK covers a basic alert, the approval
|
|
423
|
+
gate, a LangChain agent, the LangGraph `interrupt()` pattern, a Vercel AI SDK
|
|
424
|
+
agent, and webhook URLs for Grafana and friends.
|
|
425
|
+
|
|
426
|
+
Full API documentation: **<https://m91.msg91.com/docs>**
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The send-link client: raise an alert, read what was decided, close it.
|
|
3
|
+
*
|
|
4
|
+
* A send link is a credential in a URL. It goes in a Grafana webhook field, a
|
|
5
|
+
* cron, a CI job or an agent's environment, and it needs no header, no account
|
|
6
|
+
* and no library — which is why this client is a convenience over one URL
|
|
7
|
+
* rather than the only way in.
|
|
8
|
+
*
|
|
9
|
+
* Two things it does that a bare `fetch` does not, and both are why it exists:
|
|
10
|
+
*
|
|
11
|
+
* - **It checks that somebody was actually reached.** A 201 means the alert was
|
|
12
|
+
* filed, not that a phone rang.
|
|
13
|
+
* - **It reads the decision, not the responses.** Only a SHARED response decides.
|
|
14
|
+
*/
|
|
15
|
+
import { type TransportOptions } from './http.js';
|
|
16
|
+
import { type AlertDetail, type AlertResponse, type ChannelInfo, type Decision, type ResponseOption, type Severity } from './models.js';
|
|
17
|
+
/**
|
|
18
|
+
* Below this, polling is a busy loop against a rate-limited endpoint and buys
|
|
19
|
+
* nothing: a human waking up and reaching for a phone is not a sub-second event.
|
|
20
|
+
*/
|
|
21
|
+
export declare const MIN_POLL_INTERVAL_MS = 2000;
|
|
22
|
+
export interface SendOptions {
|
|
23
|
+
description?: string;
|
|
24
|
+
/**
|
|
25
|
+
* LOW and MEDIUM deliver at alarm volume through Do Not Disturb. **HIGH and
|
|
26
|
+
* CRITICAL also place a voice call** if nobody answers in 45 seconds, so they
|
|
27
|
+
* are never the default — "I didn't specify" must not mean "ring everyone".
|
|
28
|
+
*/
|
|
29
|
+
severity?: Severity;
|
|
30
|
+
/**
|
|
31
|
+
* Your own stable id for *the condition*, not for this occurrence. The same
|
|
32
|
+
* disk filling up is the same `customId` every time. Built from a timestamp
|
|
33
|
+
* or a UUID it defeats deduplication — a check running every minute raises
|
|
34
|
+
* sixty alerts an hour — and leaves nothing stable to close by.
|
|
35
|
+
*/
|
|
36
|
+
customId?: string;
|
|
37
|
+
/**
|
|
38
|
+
* Left false, the alert stays open until somebody closes it, even after a
|
|
39
|
+
* SHARED response claims it. Set true when the first person taking it on is
|
|
40
|
+
* genuinely the end of the matter.
|
|
41
|
+
*/
|
|
42
|
+
isAutoClose?: boolean;
|
|
43
|
+
responseOptions?: Array<ResponseOption | string>;
|
|
44
|
+
/**
|
|
45
|
+
* Whether the free-text "Something else" reply is offered. Tri-state: leave
|
|
46
|
+
* it unset and the server's default (on) stands.
|
|
47
|
+
*/
|
|
48
|
+
other?: boolean;
|
|
49
|
+
/** https links to files the alert is about. URLs, never bytes. */
|
|
50
|
+
attachments?: string[];
|
|
51
|
+
/** Where the outcome is POSTed when somebody answers. */
|
|
52
|
+
callbackUrl?: string;
|
|
53
|
+
}
|
|
54
|
+
export interface WaitOptions {
|
|
55
|
+
timeoutMs?: number;
|
|
56
|
+
pollIntervalMs?: number;
|
|
57
|
+
throwOnTimeout?: boolean;
|
|
58
|
+
}
|
|
59
|
+
export interface ApprovalOptions {
|
|
60
|
+
customId: string;
|
|
61
|
+
description?: string;
|
|
62
|
+
severity?: Severity;
|
|
63
|
+
approveLabel?: string;
|
|
64
|
+
rejectLabel?: string;
|
|
65
|
+
timeoutMs?: number;
|
|
66
|
+
pollIntervalMs?: number;
|
|
67
|
+
/** A timeout returns false. There is deliberately no option making it true. */
|
|
68
|
+
timeoutMeansReject?: boolean;
|
|
69
|
+
}
|
|
70
|
+
export interface ClientOptions extends TransportOptions {
|
|
71
|
+
/**
|
|
72
|
+
* Throw `M91DeliveryError` when a send reaches nobody. On by default, because
|
|
73
|
+
* the silent version of this failure is an alert nobody ever hears about.
|
|
74
|
+
*/
|
|
75
|
+
throwOnZeroDelivery?: boolean;
|
|
76
|
+
}
|
|
77
|
+
/** Alerts one channel, through one send link. */
|
|
78
|
+
export declare class M91Client {
|
|
79
|
+
readonly sendLink: string;
|
|
80
|
+
readonly throwOnZeroDelivery: boolean;
|
|
81
|
+
private readonly http;
|
|
82
|
+
private readonly sleep;
|
|
83
|
+
private readonly now;
|
|
84
|
+
constructor(sendLink: string, options?: ClientOptions);
|
|
85
|
+
/** Redacted, always: a token in a log, a trace or a crash report can wake a team. */
|
|
86
|
+
toString(): string;
|
|
87
|
+
private url;
|
|
88
|
+
/**
|
|
89
|
+
* Check the link works, and see which channel it points at.
|
|
90
|
+
*
|
|
91
|
+
* **Raises no alert.** Safe on startup, in a health check, or while somebody
|
|
92
|
+
* is pasting the URL into a settings page — which is the point: a mistyped
|
|
93
|
+
* link otherwise shows "connected" and only reveals itself days later, when
|
|
94
|
+
* an alert silently fails to fire.
|
|
95
|
+
*/
|
|
96
|
+
verify(): Promise<ChannelInfo>;
|
|
97
|
+
/** Raise an alert on this channel. */
|
|
98
|
+
send(title: string, options?: SendOptions): Promise<AlertResponse>;
|
|
99
|
+
/**
|
|
100
|
+
* Read one alert back. `null` when there is no such alert on this channel.
|
|
101
|
+
*
|
|
102
|
+
* The id is **either** the `customId` you invented or the `_id` the send
|
|
103
|
+
* returned — a caller knows one of them and should not have to say which.
|
|
104
|
+
*/
|
|
105
|
+
getAlert(customIdOrAlertId: string): Promise<AlertDetail | null>;
|
|
106
|
+
/**
|
|
107
|
+
* Wait until somebody takes the alert on, or the clock runs out.
|
|
108
|
+
*
|
|
109
|
+
* Resolves with the SHARED response that claimed it. **PERSONAL responses
|
|
110
|
+
* never end this wait** — a "Seen" is an acknowledgement, not an answer, and
|
|
111
|
+
* treating it as one is the mistake `decision` exists to prevent.
|
|
112
|
+
*
|
|
113
|
+
* Stops early and resolves `null` if the alert is closed without ever being
|
|
114
|
+
* claimed: there is nothing left to wait for.
|
|
115
|
+
*/
|
|
116
|
+
waitForDecision(customId: string, options?: WaitOptions): Promise<Decision | null>;
|
|
117
|
+
/**
|
|
118
|
+
* Close an alert this link raised. Always `true` when there is nothing open.
|
|
119
|
+
*
|
|
120
|
+
* A monitor that sees the problem heal closes it itself rather than waiting
|
|
121
|
+
* for a person to wake up and notice.
|
|
122
|
+
*
|
|
123
|
+
* **404 and 403 are success.** "No such alert" and "not yours to close" both
|
|
124
|
+
* mean the alert is not open on this channel, which is the state the caller
|
|
125
|
+
* asked for. Anything else would make every close a branch, and every retry
|
|
126
|
+
* after a dropped response a false failure.
|
|
127
|
+
*/
|
|
128
|
+
close(options: {
|
|
129
|
+
customId?: string;
|
|
130
|
+
alertId?: string;
|
|
131
|
+
}): Promise<boolean>;
|
|
132
|
+
/**
|
|
133
|
+
* Ask a human, on their phone, and wait for the answer. `true` = approved.
|
|
134
|
+
*
|
|
135
|
+
* The shape of every human-in-the-loop gate: raise the question, block, act
|
|
136
|
+
* on the answer, close the alert either way. Severity defaults to HIGH here —
|
|
137
|
+
* unlike `send` — because something is *waiting* on the answer, so a phone
|
|
138
|
+
* call after 45 seconds of silence is the point rather than an escalation.
|
|
139
|
+
*
|
|
140
|
+
* Both labels are SHARED: approving and rejecting are each a final answer
|
|
141
|
+
* that settles the question for everybody, and neither is an "I saw it".
|
|
142
|
+
*
|
|
143
|
+
* On timeout with `timeoutMeansReject` (the default) it resolves false —
|
|
144
|
+
* nobody answered, so nothing consequential happens. Set it false to get
|
|
145
|
+
* `M91TimeoutError` instead. There is deliberately no way to make a timeout
|
|
146
|
+
* mean approved.
|
|
147
|
+
*/
|
|
148
|
+
requestApproval(title: string, options: ApprovalOptions): Promise<boolean>;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* A 2xx that woke nobody is a failure, and only the caller can decide what to do.
|
|
152
|
+
*
|
|
153
|
+
* `delivered` counts phones actually sent to, so this fires when a channel's
|
|
154
|
+
* devices are all stale or its members have not installed the app — a case that
|
|
155
|
+
* answers 201 and looks exactly like success.
|
|
156
|
+
*/
|
|
157
|
+
export declare function checkDelivery(alert: AlertResponse, enabled: boolean): void;
|
|
158
|
+
/**
|
|
159
|
+
* Whether one poll ends the wait. Pure, so the rule lives in one place.
|
|
160
|
+
*/
|
|
161
|
+
export declare function waitVerdict(detail: AlertDetail | null): {
|
|
162
|
+
stop: boolean;
|
|
163
|
+
decision: Decision | null;
|
|
164
|
+
};
|
|
165
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/client.ts"],"names":[],"mappings":"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"}
|