@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.
Files changed (70) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/LICENSE +21 -0
  3. package/README.md +425 -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/index.d.ts +22 -0
  21. package/dist/src/integrations/index.d.ts.map +1 -0
  22. package/dist/src/integrations/index.js +21 -0
  23. package/dist/src/integrations/index.js.map +1 -0
  24. package/dist/src/integrations/langchain.d.ts +43 -0
  25. package/dist/src/integrations/langchain.d.ts.map +1 -0
  26. package/dist/src/integrations/langchain.js +75 -0
  27. package/dist/src/integrations/langchain.js.map +1 -0
  28. package/dist/src/integrations/langgraph.d.ts +79 -0
  29. package/dist/src/integrations/langgraph.d.ts.map +1 -0
  30. package/dist/src/integrations/langgraph.js +114 -0
  31. package/dist/src/integrations/langgraph.js.map +1 -0
  32. package/dist/src/integrations/specs.d.ts +131 -0
  33. package/dist/src/integrations/specs.d.ts.map +1 -0
  34. package/dist/src/integrations/specs.js +156 -0
  35. package/dist/src/integrations/specs.js.map +1 -0
  36. package/dist/src/integrations/vercel-ai.d.ts +36 -0
  37. package/dist/src/integrations/vercel-ai.d.ts.map +1 -0
  38. package/dist/src/integrations/vercel-ai.js +67 -0
  39. package/dist/src/integrations/vercel-ai.js.map +1 -0
  40. package/dist/src/models.d.ts +274 -0
  41. package/dist/src/models.d.ts.map +1 -0
  42. package/dist/src/models.js +383 -0
  43. package/dist/src/models.js.map +1 -0
  44. package/dist/src/platform.d.ts +176 -0
  45. package/dist/src/platform.d.ts.map +1 -0
  46. package/dist/src/platform.js +286 -0
  47. package/dist/src/platform.js.map +1 -0
  48. package/dist/src/url-builder.d.ts +65 -0
  49. package/dist/src/url-builder.d.ts.map +1 -0
  50. package/dist/src/url-builder.js +167 -0
  51. package/dist/src/url-builder.js.map +1 -0
  52. package/examples/approval-gate.ts +35 -0
  53. package/examples/basic-alert.ts +53 -0
  54. package/examples/langchain-agent.ts +65 -0
  55. package/examples/langgraph-hitl.ts +70 -0
  56. package/examples/vercel-ai-agent.ts +34 -0
  57. package/examples/webhook-urls.ts +44 -0
  58. package/package.json +83 -4
  59. package/src/client.ts +324 -0
  60. package/src/exceptions.ts +229 -0
  61. package/src/http.ts +174 -0
  62. package/src/index.ts +91 -0
  63. package/src/integrations/index.ts +23 -0
  64. package/src/integrations/langchain.ts +104 -0
  65. package/src/integrations/langgraph.ts +142 -0
  66. package/src/integrations/specs.ts +224 -0
  67. package/src/integrations/vercel-ai.ts +99 -0
  68. package/src/models.ts +660 -0
  69. package/src/platform.ts +343 -0
  70. 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"}