@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
|
@@ -0,0 +1,221 @@
|
|
|
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 { M91DeliveryError, M91ForbiddenError, M91NotFoundError, M91TimeoutError, M91ValidationError, } from './exceptions.js';
|
|
16
|
+
import { Transport, redact } from './http.js';
|
|
17
|
+
import { buildAlertPayload, isApproved, parseAlertDetail, parseAlertResponse, parseChannelInfo, } from './models.js';
|
|
18
|
+
import { normalizeSendLink } from './url-builder.js';
|
|
19
|
+
/**
|
|
20
|
+
* Below this, polling is a busy loop against a rate-limited endpoint and buys
|
|
21
|
+
* nothing: a human waking up and reaching for a phone is not a sub-second event.
|
|
22
|
+
*/
|
|
23
|
+
export const MIN_POLL_INTERVAL_MS = 2000;
|
|
24
|
+
/** Alerts one channel, through one send link. */
|
|
25
|
+
export class M91Client {
|
|
26
|
+
sendLink;
|
|
27
|
+
throwOnZeroDelivery;
|
|
28
|
+
http;
|
|
29
|
+
sleep;
|
|
30
|
+
now;
|
|
31
|
+
constructor(sendLink, options = {}) {
|
|
32
|
+
this.sendLink = normalizeSendLink(sendLink);
|
|
33
|
+
this.throwOnZeroDelivery = options.throwOnZeroDelivery !== false;
|
|
34
|
+
this.http = new Transport(options);
|
|
35
|
+
this.sleep = options.sleepImpl ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
36
|
+
this.now = () => Date.now();
|
|
37
|
+
}
|
|
38
|
+
/** Redacted, always: a token in a log, a trace or a crash report can wake a team. */
|
|
39
|
+
toString() {
|
|
40
|
+
return `M91Client(${redact(this.sendLink)})`;
|
|
41
|
+
}
|
|
42
|
+
url(suffix = '') {
|
|
43
|
+
return this.sendLink + suffix;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Check the link works, and see which channel it points at.
|
|
47
|
+
*
|
|
48
|
+
* **Raises no alert.** Safe on startup, in a health check, or while somebody
|
|
49
|
+
* is pasting the URL into a settings page — which is the point: a mistyped
|
|
50
|
+
* link otherwise shows "connected" and only reveals itself days later, when
|
|
51
|
+
* an alert silently fails to fire.
|
|
52
|
+
*/
|
|
53
|
+
async verify() {
|
|
54
|
+
return parseChannelInfo(await this.http.request({ method: 'GET', url: this.url() }));
|
|
55
|
+
}
|
|
56
|
+
/** Raise an alert on this channel. */
|
|
57
|
+
async send(title, options = {}) {
|
|
58
|
+
const payload = buildAlertPayload(title, options);
|
|
59
|
+
const alert = parseAlertResponse(await this.http.request({ method: 'POST', url: this.url(), body: payload }));
|
|
60
|
+
checkDelivery(alert, this.throwOnZeroDelivery);
|
|
61
|
+
return alert;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Read one alert back. `null` when there is no such alert on this channel.
|
|
65
|
+
*
|
|
66
|
+
* The id is **either** the `customId` you invented or the `_id` the send
|
|
67
|
+
* returned — a caller knows one of them and should not have to say which.
|
|
68
|
+
*/
|
|
69
|
+
async getAlert(customIdOrAlertId) {
|
|
70
|
+
try {
|
|
71
|
+
const raw = await this.http.request({
|
|
72
|
+
method: 'GET',
|
|
73
|
+
url: this.url(`/alerts/${encodeURIComponent(String(customIdOrAlertId))}`),
|
|
74
|
+
});
|
|
75
|
+
return parseAlertDetail(raw);
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
if (error instanceof M91NotFoundError)
|
|
79
|
+
return null;
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Wait until somebody takes the alert on, or the clock runs out.
|
|
85
|
+
*
|
|
86
|
+
* Resolves with the SHARED response that claimed it. **PERSONAL responses
|
|
87
|
+
* never end this wait** — a "Seen" is an acknowledgement, not an answer, and
|
|
88
|
+
* treating it as one is the mistake `decision` exists to prevent.
|
|
89
|
+
*
|
|
90
|
+
* Stops early and resolves `null` if the alert is closed without ever being
|
|
91
|
+
* claimed: there is nothing left to wait for.
|
|
92
|
+
*/
|
|
93
|
+
async waitForDecision(customId, options = {}) {
|
|
94
|
+
const timeoutMs = options.timeoutMs ?? 300_000;
|
|
95
|
+
const requested = options.pollIntervalMs ?? 5000;
|
|
96
|
+
if (requested < MIN_POLL_INTERVAL_MS) {
|
|
97
|
+
console.warn(`[m91] pollIntervalMs=${requested} is below ${MIN_POLL_INTERVAL_MS}. The send link is ` +
|
|
98
|
+
`rate limited to 60 requests a minute and a person cannot answer that fast; ` +
|
|
99
|
+
`using ${MIN_POLL_INTERVAL_MS}ms instead.`);
|
|
100
|
+
}
|
|
101
|
+
const interval = Math.max(requested, MIN_POLL_INTERVAL_MS);
|
|
102
|
+
const deadline = this.now() + timeoutMs;
|
|
103
|
+
for (;;) {
|
|
104
|
+
const verdict = waitVerdict(await this.getAlert(customId));
|
|
105
|
+
if (verdict.stop)
|
|
106
|
+
return verdict.decision;
|
|
107
|
+
const remaining = deadline - this.now();
|
|
108
|
+
if (remaining <= 0) {
|
|
109
|
+
if (options.throwOnTimeout)
|
|
110
|
+
throw new M91TimeoutError(customId, timeoutMs);
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
await this.sleep(Math.min(interval, remaining));
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Close an alert this link raised. Always `true` when there is nothing open.
|
|
118
|
+
*
|
|
119
|
+
* A monitor that sees the problem heal closes it itself rather than waiting
|
|
120
|
+
* for a person to wake up and notice.
|
|
121
|
+
*
|
|
122
|
+
* **404 and 403 are success.** "No such alert" and "not yours to close" both
|
|
123
|
+
* mean the alert is not open on this channel, which is the state the caller
|
|
124
|
+
* asked for. Anything else would make every close a branch, and every retry
|
|
125
|
+
* after a dropped response a false failure.
|
|
126
|
+
*/
|
|
127
|
+
async close(options) {
|
|
128
|
+
const { customId, alertId } = options ?? {};
|
|
129
|
+
if (Boolean(customId) === Boolean(alertId)) {
|
|
130
|
+
throw new M91ValidationError(`close() needs exactly one of customId or alertId — got ${customId ? 'both' : 'neither'}`);
|
|
131
|
+
}
|
|
132
|
+
try {
|
|
133
|
+
await this.http.request({
|
|
134
|
+
method: 'POST',
|
|
135
|
+
url: this.url('/close'),
|
|
136
|
+
body: customId ? { customId } : { alertId },
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
if (error instanceof M91NotFoundError || error instanceof M91ForbiddenError)
|
|
141
|
+
return true;
|
|
142
|
+
throw error;
|
|
143
|
+
}
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Ask a human, on their phone, and wait for the answer. `true` = approved.
|
|
148
|
+
*
|
|
149
|
+
* The shape of every human-in-the-loop gate: raise the question, block, act
|
|
150
|
+
* on the answer, close the alert either way. Severity defaults to HIGH here —
|
|
151
|
+
* unlike `send` — because something is *waiting* on the answer, so a phone
|
|
152
|
+
* call after 45 seconds of silence is the point rather than an escalation.
|
|
153
|
+
*
|
|
154
|
+
* Both labels are SHARED: approving and rejecting are each a final answer
|
|
155
|
+
* that settles the question for everybody, and neither is an "I saw it".
|
|
156
|
+
*
|
|
157
|
+
* On timeout with `timeoutMeansReject` (the default) it resolves false —
|
|
158
|
+
* nobody answered, so nothing consequential happens. Set it false to get
|
|
159
|
+
* `M91TimeoutError` instead. There is deliberately no way to make a timeout
|
|
160
|
+
* mean approved.
|
|
161
|
+
*/
|
|
162
|
+
async requestApproval(title, options) {
|
|
163
|
+
const { customId, description, severity = 'HIGH', approveLabel = 'Approve', rejectLabel = 'Reject', timeoutMs = 300_000, pollIntervalMs = 5000, timeoutMeansReject = true, } = options;
|
|
164
|
+
await this.send(title, {
|
|
165
|
+
description,
|
|
166
|
+
severity,
|
|
167
|
+
customId,
|
|
168
|
+
isAutoClose: false,
|
|
169
|
+
responseOptions: [
|
|
170
|
+
{ label: approveLabel, effect: 'SHARED' },
|
|
171
|
+
{ label: rejectLabel, effect: 'SHARED' },
|
|
172
|
+
],
|
|
173
|
+
});
|
|
174
|
+
try {
|
|
175
|
+
const decision = await this.waitForDecision(customId, { timeoutMs, pollIntervalMs });
|
|
176
|
+
if (decision === null) {
|
|
177
|
+
if (!timeoutMeansReject)
|
|
178
|
+
throw new M91TimeoutError(customId, timeoutMs);
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
return isApproved(decision, approveLabel);
|
|
182
|
+
}
|
|
183
|
+
finally {
|
|
184
|
+
// Answered or not, the question is no longer live. Left open, a rejected
|
|
185
|
+
// deploy keeps somebody's phone in the "needs attention" list for an
|
|
186
|
+
// action that already resolved.
|
|
187
|
+
await this.close({ customId });
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* A 2xx that woke nobody is a failure, and only the caller can decide what to do.
|
|
193
|
+
*
|
|
194
|
+
* `delivered` counts phones actually sent to, so this fires when a channel's
|
|
195
|
+
* devices are all stale or its members have not installed the app — a case that
|
|
196
|
+
* answers 201 and looks exactly like success.
|
|
197
|
+
*/
|
|
198
|
+
export function checkDelivery(alert, enabled) {
|
|
199
|
+
if (!enabled || alert.actuallyReachedSomeone)
|
|
200
|
+
return;
|
|
201
|
+
throw new M91DeliveryError(`The alert was filed but reached nobody: delivered=0, ` +
|
|
202
|
+
`unregistered=${alert.unregistered.length}, noDevice=${alert.noDevice}, ` +
|
|
203
|
+
`pending=${alert.pending}. Nobody's phone rang.`, alert.delivered, alert.unregistered, alert.noDevice, alert.pending);
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Whether one poll ends the wait. Pure, so the rule lives in one place.
|
|
207
|
+
*/
|
|
208
|
+
export function waitVerdict(detail) {
|
|
209
|
+
// Not visible yet. A send answers before the fan-out finishes, so an
|
|
210
|
+
// immediate read can outrun it; keep waiting rather than calling it gone.
|
|
211
|
+
if (detail === null)
|
|
212
|
+
return { stop: false, decision: null };
|
|
213
|
+
if (detail.decision !== null)
|
|
214
|
+
return { stop: true, decision: detail.decision };
|
|
215
|
+
// Closed with nobody having claimed it — by a monitor that saw the problem
|
|
216
|
+
// heal, or by hand. There is no answer coming.
|
|
217
|
+
if (detail.isClosed)
|
|
218
|
+
return { stop: true, decision: null };
|
|
219
|
+
return { stop: false, decision: null };
|
|
220
|
+
}
|
|
221
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/client.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every failure the SDK can throw, and the one place an HTTP response becomes one.
|
|
3
|
+
*
|
|
4
|
+
* Branch on `.code` — a stable string the API contract pins down — never on
|
|
5
|
+
* `.message`, which is written for a person reading a terminal and is expected
|
|
6
|
+
* to change.
|
|
7
|
+
*/
|
|
8
|
+
export declare class M91Error extends Error {
|
|
9
|
+
readonly code: string;
|
|
10
|
+
readonly statusCode?: number;
|
|
11
|
+
readonly details?: unknown;
|
|
12
|
+
constructor(message: string, code?: string, statusCode?: number, details?: unknown);
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* 400/413. The request was wrong, and it is never retried.
|
|
16
|
+
*
|
|
17
|
+
* Also thrown locally, before any HTTP call, when a field breaks a documented
|
|
18
|
+
* limit. Every schema server-side is `.strict()`, so a mistyped field name is a
|
|
19
|
+
* 400 rather than a silent drop — and a round trip to learn that is a round
|
|
20
|
+
* trip wasted.
|
|
21
|
+
*/
|
|
22
|
+
export declare class M91ValidationError extends M91Error {
|
|
23
|
+
constructor(message: string, details?: unknown, statusCode?: number, code?: string);
|
|
24
|
+
}
|
|
25
|
+
/** 401. Credential invalid, expired or revoked — nothing is cached server-side. */
|
|
26
|
+
export declare class M91AuthError extends M91Error {
|
|
27
|
+
constructor(message?: string, details?: unknown);
|
|
28
|
+
}
|
|
29
|
+
/** 403. Authenticated, but not allowed. On `close()` this means success. */
|
|
30
|
+
export declare class M91ForbiddenError extends M91Error {
|
|
31
|
+
constructor(message?: string, details?: unknown);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* 404. Absent — **or present and not yours**. The API answers the same way to
|
|
35
|
+
* both on purpose, so an id cannot become an oracle for what exists.
|
|
36
|
+
*/
|
|
37
|
+
export declare class M91NotFoundError extends M91Error {
|
|
38
|
+
constructor(message?: string, details?: unknown);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* 409. The channel has nobody who can actually be woken.
|
|
42
|
+
*
|
|
43
|
+
* `unregistered` is the invited numbers with no M91 account — invited, never
|
|
44
|
+
* installed the app, and unreachable. An ordinary SMS would not pierce Do Not
|
|
45
|
+
* Disturb, would not repeat, and would not escalate.
|
|
46
|
+
*/
|
|
47
|
+
export declare class M91ChannelEmptyError extends M91Error {
|
|
48
|
+
readonly unregistered: string[];
|
|
49
|
+
constructor(message: string, unregistered?: string[], details?: unknown);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* 429. 60 sends a minute per send link.
|
|
53
|
+
*
|
|
54
|
+
* `retryAfter` always has a value: the server sets `Retry-After` on every 429
|
|
55
|
+
* precisely so a caller does not have to guess, and the usual guess is to retry
|
|
56
|
+
* immediately and forever.
|
|
57
|
+
*/
|
|
58
|
+
export declare class M91RateLimitError extends M91Error {
|
|
59
|
+
readonly retryAfter: number;
|
|
60
|
+
constructor(message?: string, retryAfter?: number, details?: unknown);
|
|
61
|
+
}
|
|
62
|
+
/** 5xx. Not the caller's fault. Retried with backoff before it reaches you. */
|
|
63
|
+
export declare class M91ServerError extends M91Error {
|
|
64
|
+
constructor(message?: string, statusCode?: number, details?: unknown);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* The request never got an answer — DNS, TLS, connect or read timeout.
|
|
68
|
+
*
|
|
69
|
+
* Not retried. A send that timed out may or may not have raised an alert, and
|
|
70
|
+
* retrying it blindly is how one condition wakes a team four times.
|
|
71
|
+
*/
|
|
72
|
+
export declare class M91NetworkError extends M91Error {
|
|
73
|
+
constructor(message?: string, details?: unknown);
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* The API said 2xx and **nobody's phone rang**.
|
|
77
|
+
*
|
|
78
|
+
* The failure mode integrations actually hit, and it is invisible if you only
|
|
79
|
+
* check the status code. `delivered` counts phones sent to, not the size of the
|
|
80
|
+
* roster: a channel of four whose device tokens are all stale answers 201 with
|
|
81
|
+
* `delivered: 0`.
|
|
82
|
+
*
|
|
83
|
+
* A *deduplicated* send is not this error — `delivered` is 0 because the same
|
|
84
|
+
* alert is already open and already ringing, which is success.
|
|
85
|
+
*/
|
|
86
|
+
export declare class M91DeliveryError extends M91Error {
|
|
87
|
+
readonly delivered: number;
|
|
88
|
+
readonly unregistered: string[];
|
|
89
|
+
readonly noDevice: number;
|
|
90
|
+
readonly pending: number;
|
|
91
|
+
constructor(message: string, delivered?: number, unregistered?: string[], noDevice?: number, pending?: number);
|
|
92
|
+
}
|
|
93
|
+
/** `waitForDecision` ran out of time with no SHARED response. Nobody answered. */
|
|
94
|
+
export declare class M91TimeoutError extends M91Error {
|
|
95
|
+
readonly customId: string;
|
|
96
|
+
readonly timeoutMs: number;
|
|
97
|
+
constructor(customId: string, timeoutMs: number);
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Turn one HTTP response into either the payload or a thrown error.
|
|
101
|
+
*
|
|
102
|
+
* **Every M91 route answers in an envelope** — `{ success: true, data }` or
|
|
103
|
+
* `{ success: false, error: { code, message, details } }` — and this is the only
|
|
104
|
+
* place the SDK knows that. Everything above this line sees the inner `data`.
|
|
105
|
+
*
|
|
106
|
+
* The error class is chosen by HTTP status rather than by `error.code`, because
|
|
107
|
+
* the status is what the server derives the code from in the first place;
|
|
108
|
+
* keeping them in step in two places is how they drift apart. The body's `code`
|
|
109
|
+
* is still carried onto the error.
|
|
110
|
+
*/
|
|
111
|
+
export declare function unwrap(status: number, body: unknown, headers?: Headers | Record<string, string>): unknown;
|
|
112
|
+
/** Seconds from `Retry-After`, or undefined. Header lookups are case-insensitive. */
|
|
113
|
+
export declare function retryAfter(headers?: Headers | Record<string, string>): number | undefined;
|
|
114
|
+
//# sourceMappingURL=exceptions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"exceptions.d.ts","sourceRoot":"","sources":["../../src/exceptions.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,qBAAa,QAAS,SAAQ,KAAK;IACjC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;gBAEf,OAAO,EAAE,MAAM,EAAE,IAAI,SAAc,EAAE,UAAU,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO;CAUxF;AAED;;;;;;;GAOG;AACH,qBAAa,kBAAmB,SAAQ,QAAQ;gBAClC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,EAAE,UAAU,SAAM,EAAE,IAAI,SAAqB;CAG5F;AAED,mFAAmF;AACnF,qBAAa,YAAa,SAAQ,QAAQ;gBAC5B,OAAO,SAAiB,EAAE,OAAO,CAAC,EAAE,OAAO;CAGxD;AAED,4EAA4E;AAC5E,qBAAa,iBAAkB,SAAQ,QAAQ;gBACjC,OAAO,SAAc,EAAE,OAAO,CAAC,EAAE,OAAO;CAGrD;AAED;;;GAGG;AACH,qBAAa,gBAAiB,SAAQ,QAAQ;gBAChC,OAAO,SAAc,EAAE,OAAO,CAAC,EAAE,OAAO;CAGrD;AAED;;;;;;GAMG;AACH,qBAAa,oBAAqB,SAAQ,QAAQ;IAChD,QAAQ,CAAC,YAAY,EAAE,MAAM,EAAE,CAAC;gBAEpB,OAAO,EAAE,MAAM,EAAE,YAAY,GAAE,MAAM,EAAO,EAAE,OAAO,CAAC,EAAE,OAAO;CAI5E;AAED;;;;;;GAMG;AACH,qBAAa,iBAAkB,SAAQ,QAAQ;IAC7C,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;gBAEhB,OAAO,SAAiB,EAAE,UAAU,SAAK,EAAE,OAAO,CAAC,EAAE,OAAO;CAIzE;AAED,+EAA+E;AAC/E,qBAAa,cAAe,SAAQ,QAAQ;gBAC9B,OAAO,SAA0B,EAAE,UAAU,SAAM,EAAE,OAAO,CAAC,EAAE,OAAO;CAGnF;AAED;;;;;GAKG;AACH,qBAAa,eAAgB,SAAQ,QAAQ;gBAC/B,OAAO,SAAkB,EAAE,OAAO,CAAC,EAAE,OAAO;CAGzD;AAED;;;;;;;;;;GAUG;AACH,qBAAa,gBAAiB,SAAQ,QAAQ;IAC5C,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,YAAY,EAAE,MAAM,EAAE,CAAC;IAChC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;gBAGvB,OAAO,EAAE,MAAM,EACf,SAAS,SAAI,EACb,YAAY,GAAE,MAAM,EAAO,EAC3B,QAAQ,SAAI,EACZ,OAAO,SAAI;CAQd;AAED,kFAAkF;AAClF,qBAAa,eAAgB,SAAQ,QAAQ;IAC3C,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;gBAEf,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM;CAQhD;AAQD;;;;;;;;;;;GAWG;AACH,wBAAgB,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,OAAO,CA8BzG;AAOD,qFAAqF;AACrF,wBAAgB,UAAU,CAAC,OAAO,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,GAAG,SAAS,CAWzF"}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every failure the SDK can throw, and the one place an HTTP response becomes one.
|
|
3
|
+
*
|
|
4
|
+
* Branch on `.code` — a stable string the API contract pins down — never on
|
|
5
|
+
* `.message`, which is written for a person reading a terminal and is expected
|
|
6
|
+
* to change.
|
|
7
|
+
*/
|
|
8
|
+
export class M91Error extends Error {
|
|
9
|
+
code;
|
|
10
|
+
statusCode;
|
|
11
|
+
details;
|
|
12
|
+
constructor(message, code = 'M91_ERROR', statusCode, details) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.name = new.target.name;
|
|
15
|
+
this.code = code;
|
|
16
|
+
this.statusCode = statusCode;
|
|
17
|
+
this.details = details;
|
|
18
|
+
// Without this, `err instanceof M91ValidationError` is false when the
|
|
19
|
+
// package is compiled down to ES5 by a consumer's bundler.
|
|
20
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* 400/413. The request was wrong, and it is never retried.
|
|
25
|
+
*
|
|
26
|
+
* Also thrown locally, before any HTTP call, when a field breaks a documented
|
|
27
|
+
* limit. Every schema server-side is `.strict()`, so a mistyped field name is a
|
|
28
|
+
* 400 rather than a silent drop — and a round trip to learn that is a round
|
|
29
|
+
* trip wasted.
|
|
30
|
+
*/
|
|
31
|
+
export class M91ValidationError extends M91Error {
|
|
32
|
+
constructor(message, details, statusCode = 400, code = 'VALIDATION_ERROR') {
|
|
33
|
+
super(message, code, statusCode, details);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/** 401. Credential invalid, expired or revoked — nothing is cached server-side. */
|
|
37
|
+
export class M91AuthError extends M91Error {
|
|
38
|
+
constructor(message = 'Unauthorized', details) {
|
|
39
|
+
super(message, 'UNAUTHORIZED', 401, details);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/** 403. Authenticated, but not allowed. On `close()` this means success. */
|
|
43
|
+
export class M91ForbiddenError extends M91Error {
|
|
44
|
+
constructor(message = 'Forbidden', details) {
|
|
45
|
+
super(message, 'FORBIDDEN', 403, details);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* 404. Absent — **or present and not yours**. The API answers the same way to
|
|
50
|
+
* both on purpose, so an id cannot become an oracle for what exists.
|
|
51
|
+
*/
|
|
52
|
+
export class M91NotFoundError extends M91Error {
|
|
53
|
+
constructor(message = 'Not found', details) {
|
|
54
|
+
super(message, 'NOT_FOUND', 404, details);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* 409. The channel has nobody who can actually be woken.
|
|
59
|
+
*
|
|
60
|
+
* `unregistered` is the invited numbers with no M91 account — invited, never
|
|
61
|
+
* installed the app, and unreachable. An ordinary SMS would not pierce Do Not
|
|
62
|
+
* Disturb, would not repeat, and would not escalate.
|
|
63
|
+
*/
|
|
64
|
+
export class M91ChannelEmptyError extends M91Error {
|
|
65
|
+
unregistered;
|
|
66
|
+
constructor(message, unregistered = [], details) {
|
|
67
|
+
super(message, 'CHANNEL_EMPTY', 409, details);
|
|
68
|
+
this.unregistered = unregistered;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* 429. 60 sends a minute per send link.
|
|
73
|
+
*
|
|
74
|
+
* `retryAfter` always has a value: the server sets `Retry-After` on every 429
|
|
75
|
+
* precisely so a caller does not have to guess, and the usual guess is to retry
|
|
76
|
+
* immediately and forever.
|
|
77
|
+
*/
|
|
78
|
+
export class M91RateLimitError extends M91Error {
|
|
79
|
+
retryAfter;
|
|
80
|
+
constructor(message = 'Rate limited', retryAfter = 60, details) {
|
|
81
|
+
super(message, 'RATE_LIMITED', 429, details);
|
|
82
|
+
this.retryAfter = retryAfter;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/** 5xx. Not the caller's fault. Retried with backoff before it reaches you. */
|
|
86
|
+
export class M91ServerError extends M91Error {
|
|
87
|
+
constructor(message = 'Internal server error', statusCode = 500, details) {
|
|
88
|
+
super(message, 'INTERNAL_ERROR', statusCode, details);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* The request never got an answer — DNS, TLS, connect or read timeout.
|
|
93
|
+
*
|
|
94
|
+
* Not retried. A send that timed out may or may not have raised an alert, and
|
|
95
|
+
* retrying it blindly is how one condition wakes a team four times.
|
|
96
|
+
*/
|
|
97
|
+
export class M91NetworkError extends M91Error {
|
|
98
|
+
constructor(message = 'Network error', details) {
|
|
99
|
+
super(message, 'NETWORK_ERROR', undefined, details);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* The API said 2xx and **nobody's phone rang**.
|
|
104
|
+
*
|
|
105
|
+
* The failure mode integrations actually hit, and it is invisible if you only
|
|
106
|
+
* check the status code. `delivered` counts phones sent to, not the size of the
|
|
107
|
+
* roster: a channel of four whose device tokens are all stale answers 201 with
|
|
108
|
+
* `delivered: 0`.
|
|
109
|
+
*
|
|
110
|
+
* A *deduplicated* send is not this error — `delivered` is 0 because the same
|
|
111
|
+
* alert is already open and already ringing, which is success.
|
|
112
|
+
*/
|
|
113
|
+
export class M91DeliveryError extends M91Error {
|
|
114
|
+
delivered;
|
|
115
|
+
unregistered;
|
|
116
|
+
noDevice;
|
|
117
|
+
pending;
|
|
118
|
+
constructor(message, delivered = 0, unregistered = [], noDevice = 0, pending = 0) {
|
|
119
|
+
super(message, 'DELIVERY_FAILED');
|
|
120
|
+
this.delivered = delivered;
|
|
121
|
+
this.unregistered = unregistered;
|
|
122
|
+
this.noDevice = noDevice;
|
|
123
|
+
this.pending = pending;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
/** `waitForDecision` ran out of time with no SHARED response. Nobody answered. */
|
|
127
|
+
export class M91TimeoutError extends M91Error {
|
|
128
|
+
customId;
|
|
129
|
+
timeoutMs;
|
|
130
|
+
constructor(customId, timeoutMs) {
|
|
131
|
+
super(`Nobody answered '${customId}' within ${Math.round(timeoutMs / 1000)}s. The alert is still open.`, 'TIMEOUT');
|
|
132
|
+
this.customId = customId;
|
|
133
|
+
this.timeoutMs = timeoutMs;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Turn one HTTP response into either the payload or a thrown error.
|
|
138
|
+
*
|
|
139
|
+
* **Every M91 route answers in an envelope** — `{ success: true, data }` or
|
|
140
|
+
* `{ success: false, error: { code, message, details } }` — and this is the only
|
|
141
|
+
* place the SDK knows that. Everything above this line sees the inner `data`.
|
|
142
|
+
*
|
|
143
|
+
* The error class is chosen by HTTP status rather than by `error.code`, because
|
|
144
|
+
* the status is what the server derives the code from in the first place;
|
|
145
|
+
* keeping them in step in two places is how they drift apart. The body's `code`
|
|
146
|
+
* is still carried onto the error.
|
|
147
|
+
*/
|
|
148
|
+
export function unwrap(status, body, headers) {
|
|
149
|
+
const envelope = (body ?? {});
|
|
150
|
+
const err = envelope.error ?? {};
|
|
151
|
+
const details = err.details;
|
|
152
|
+
const code = err.code;
|
|
153
|
+
if (status >= 200 && status < 300) {
|
|
154
|
+
if (envelope && typeof envelope === 'object' && 'data' in envelope)
|
|
155
|
+
return envelope.data;
|
|
156
|
+
// A route outside the envelope, or a proxy that rewrote the body. Hand it
|
|
157
|
+
// back rather than inventing a failure the server never reported.
|
|
158
|
+
return body;
|
|
159
|
+
}
|
|
160
|
+
const message = err.message || `M91 request failed with HTTP ${status}`;
|
|
161
|
+
if (status === 409 || code === 'CHANNEL_EMPTY') {
|
|
162
|
+
const unregistered = details?.unregistered ?? [];
|
|
163
|
+
throw new M91ChannelEmptyError(message, unregistered, details);
|
|
164
|
+
}
|
|
165
|
+
if (status === 429)
|
|
166
|
+
throw new M91RateLimitError(message, retryAfter(headers) ?? 60, details);
|
|
167
|
+
if (status === 400)
|
|
168
|
+
throw withCode(new M91ValidationError(message, details), code);
|
|
169
|
+
if (status === 413) {
|
|
170
|
+
throw new M91ValidationError(message, details, 413, code || 'PAYLOAD_TOO_LARGE');
|
|
171
|
+
}
|
|
172
|
+
if (status === 401)
|
|
173
|
+
throw withCode(new M91AuthError(message, details), code);
|
|
174
|
+
if (status === 403)
|
|
175
|
+
throw withCode(new M91ForbiddenError(message, details), code);
|
|
176
|
+
if (status === 404)
|
|
177
|
+
throw withCode(new M91NotFoundError(message, details), code);
|
|
178
|
+
if (status >= 500)
|
|
179
|
+
throw new M91ServerError(message, status, details);
|
|
180
|
+
throw new M91Error(message, code || 'M91_ERROR', status, details);
|
|
181
|
+
}
|
|
182
|
+
function withCode(error, code) {
|
|
183
|
+
if (code)
|
|
184
|
+
error.code = code;
|
|
185
|
+
return error;
|
|
186
|
+
}
|
|
187
|
+
/** Seconds from `Retry-After`, or undefined. Header lookups are case-insensitive. */
|
|
188
|
+
export function retryAfter(headers) {
|
|
189
|
+
if (!headers)
|
|
190
|
+
return undefined;
|
|
191
|
+
const raw = typeof headers.get === 'function'
|
|
192
|
+
? headers.get('retry-after')
|
|
193
|
+
: Object.entries(headers).find(([k]) => k.toLowerCase() === 'retry-after')?.[1];
|
|
194
|
+
if (raw === null || raw === undefined)
|
|
195
|
+
return undefined;
|
|
196
|
+
const seconds = Number(String(raw).trim());
|
|
197
|
+
return Number.isFinite(seconds) ? Math.max(0, Math.trunc(seconds)) : undefined;
|
|
198
|
+
}
|
|
199
|
+
//# sourceMappingURL=exceptions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"exceptions.js","sourceRoot":"","sources":["../../src/exceptions.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP on native `fetch`, with the retry policy applied. Zero dependencies.
|
|
3
|
+
*
|
|
4
|
+
* The rules, which are the interesting part:
|
|
5
|
+
*
|
|
6
|
+
* - **429 is retried exactly once**, after exactly the delay the server asked
|
|
7
|
+
* for. The send limit is 60/minute per link and every 429 carries
|
|
8
|
+
* `Retry-After: 60`. Retrying a second time would be arguing with it.
|
|
9
|
+
* - **5xx is retried with backoff.** Not the caller's fault, and the alert has
|
|
10
|
+
* not been raised, so repeating it cannot double-page anybody.
|
|
11
|
+
* - **4xx is never retried.** Not transient; the next identical request fails
|
|
12
|
+
* identically.
|
|
13
|
+
* - **A network failure is never retried.** A send that timed out may or may not
|
|
14
|
+
* have raised the alert — the response is what was lost, not necessarily the
|
|
15
|
+
* request. Blind retries are how one condition wakes a team four times.
|
|
16
|
+
*/
|
|
17
|
+
export declare const USER_AGENT = "m91-node/1.0.0";
|
|
18
|
+
/** Delays before the 2nd, 3rd and 4th attempt at a 5xx. Four requests total. */
|
|
19
|
+
export declare const SERVER_BACKOFF: readonly [1000, 2000, 4000];
|
|
20
|
+
export declare const RETRYABLE_STATUSES: Set<number>;
|
|
21
|
+
/**
|
|
22
|
+
* A 429 whose `Retry-After` is longer than this is thrown rather than slept
|
|
23
|
+
* through. The server sends 60; anything far past that is a caller sitting
|
|
24
|
+
* blocked in an alerting path, which is worse than an error it can see.
|
|
25
|
+
*/
|
|
26
|
+
export declare const MAX_RETRY_AFTER_MS = 120000;
|
|
27
|
+
/**
|
|
28
|
+
* Milliseconds to wait before trying again, or `null` to stop and throw.
|
|
29
|
+
* `attempt` is 1-based and counts requests already made.
|
|
30
|
+
*/
|
|
31
|
+
export declare function retryDelay(status: number, attempt: number, retryAfterSeconds?: number): number | null;
|
|
32
|
+
/**
|
|
33
|
+
* Replace a send link's token with `***`, wherever it appears.
|
|
34
|
+
*
|
|
35
|
+
* **The token is the credential and it lives in the path**, so a URL is as
|
|
36
|
+
* sensitive as a password and must never reach a log, an error message or a
|
|
37
|
+
* stack trace intact. The server holds itself to the same rule — its error
|
|
38
|
+
* handler reports the route *pattern*, never `req.path`.
|
|
39
|
+
*
|
|
40
|
+
* This takes free text, not only a URL, and that is the point. Redacting only
|
|
41
|
+
* the URL the SDK interpolates is not enough: a fetch failure can carry the URL
|
|
42
|
+
* in its own message, so a carefully-redacted sentence would arrive with the
|
|
43
|
+
* raw token attached to it by the runtime.
|
|
44
|
+
*
|
|
45
|
+
* Query strings go too: a GET-form send carries the alert title, and titles
|
|
46
|
+
* routinely name customers and hosts.
|
|
47
|
+
*/
|
|
48
|
+
export declare function redact(text: string): string;
|
|
49
|
+
export interface RequestOptions {
|
|
50
|
+
method: string;
|
|
51
|
+
url: string;
|
|
52
|
+
body?: unknown;
|
|
53
|
+
query?: Record<string, string>;
|
|
54
|
+
}
|
|
55
|
+
export interface TransportOptions {
|
|
56
|
+
/** Whole-request budget in milliseconds. Default 15s (5s connect + 10s read). */
|
|
57
|
+
timeoutMs?: number;
|
|
58
|
+
headers?: Record<string, string>;
|
|
59
|
+
/** Swappable so tests need no network and no mocking library. */
|
|
60
|
+
fetchImpl?: typeof fetch;
|
|
61
|
+
sleepImpl?: (ms: number) => Promise<void>;
|
|
62
|
+
}
|
|
63
|
+
export declare class Transport {
|
|
64
|
+
private readonly timeoutMs;
|
|
65
|
+
private readonly headers;
|
|
66
|
+
private readonly fetchImpl;
|
|
67
|
+
private readonly sleepImpl;
|
|
68
|
+
constructor(options?: TransportOptions);
|
|
69
|
+
request({ method, url, body, query }: RequestOptions): Promise<unknown>;
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=http.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../../src/http.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAIH,eAAO,MAAM,UAAU,mBAAmB,CAAC;AAE3C,gFAAgF;AAChF,eAAO,MAAM,cAAc,6BAA8B,CAAC;AAE1D,eAAO,MAAM,kBAAkB,aAAgC,CAAC;AAEhE;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,SAAU,CAAC;AAE1C;;;GAGG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,iBAAiB,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAYrG;AASD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAG3C;AAED,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAChC;AAED,MAAM,WAAW,gBAAgB;IAC/B,iFAAiF;IACjF,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,iEAAiE;IACjE,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IACzB,SAAS,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CAC3C;AAID,qBAAa,SAAS;IACpB,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAyB;IACjD,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAe;IACzC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAgC;gBAE9C,OAAO,GAAE,gBAAqB;IAepC,OAAO,CAAC,EAAE,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,KAAK,EAAE,EAAE,cAAc,GAAG,OAAO,CAAC,OAAO,CAAC;CAsC9E"}
|