@ariaflowagents/messaging-meta 0.8.1
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/README.md +236 -0
- package/dist/graph-api/client.d.ts +125 -0
- package/dist/graph-api/client.d.ts.map +1 -0
- package/dist/graph-api/client.js +204 -0
- package/dist/graph-api/client.js.map +1 -0
- package/dist/graph-api/errors.d.ts +41 -0
- package/dist/graph-api/errors.d.ts.map +1 -0
- package/dist/graph-api/errors.js +72 -0
- package/dist/graph-api/errors.js.map +1 -0
- package/dist/graph-api/index.d.ts +15 -0
- package/dist/graph-api/index.d.ts.map +1 -0
- package/dist/graph-api/index.js +11 -0
- package/dist/graph-api/index.js.map +1 -0
- package/dist/graph-api/rate-limiter.d.ts +90 -0
- package/dist/graph-api/rate-limiter.d.ts.map +1 -0
- package/dist/graph-api/rate-limiter.js +172 -0
- package/dist/graph-api/rate-limiter.js.map +1 -0
- package/dist/graph-api/retry.d.ts +55 -0
- package/dist/graph-api/retry.d.ts.map +1 -0
- package/dist/graph-api/retry.js +103 -0
- package/dist/graph-api/retry.js.map +1 -0
- package/dist/index.d.ts +36 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +38 -0
- package/dist/index.js.map +1 -0
- package/dist/instagram/client.d.ts +365 -0
- package/dist/instagram/client.d.ts.map +1 -0
- package/dist/instagram/client.js +857 -0
- package/dist/instagram/client.js.map +1 -0
- package/dist/instagram/format.d.ts +62 -0
- package/dist/instagram/format.d.ts.map +1 -0
- package/dist/instagram/format.js +92 -0
- package/dist/instagram/format.js.map +1 -0
- package/dist/instagram/ice-breakers.d.ts +63 -0
- package/dist/instagram/ice-breakers.d.ts.map +1 -0
- package/dist/instagram/ice-breakers.js +87 -0
- package/dist/instagram/ice-breakers.js.map +1 -0
- package/dist/instagram/index.d.ts +44 -0
- package/dist/instagram/index.d.ts.map +1 -0
- package/dist/instagram/index.js +46 -0
- package/dist/instagram/index.js.map +1 -0
- package/dist/instagram/types.d.ts +188 -0
- package/dist/instagram/types.d.ts.map +1 -0
- package/dist/instagram/types.js +19 -0
- package/dist/instagram/types.js.map +1 -0
- package/dist/messenger/client.d.ts +339 -0
- package/dist/messenger/client.d.ts.map +1 -0
- package/dist/messenger/client.js +782 -0
- package/dist/messenger/client.js.map +1 -0
- package/dist/messenger/format.d.ts +69 -0
- package/dist/messenger/format.d.ts.map +1 -0
- package/dist/messenger/format.js +98 -0
- package/dist/messenger/format.js.map +1 -0
- package/dist/messenger/index.d.ts +34 -0
- package/dist/messenger/index.d.ts.map +1 -0
- package/dist/messenger/index.js +35 -0
- package/dist/messenger/index.js.map +1 -0
- package/dist/messenger/types.d.ts +181 -0
- package/dist/messenger/types.d.ts.map +1 -0
- package/dist/messenger/types.js +10 -0
- package/dist/messenger/types.js.map +1 -0
- package/dist/server.d.ts +31 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +29 -0
- package/dist/server.js.map +1 -0
- package/dist/webhook/index.d.ts +10 -0
- package/dist/webhook/index.d.ts.map +1 -0
- package/dist/webhook/index.js +8 -0
- package/dist/webhook/index.js.map +1 -0
- package/dist/webhook/normalizer.d.ts +169 -0
- package/dist/webhook/normalizer.d.ts.map +1 -0
- package/dist/webhook/normalizer.js +301 -0
- package/dist/webhook/normalizer.js.map +1 -0
- package/dist/webhook/verifier.d.ts +45 -0
- package/dist/webhook/verifier.d.ts.map +1 -0
- package/dist/webhook/verifier.js +62 -0
- package/dist/webhook/verifier.js.map +1 -0
- package/dist/whatsapp/client.d.ts +481 -0
- package/dist/whatsapp/client.d.ts.map +1 -0
- package/dist/whatsapp/client.js +1043 -0
- package/dist/whatsapp/client.js.map +1 -0
- package/dist/whatsapp/flows.d.ts +74 -0
- package/dist/whatsapp/flows.d.ts.map +1 -0
- package/dist/whatsapp/flows.js +77 -0
- package/dist/whatsapp/flows.js.map +1 -0
- package/dist/whatsapp/format.d.ts +78 -0
- package/dist/whatsapp/format.d.ts.map +1 -0
- package/dist/whatsapp/format.js +195 -0
- package/dist/whatsapp/format.js.map +1 -0
- package/dist/whatsapp/index.d.ts +39 -0
- package/dist/whatsapp/index.d.ts.map +1 -0
- package/dist/whatsapp/index.js +42 -0
- package/dist/whatsapp/index.js.map +1 -0
- package/dist/whatsapp/split.d.ts +35 -0
- package/dist/whatsapp/split.d.ts.map +1 -0
- package/dist/whatsapp/split.js +76 -0
- package/dist/whatsapp/split.js.map +1 -0
- package/dist/whatsapp/templates.d.ts +129 -0
- package/dist/whatsapp/templates.d.ts.map +1 -0
- package/dist/whatsapp/templates.js +125 -0
- package/dist/whatsapp/templates.js.map +1 -0
- package/dist/whatsapp/types.d.ts +440 -0
- package/dist/whatsapp/types.d.ts.map +1 -0
- package/dist/whatsapp/types.js +11 -0
- package/dist/whatsapp/types.js.map +1 -0
- package/package.json +31 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module graph-api/errors
|
|
3
|
+
*
|
|
4
|
+
* Meta-specific error classification helper.
|
|
5
|
+
*
|
|
6
|
+
* Imports the canonical error hierarchy from `@ariaflowagents/messaging` and
|
|
7
|
+
* re-exports it alongside the {@link classifyMetaError} function. This ensures
|
|
8
|
+
* that `instanceof` checks work correctly across the entire SDK — there is only
|
|
9
|
+
* ONE `WindowClosedError` class, ONE `RateLimitError` class, etc.
|
|
10
|
+
*/
|
|
11
|
+
import { MessagingError, RateLimitError, AuthenticationError, PermissionError, RecipientError, WindowClosedError, TemplateError, MediaError, WebhookVerificationError, } from '@ariaflowagents/messaging';
|
|
12
|
+
// Re-export so consumers of this module can access the error classes.
|
|
13
|
+
export { MessagingError, RateLimitError, AuthenticationError, PermissionError, RecipientError, WindowClosedError, TemplateError, MediaError, WebhookVerificationError, };
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Classification
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
/**
|
|
18
|
+
* Classify a Meta Graph API error response into a typed {@link MessagingError}.
|
|
19
|
+
*
|
|
20
|
+
* The mapping follows Meta's documented error codes:
|
|
21
|
+
* - 429 / codes 4, 32, 613 -> {@link RateLimitError}
|
|
22
|
+
* - 401 / code 190 -> {@link AuthenticationError}
|
|
23
|
+
* - 403 / codes 10, 200 -> {@link PermissionError}
|
|
24
|
+
* - 404 -> {@link RecipientError}
|
|
25
|
+
* - code 131047 -> {@link WindowClosedError} (24-hour window expired)
|
|
26
|
+
* - code 131026 -> {@link RecipientError} (not on WhatsApp)
|
|
27
|
+
* - codes 132000-132999 -> {@link TemplateError}
|
|
28
|
+
*
|
|
29
|
+
* @param status - HTTP status code from the response.
|
|
30
|
+
* @param body - Parsed JSON body (may be `null` if parsing failed).
|
|
31
|
+
* @param platform - Platform identifier (e.g. `"whatsapp"`, `"messenger"`).
|
|
32
|
+
* @returns A typed {@link MessagingError} subclass.
|
|
33
|
+
*/
|
|
34
|
+
export function classifyMetaError(status, body, platform) {
|
|
35
|
+
const parsed = body;
|
|
36
|
+
const err = parsed?.error;
|
|
37
|
+
const metaCode = err?.code;
|
|
38
|
+
const metaSubcode = err?.error_subcode;
|
|
39
|
+
const _fbtraceId = err?.fbtrace_id;
|
|
40
|
+
const metaMessage = err?.message ?? `Meta API error (HTTP ${status})`;
|
|
41
|
+
// --- Rate limiting (HTTP 429 or Meta codes 4, 32, 613) ---
|
|
42
|
+
if (status === 429 || metaCode === 4 || metaCode === 32 || metaCode === 613) {
|
|
43
|
+
return new RateLimitError(`Rate limited — retry after 5000ms`, platform, 5_000);
|
|
44
|
+
}
|
|
45
|
+
// --- Authentication (HTTP 401 or Meta code 190) ---
|
|
46
|
+
if (status === 401 || metaCode === 190) {
|
|
47
|
+
return new AuthenticationError(metaMessage, platform);
|
|
48
|
+
}
|
|
49
|
+
// --- Permissions (HTTP 403 or Meta codes 10, 200) ---
|
|
50
|
+
if (status === 403 || metaCode === 10 || metaCode === 200) {
|
|
51
|
+
return new PermissionError(metaMessage, platform);
|
|
52
|
+
}
|
|
53
|
+
// --- 24-hour window closed (Meta code 131047) ---
|
|
54
|
+
if (metaCode === 131047) {
|
|
55
|
+
return new WindowClosedError(metaMessage, platform, new Date());
|
|
56
|
+
}
|
|
57
|
+
// --- Recipient not reachable (HTTP 404 or Meta code 131026) ---
|
|
58
|
+
if (status === 404 || metaCode === 131026) {
|
|
59
|
+
return new RecipientError(metaMessage, platform);
|
|
60
|
+
}
|
|
61
|
+
// --- Template errors (Meta codes 132000-132999) ---
|
|
62
|
+
if (metaCode !== undefined && metaCode >= 132000 && metaCode <= 132999) {
|
|
63
|
+
return new TemplateError(metaMessage, platform);
|
|
64
|
+
}
|
|
65
|
+
// --- Template sub-code errors (some come through subcode) ---
|
|
66
|
+
if (metaSubcode !== undefined && metaSubcode >= 132000 && metaSubcode <= 132999) {
|
|
67
|
+
return new TemplateError(metaMessage, platform);
|
|
68
|
+
}
|
|
69
|
+
// --- Fallback ---
|
|
70
|
+
return new MessagingError(metaMessage, `meta_error_${metaCode ?? status}`, platform);
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/graph-api/errors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EACL,cAAc,EACd,cAAc,EACd,mBAAmB,EACnB,eAAe,EACf,cAAc,EACd,iBAAiB,EACjB,aAAa,EACb,UAAU,EACV,wBAAwB,GACzB,MAAM,2BAA2B,CAAC;AAEnC,sEAAsE;AACtE,OAAO,EACL,cAAc,EACd,cAAc,EACd,mBAAmB,EACnB,eAAe,EACf,cAAc,EACd,iBAAiB,EACjB,aAAa,EACb,UAAU,EACV,wBAAwB,GACzB,CAAC;AAiBF,8EAA8E;AAC9E,iBAAiB;AACjB,8EAA8E;AAE9E;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,iBAAiB,CAC/B,MAAc,EACd,IAAa,EACb,QAAgB;IAEhB,MAAM,MAAM,GAAG,IAAgC,CAAC;IAChD,MAAM,GAAG,GAAG,MAAM,EAAE,KAAK,CAAC;IAC1B,MAAM,QAAQ,GAAG,GAAG,EAAE,IAAI,CAAC;IAC3B,MAAM,WAAW,GAAG,GAAG,EAAE,aAAa,CAAC;IACvC,MAAM,UAAU,GAAG,GAAG,EAAE,UAAU,CAAC;IACnC,MAAM,WAAW,GAAG,GAAG,EAAE,OAAO,IAAI,wBAAwB,MAAM,GAAG,CAAC;IAEtE,4DAA4D;IAC5D,IAAI,MAAM,KAAK,GAAG,IAAI,QAAQ,KAAK,CAAC,IAAI,QAAQ,KAAK,EAAE,IAAI,QAAQ,KAAK,GAAG,EAAE,CAAC;QAC5E,OAAO,IAAI,cAAc,CAAC,mCAAmC,EAAE,QAAQ,EAAE,KAAK,CAAC,CAAC;IAClF,CAAC;IAED,qDAAqD;IACrD,IAAI,MAAM,KAAK,GAAG,IAAI,QAAQ,KAAK,GAAG,EAAE,CAAC;QACvC,OAAO,IAAI,mBAAmB,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAC;IACxD,CAAC;IAED,uDAAuD;IACvD,IAAI,MAAM,KAAK,GAAG,IAAI,QAAQ,KAAK,EAAE,IAAI,QAAQ,KAAK,GAAG,EAAE,CAAC;QAC1D,OAAO,IAAI,eAAe,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAC;IACpD,CAAC;IAED,mDAAmD;IACnD,IAAI,QAAQ,KAAK,MAAM,EAAE,CAAC;QACxB,OAAO,IAAI,iBAAiB,CAAC,WAAW,EAAE,QAAQ,EAAE,IAAI,IAAI,EAAE,CAAC,CAAC;IAClE,CAAC;IAED,iEAAiE;IACjE,IAAI,MAAM,KAAK,GAAG,IAAI,QAAQ,KAAK,MAAM,EAAE,CAAC;QAC1C,OAAO,IAAI,cAAc,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAC;IACnD,CAAC;IAED,qDAAqD;IACrD,IAAI,QAAQ,KAAK,SAAS,IAAI,QAAQ,IAAI,MAAM,IAAI,QAAQ,IAAI,MAAM,EAAE,CAAC;QACvE,OAAO,IAAI,aAAa,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAC;IAClD,CAAC;IAED,+DAA+D;IAC/D,IAAI,WAAW,KAAK,SAAS,IAAI,WAAW,IAAI,MAAM,IAAI,WAAW,IAAI,MAAM,EAAE,CAAC;QAChF,OAAO,IAAI,aAAa,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAC;IAClD,CAAC;IAED,mBAAmB;IACnB,OAAO,IAAI,cAAc,CAAC,WAAW,EAAE,cAAc,QAAQ,IAAI,MAAM,EAAE,EAAE,QAAQ,CAAC,CAAC;AACvF,CAAC"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module graph-api
|
|
3
|
+
*
|
|
4
|
+
* Graph API sub-module — typed HTTP client, retry queue, rate limiter,
|
|
5
|
+
* and Meta-specific error classification.
|
|
6
|
+
*/
|
|
7
|
+
export { GraphAPIClient } from './client.js';
|
|
8
|
+
export type { GraphAPIClientConfig, Logger } from './client.js';
|
|
9
|
+
export { RetryQueue, DEFAULT_RETRY_CONFIG } from './retry.js';
|
|
10
|
+
export type { RetryConfig } from './retry.js';
|
|
11
|
+
export { RateLimiter, DEFAULT_RATE_LIMITER_CONFIG } from './rate-limiter.js';
|
|
12
|
+
export type { RateLimiterConfig } from './rate-limiter.js';
|
|
13
|
+
export { classifyMetaError, MessagingError, RateLimitError, AuthenticationError, PermissionError, RecipientError, WindowClosedError, TemplateError, MediaError, WebhookVerificationError, } from './errors.js';
|
|
14
|
+
export type { MetaErrorResponse } from './errors.js';
|
|
15
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/graph-api/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAC7C,YAAY,EAAE,oBAAoB,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAEhE,OAAO,EAAE,UAAU,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAC9D,YAAY,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C,OAAO,EAAE,WAAW,EAAE,2BAA2B,EAAE,MAAM,mBAAmB,CAAC;AAC7E,YAAY,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAE3D,OAAO,EACL,iBAAiB,EACjB,cAAc,EACd,cAAc,EACd,mBAAmB,EACnB,eAAe,EACf,cAAc,EACd,iBAAiB,EACjB,aAAa,EACb,UAAU,EACV,wBAAwB,GACzB,MAAM,aAAa,CAAC;AACrB,YAAY,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module graph-api
|
|
3
|
+
*
|
|
4
|
+
* Graph API sub-module — typed HTTP client, retry queue, rate limiter,
|
|
5
|
+
* and Meta-specific error classification.
|
|
6
|
+
*/
|
|
7
|
+
export { GraphAPIClient } from './client.js';
|
|
8
|
+
export { RetryQueue, DEFAULT_RETRY_CONFIG } from './retry.js';
|
|
9
|
+
export { RateLimiter, DEFAULT_RATE_LIMITER_CONFIG } from './rate-limiter.js';
|
|
10
|
+
export { classifyMetaError, MessagingError, RateLimitError, AuthenticationError, PermissionError, RecipientError, WindowClosedError, TemplateError, MediaError, WebhookVerificationError, } from './errors.js';
|
|
11
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/graph-api/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAG7C,OAAO,EAAE,UAAU,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAG9D,OAAO,EAAE,WAAW,EAAE,2BAA2B,EAAE,MAAM,mBAAmB,CAAC;AAG7E,OAAO,EACL,iBAAiB,EACjB,cAAc,EACd,cAAc,EACd,mBAAmB,EACnB,eAAe,EACf,cAAc,EACd,iBAAiB,EACjB,aAAa,EACb,UAAU,EACV,wBAAwB,GACzB,MAAM,aAAa,CAAC"}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module graph-api/rate-limiter
|
|
3
|
+
*
|
|
4
|
+
* Token-bucket rate limiter with concurrency control for the Meta Graph API.
|
|
5
|
+
*
|
|
6
|
+
* The limiter enforces two constraints simultaneously:
|
|
7
|
+
* 1. **Per-second throughput** — a token bucket that refills at `perSecondLimit` tokens/s.
|
|
8
|
+
* 2. **Concurrent requests** — at most `maxConcurrent` in-flight requests.
|
|
9
|
+
*
|
|
10
|
+
* It also parses Meta's `x-business-use-case-usage` and `x-app-usage` response
|
|
11
|
+
* headers to dynamically throttle when the account is approaching platform limits.
|
|
12
|
+
*/
|
|
13
|
+
/** Rate limiter configuration. */
|
|
14
|
+
export interface RateLimiterConfig {
|
|
15
|
+
/** Maximum number of concurrent in-flight requests. Default `40`. */
|
|
16
|
+
maxConcurrent: number;
|
|
17
|
+
/** Maximum number of requests per second (token refill rate). Default `80`. */
|
|
18
|
+
perSecondLimit: number;
|
|
19
|
+
}
|
|
20
|
+
/** Sensible defaults for Meta's Cloud API (Business tier). */
|
|
21
|
+
export declare const DEFAULT_RATE_LIMITER_CONFIG: RateLimiterConfig;
|
|
22
|
+
/**
|
|
23
|
+
* Token-bucket rate limiter with concurrency gating.
|
|
24
|
+
*
|
|
25
|
+
* Call {@link acquire} before making a request and {@link release} when the
|
|
26
|
+
* response has been fully consumed. The limiter will block callers with a
|
|
27
|
+
* promise when either the token bucket is empty or the concurrency cap is hit.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```ts
|
|
31
|
+
* const limiter = new RateLimiter();
|
|
32
|
+
* await limiter.acquire();
|
|
33
|
+
* try {
|
|
34
|
+
* const res = await fetch(url);
|
|
35
|
+
* limiter.updateFromHeaders(res.headers);
|
|
36
|
+
* return await res.json();
|
|
37
|
+
* } finally {
|
|
38
|
+
* limiter.release();
|
|
39
|
+
* }
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
export declare class RateLimiter {
|
|
43
|
+
private readonly config;
|
|
44
|
+
/** Number of requests currently in-flight. */
|
|
45
|
+
private concurrent;
|
|
46
|
+
/** Available tokens in the bucket. */
|
|
47
|
+
private tokens;
|
|
48
|
+
/** Timestamp (ms) of the last token refill. */
|
|
49
|
+
private lastRefill;
|
|
50
|
+
/** FIFO queue of callers waiting for capacity. */
|
|
51
|
+
private waitQueue;
|
|
52
|
+
/**
|
|
53
|
+
* When Meta headers indicate high usage (>80 %), we temporarily halve
|
|
54
|
+
* the effective per-second limit to back off gracefully.
|
|
55
|
+
*/
|
|
56
|
+
private throttled;
|
|
57
|
+
constructor(config?: Partial<RateLimiterConfig>);
|
|
58
|
+
/**
|
|
59
|
+
* Wait until a request slot is available.
|
|
60
|
+
*
|
|
61
|
+
* Resolves immediately if both a token and a concurrency slot are free.
|
|
62
|
+
* Otherwise the caller is queued and the returned promise settles when
|
|
63
|
+
* capacity becomes available.
|
|
64
|
+
*/
|
|
65
|
+
acquire(): Promise<void>;
|
|
66
|
+
/**
|
|
67
|
+
* Release a concurrency slot after a request completes.
|
|
68
|
+
*
|
|
69
|
+
* Must be called exactly once for every successful {@link acquire}.
|
|
70
|
+
*/
|
|
71
|
+
release(): void;
|
|
72
|
+
/**
|
|
73
|
+
* Parse Meta usage headers and enable throttling when approaching limits.
|
|
74
|
+
*
|
|
75
|
+
* Meta returns usage information in two headers:
|
|
76
|
+
* - `x-app-usage` — app-level call/CPU/memory percentages.
|
|
77
|
+
* - `x-business-use-case-usage` — per-WABA usage buckets (WhatsApp).
|
|
78
|
+
*
|
|
79
|
+
* If any reported percentage exceeds 80 %, the limiter enters a throttled
|
|
80
|
+
* state that halves the effective token refill rate.
|
|
81
|
+
*
|
|
82
|
+
* @param headers - Response headers from a Graph API call.
|
|
83
|
+
*/
|
|
84
|
+
updateFromHeaders(headers: Headers): void;
|
|
85
|
+
/** Refill tokens based on elapsed wall-clock time. */
|
|
86
|
+
private refillTokens;
|
|
87
|
+
/** Attempt to wake queued callers if capacity has become available. */
|
|
88
|
+
private drain;
|
|
89
|
+
}
|
|
90
|
+
//# sourceMappingURL=rate-limiter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rate-limiter.d.ts","sourceRoot":"","sources":["../../src/graph-api/rate-limiter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAMH,kCAAkC;AAClC,MAAM,WAAW,iBAAiB;IAChC,qEAAqE;IACrE,aAAa,EAAE,MAAM,CAAC;IACtB,+EAA+E;IAC/E,cAAc,EAAE,MAAM,CAAC;CACxB;AAED,8DAA8D;AAC9D,eAAO,MAAM,2BAA2B,EAAE,iBAGzC,CAAC;AAcF;;;;;;;;;;;;;;;;;;;GAmBG;AACH,qBAAa,WAAW;IACtB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAoB;IAE3C,8CAA8C;IAC9C,OAAO,CAAC,UAAU,CAAK;IAEvB,sCAAsC;IACtC,OAAO,CAAC,MAAM,CAAS;IAEvB,+CAA+C;IAC/C,OAAO,CAAC,UAAU,CAAS;IAE3B,kDAAkD;IAClD,OAAO,CAAC,SAAS,CAAuB;IAExC;;;OAGG;IACH,OAAO,CAAC,SAAS,CAAS;gBAEd,MAAM,GAAE,OAAO,CAAC,iBAAiB,CAAM;IAUnD;;;;;;OAMG;IACG,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAyB9B;;;;OAIG;IACH,OAAO,IAAI,IAAI;IAKf;;;;;;;;;;;OAWG;IACH,iBAAiB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI;IAyCzC,sDAAsD;IACtD,OAAO,CAAC,YAAY;IAWpB,uEAAuE;IACvE,OAAO,CAAC,KAAK;CAcd"}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module graph-api/rate-limiter
|
|
3
|
+
*
|
|
4
|
+
* Token-bucket rate limiter with concurrency control for the Meta Graph API.
|
|
5
|
+
*
|
|
6
|
+
* The limiter enforces two constraints simultaneously:
|
|
7
|
+
* 1. **Per-second throughput** — a token bucket that refills at `perSecondLimit` tokens/s.
|
|
8
|
+
* 2. **Concurrent requests** — at most `maxConcurrent` in-flight requests.
|
|
9
|
+
*
|
|
10
|
+
* It also parses Meta's `x-business-use-case-usage` and `x-app-usage` response
|
|
11
|
+
* headers to dynamically throttle when the account is approaching platform limits.
|
|
12
|
+
*/
|
|
13
|
+
/** Sensible defaults for Meta's Cloud API (Business tier). */
|
|
14
|
+
export const DEFAULT_RATE_LIMITER_CONFIG = {
|
|
15
|
+
maxConcurrent: 40,
|
|
16
|
+
perSecondLimit: 80,
|
|
17
|
+
};
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// RateLimiter
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
/**
|
|
22
|
+
* Token-bucket rate limiter with concurrency gating.
|
|
23
|
+
*
|
|
24
|
+
* Call {@link acquire} before making a request and {@link release} when the
|
|
25
|
+
* response has been fully consumed. The limiter will block callers with a
|
|
26
|
+
* promise when either the token bucket is empty or the concurrency cap is hit.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```ts
|
|
30
|
+
* const limiter = new RateLimiter();
|
|
31
|
+
* await limiter.acquire();
|
|
32
|
+
* try {
|
|
33
|
+
* const res = await fetch(url);
|
|
34
|
+
* limiter.updateFromHeaders(res.headers);
|
|
35
|
+
* return await res.json();
|
|
36
|
+
* } finally {
|
|
37
|
+
* limiter.release();
|
|
38
|
+
* }
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
export class RateLimiter {
|
|
42
|
+
config;
|
|
43
|
+
/** Number of requests currently in-flight. */
|
|
44
|
+
concurrent = 0;
|
|
45
|
+
/** Available tokens in the bucket. */
|
|
46
|
+
tokens;
|
|
47
|
+
/** Timestamp (ms) of the last token refill. */
|
|
48
|
+
lastRefill;
|
|
49
|
+
/** FIFO queue of callers waiting for capacity. */
|
|
50
|
+
waitQueue = [];
|
|
51
|
+
/**
|
|
52
|
+
* When Meta headers indicate high usage (>80 %), we temporarily halve
|
|
53
|
+
* the effective per-second limit to back off gracefully.
|
|
54
|
+
*/
|
|
55
|
+
throttled = false;
|
|
56
|
+
constructor(config = {}) {
|
|
57
|
+
this.config = { ...DEFAULT_RATE_LIMITER_CONFIG, ...config };
|
|
58
|
+
this.tokens = this.config.perSecondLimit;
|
|
59
|
+
this.lastRefill = Date.now();
|
|
60
|
+
}
|
|
61
|
+
// -----------------------------------------------------------------------
|
|
62
|
+
// Public API
|
|
63
|
+
// -----------------------------------------------------------------------
|
|
64
|
+
/**
|
|
65
|
+
* Wait until a request slot is available.
|
|
66
|
+
*
|
|
67
|
+
* Resolves immediately if both a token and a concurrency slot are free.
|
|
68
|
+
* Otherwise the caller is queued and the returned promise settles when
|
|
69
|
+
* capacity becomes available.
|
|
70
|
+
*/
|
|
71
|
+
async acquire() {
|
|
72
|
+
this.refillTokens();
|
|
73
|
+
const effectiveLimit = this.throttled
|
|
74
|
+
? Math.ceil(this.config.perSecondLimit / 2)
|
|
75
|
+
: this.config.perSecondLimit;
|
|
76
|
+
if (this.tokens > 0 && this.concurrent < this.config.maxConcurrent) {
|
|
77
|
+
this.tokens = Math.min(this.tokens, effectiveLimit);
|
|
78
|
+
this.tokens--;
|
|
79
|
+
this.concurrent++;
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
// No capacity — enqueue and wait.
|
|
83
|
+
return new Promise((resolve) => {
|
|
84
|
+
this.waitQueue.push({ resolve });
|
|
85
|
+
// Schedule a wake-up check so queued requests don't starve when no
|
|
86
|
+
// release() calls arrive (e.g. all slots are token-limited, not
|
|
87
|
+
// concurrency-limited).
|
|
88
|
+
setTimeout(() => this.drain(), 1_000 / this.config.perSecondLimit);
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Release a concurrency slot after a request completes.
|
|
93
|
+
*
|
|
94
|
+
* Must be called exactly once for every successful {@link acquire}.
|
|
95
|
+
*/
|
|
96
|
+
release() {
|
|
97
|
+
this.concurrent = Math.max(0, this.concurrent - 1);
|
|
98
|
+
this.drain();
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Parse Meta usage headers and enable throttling when approaching limits.
|
|
102
|
+
*
|
|
103
|
+
* Meta returns usage information in two headers:
|
|
104
|
+
* - `x-app-usage` — app-level call/CPU/memory percentages.
|
|
105
|
+
* - `x-business-use-case-usage` — per-WABA usage buckets (WhatsApp).
|
|
106
|
+
*
|
|
107
|
+
* If any reported percentage exceeds 80 %, the limiter enters a throttled
|
|
108
|
+
* state that halves the effective token refill rate.
|
|
109
|
+
*
|
|
110
|
+
* @param headers - Response headers from a Graph API call.
|
|
111
|
+
*/
|
|
112
|
+
updateFromHeaders(headers) {
|
|
113
|
+
// --- x-app-usage ---
|
|
114
|
+
const appUsage = headers.get('x-app-usage');
|
|
115
|
+
if (appUsage) {
|
|
116
|
+
try {
|
|
117
|
+
const parsed = JSON.parse(appUsage);
|
|
118
|
+
const maxPct = Math.max(parsed.call_count ?? 0, parsed.total_cputime ?? 0, parsed.total_time ?? 0);
|
|
119
|
+
this.throttled = maxPct > 80;
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
// Malformed header — ignore.
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
// --- x-business-use-case-usage ---
|
|
127
|
+
const bizUsage = headers.get('x-business-use-case-usage');
|
|
128
|
+
if (bizUsage) {
|
|
129
|
+
try {
|
|
130
|
+
const parsed = JSON.parse(bizUsage);
|
|
131
|
+
let maxPct = 0;
|
|
132
|
+
for (const buckets of Object.values(parsed)) {
|
|
133
|
+
for (const bucket of buckets) {
|
|
134
|
+
const rate = typeof bucket.rate_limit_usage === 'number' ? bucket.rate_limit_usage : 0;
|
|
135
|
+
if (rate > maxPct)
|
|
136
|
+
maxPct = rate;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
this.throttled = maxPct > 80;
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
// Malformed header — ignore.
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
// -----------------------------------------------------------------------
|
|
147
|
+
// Private helpers
|
|
148
|
+
// -----------------------------------------------------------------------
|
|
149
|
+
/** Refill tokens based on elapsed wall-clock time. */
|
|
150
|
+
refillTokens() {
|
|
151
|
+
const now = Date.now();
|
|
152
|
+
const elapsedMs = now - this.lastRefill;
|
|
153
|
+
if (elapsedMs <= 0)
|
|
154
|
+
return;
|
|
155
|
+
const tokensToAdd = (elapsedMs / 1_000) * this.config.perSecondLimit;
|
|
156
|
+
this.tokens = Math.min(this.tokens + tokensToAdd, this.config.perSecondLimit);
|
|
157
|
+
this.lastRefill = now;
|
|
158
|
+
}
|
|
159
|
+
/** Attempt to wake queued callers if capacity has become available. */
|
|
160
|
+
drain() {
|
|
161
|
+
this.refillTokens();
|
|
162
|
+
while (this.waitQueue.length > 0 &&
|
|
163
|
+
this.tokens > 0 &&
|
|
164
|
+
this.concurrent < this.config.maxConcurrent) {
|
|
165
|
+
this.tokens--;
|
|
166
|
+
this.concurrent++;
|
|
167
|
+
const next = this.waitQueue.shift();
|
|
168
|
+
next.resolve();
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
//# sourceMappingURL=rate-limiter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rate-limiter.js","sourceRoot":"","sources":["../../src/graph-api/rate-limiter.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module graph-api/retry
|
|
3
|
+
*
|
|
4
|
+
* Exponential-backoff retry queue for Graph API requests.
|
|
5
|
+
*
|
|
6
|
+
* The queue wraps an async function and re-executes it on transient failures
|
|
7
|
+
* using exponential backoff with jitter. It integrates with the error hierarchy
|
|
8
|
+
* so that {@link RateLimitError}s are always retried while permanent errors
|
|
9
|
+
* (auth, permission, recipient) propagate immediately.
|
|
10
|
+
*/
|
|
11
|
+
/** Retry behaviour configuration. */
|
|
12
|
+
export interface RetryConfig {
|
|
13
|
+
/** Maximum number of retry attempts (does not count the initial attempt). Default `3`. */
|
|
14
|
+
maxRetries: number;
|
|
15
|
+
/** Base delay in milliseconds before the first retry. Default `1000`. */
|
|
16
|
+
baseDelayMs: number;
|
|
17
|
+
/** Upper bound on computed delay in milliseconds. Default `30000`. */
|
|
18
|
+
maxDelayMs: number;
|
|
19
|
+
/** HTTP status codes that should trigger a retry. Default `[429, 500, 502, 503, 504]`. */
|
|
20
|
+
retryableStatuses: number[];
|
|
21
|
+
}
|
|
22
|
+
/** Sensible defaults matching Meta's Cloud API guidance. */
|
|
23
|
+
export declare const DEFAULT_RETRY_CONFIG: RetryConfig;
|
|
24
|
+
/**
|
|
25
|
+
* Retry queue that wraps an async function with exponential backoff + jitter.
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```ts
|
|
29
|
+
* const queue = new RetryQueue({ maxRetries: 2 });
|
|
30
|
+
* const data = await queue.execute(() => fetch(url).then(r => r.json()));
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export declare class RetryQueue {
|
|
34
|
+
private readonly config;
|
|
35
|
+
constructor(config?: Partial<RetryConfig>);
|
|
36
|
+
/**
|
|
37
|
+
* Execute `fn` with automatic retries on transient failures.
|
|
38
|
+
*
|
|
39
|
+
* @param fn - The async operation to execute.
|
|
40
|
+
* @returns The resolved value of `fn`.
|
|
41
|
+
* @throws The last error if all retries are exhausted or the error is non-retryable.
|
|
42
|
+
*/
|
|
43
|
+
execute<T>(fn: () => Promise<T>): Promise<T>;
|
|
44
|
+
/**
|
|
45
|
+
* Determine whether an error is transient and safe to retry.
|
|
46
|
+
*
|
|
47
|
+
* - {@link RateLimitError} is always retryable.
|
|
48
|
+
* - {@link MessagingError} with a code matching `meta_error_{status}` where
|
|
49
|
+
* status is in the `retryableStatuses` list is retryable.
|
|
50
|
+
* - Plain network errors (`TypeError` from `fetch`) are retryable.
|
|
51
|
+
* - Everything else is treated as permanent (auth, permission, recipient, template).
|
|
52
|
+
*/
|
|
53
|
+
private isRetryable;
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=retry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"retry.d.ts","sourceRoot":"","sources":["../../src/graph-api/retry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAQH,qCAAqC;AACrC,MAAM,WAAW,WAAW;IAC1B,0FAA0F;IAC1F,UAAU,EAAE,MAAM,CAAC;IACnB,yEAAyE;IACzE,WAAW,EAAE,MAAM,CAAC;IACpB,sEAAsE;IACtE,UAAU,EAAE,MAAM,CAAC;IACnB,0FAA0F;IAC1F,iBAAiB,EAAE,MAAM,EAAE,CAAC;CAC7B;AAED,4DAA4D;AAC5D,eAAO,MAAM,oBAAoB,EAAE,WAKlC,CAAC;AAMF;;;;;;;;GAQG;AACH,qBAAa,UAAU;IACrB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAc;gBAEzB,MAAM,GAAE,OAAO,CAAC,WAAW,CAAM;IAI7C;;;;;;OAMG;IACG,OAAO,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;IAoClD;;;;;;;;OAQG;IACH,OAAO,CAAC,WAAW;CAwBpB"}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module graph-api/retry
|
|
3
|
+
*
|
|
4
|
+
* Exponential-backoff retry queue for Graph API requests.
|
|
5
|
+
*
|
|
6
|
+
* The queue wraps an async function and re-executes it on transient failures
|
|
7
|
+
* using exponential backoff with jitter. It integrates with the error hierarchy
|
|
8
|
+
* so that {@link RateLimitError}s are always retried while permanent errors
|
|
9
|
+
* (auth, permission, recipient) propagate immediately.
|
|
10
|
+
*/
|
|
11
|
+
import { RateLimitError, MessagingError } from './errors.js';
|
|
12
|
+
/** Sensible defaults matching Meta's Cloud API guidance. */
|
|
13
|
+
export const DEFAULT_RETRY_CONFIG = {
|
|
14
|
+
maxRetries: 3,
|
|
15
|
+
baseDelayMs: 1_000,
|
|
16
|
+
maxDelayMs: 30_000,
|
|
17
|
+
retryableStatuses: [429, 500, 502, 503, 504],
|
|
18
|
+
};
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// RetryQueue
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
/**
|
|
23
|
+
* Retry queue that wraps an async function with exponential backoff + jitter.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```ts
|
|
27
|
+
* const queue = new RetryQueue({ maxRetries: 2 });
|
|
28
|
+
* const data = await queue.execute(() => fetch(url).then(r => r.json()));
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export class RetryQueue {
|
|
32
|
+
config;
|
|
33
|
+
constructor(config = {}) {
|
|
34
|
+
this.config = { ...DEFAULT_RETRY_CONFIG, ...config };
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Execute `fn` with automatic retries on transient failures.
|
|
38
|
+
*
|
|
39
|
+
* @param fn - The async operation to execute.
|
|
40
|
+
* @returns The resolved value of `fn`.
|
|
41
|
+
* @throws The last error if all retries are exhausted or the error is non-retryable.
|
|
42
|
+
*/
|
|
43
|
+
async execute(fn) {
|
|
44
|
+
let lastError;
|
|
45
|
+
for (let attempt = 0; attempt <= this.config.maxRetries; attempt++) {
|
|
46
|
+
try {
|
|
47
|
+
return await fn();
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
lastError = error;
|
|
51
|
+
// If the error is not retryable or we've exhausted attempts, throw immediately.
|
|
52
|
+
if (!this.isRetryable(error) || attempt === this.config.maxRetries) {
|
|
53
|
+
throw lastError;
|
|
54
|
+
}
|
|
55
|
+
// Compute delay with exponential backoff + random jitter (0-1 s).
|
|
56
|
+
const exponentialDelay = this.config.baseDelayMs * Math.pow(2, attempt);
|
|
57
|
+
const jitter = Math.random() * 1_000;
|
|
58
|
+
let delay = Math.min(exponentialDelay + jitter, this.config.maxDelayMs);
|
|
59
|
+
// If the error carries a specific retryAfter, honour it.
|
|
60
|
+
if (error instanceof RateLimitError && error.retryAfterMs > delay) {
|
|
61
|
+
delay = Math.min(error.retryAfterMs, this.config.maxDelayMs);
|
|
62
|
+
}
|
|
63
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
// TypeScript needs this even though the loop always either returns or throws.
|
|
67
|
+
throw lastError;
|
|
68
|
+
}
|
|
69
|
+
// -------------------------------------------------------------------------
|
|
70
|
+
// Private helpers
|
|
71
|
+
// -------------------------------------------------------------------------
|
|
72
|
+
/**
|
|
73
|
+
* Determine whether an error is transient and safe to retry.
|
|
74
|
+
*
|
|
75
|
+
* - {@link RateLimitError} is always retryable.
|
|
76
|
+
* - {@link MessagingError} with a code matching `meta_error_{status}` where
|
|
77
|
+
* status is in the `retryableStatuses` list is retryable.
|
|
78
|
+
* - Plain network errors (`TypeError` from `fetch`) are retryable.
|
|
79
|
+
* - Everything else is treated as permanent (auth, permission, recipient, template).
|
|
80
|
+
*/
|
|
81
|
+
isRetryable(error) {
|
|
82
|
+
if (error instanceof RateLimitError) {
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
if (error instanceof MessagingError) {
|
|
86
|
+
// The classifyMetaError fallback sets code to `meta_error_{statusCode}`.
|
|
87
|
+
// Extract the status code from the error code and check if it's retryable.
|
|
88
|
+
const match = error.code.match(/^meta_error_(\d+)$/);
|
|
89
|
+
if (match) {
|
|
90
|
+
const statusCode = parseInt(match[1], 10);
|
|
91
|
+
return this.config.retryableStatuses.includes(statusCode);
|
|
92
|
+
}
|
|
93
|
+
// Known typed errors (auth, permission, recipient, template, window) are NOT retryable.
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
// Network-level failures thrown by `fetch` (e.g. DNS resolution, TCP reset).
|
|
97
|
+
if (error instanceof TypeError) {
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
//# sourceMappingURL=retry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"retry.js","sourceRoot":"","sources":["../../src/graph-api/retry.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module @ariaflowagents/messaging-meta
|
|
3
|
+
*
|
|
4
|
+
* Meta platform clients (WhatsApp, Messenger, Instagram) for AriaFlow messaging.
|
|
5
|
+
*
|
|
6
|
+
* This is the main entry point for the package. It re-exports the shared
|
|
7
|
+
* Graph API foundation (client, retry, rate limiter, errors) and the webhook
|
|
8
|
+
* utilities (signature verification, payload normalization).
|
|
9
|
+
*
|
|
10
|
+
* Platform-specific clients are available via subpath imports:
|
|
11
|
+
* - `@ariaflowagents/messaging-meta/whatsapp`
|
|
12
|
+
* - `@ariaflowagents/messaging-meta/messenger`
|
|
13
|
+
* - `@ariaflowagents/messaging-meta/instagram`
|
|
14
|
+
*
|
|
15
|
+
* Server-only utilities (verifier + normalizer without the Graph API client):
|
|
16
|
+
* - `@ariaflowagents/messaging-meta/server`
|
|
17
|
+
*
|
|
18
|
+
* @packageDocumentation
|
|
19
|
+
*/
|
|
20
|
+
export { GraphAPIClient } from './graph-api/client.js';
|
|
21
|
+
export type { GraphAPIClientConfig, Logger } from './graph-api/client.js';
|
|
22
|
+
export { RetryQueue, DEFAULT_RETRY_CONFIG } from './graph-api/retry.js';
|
|
23
|
+
export type { RetryConfig } from './graph-api/retry.js';
|
|
24
|
+
export { RateLimiter, DEFAULT_RATE_LIMITER_CONFIG } from './graph-api/rate-limiter.js';
|
|
25
|
+
export type { RateLimiterConfig } from './graph-api/rate-limiter.js';
|
|
26
|
+
export { classifyMetaError, MessagingError, RateLimitError, AuthenticationError, PermissionError, RecipientError, WindowClosedError, TemplateError, MediaError, WebhookVerificationError, } from './graph-api/errors.js';
|
|
27
|
+
export type { MetaErrorResponse } from './graph-api/errors.js';
|
|
28
|
+
export { verifySignature } from './webhook/verifier.js';
|
|
29
|
+
export type { VerifySignatureOptions } from './webhook/verifier.js';
|
|
30
|
+
export { normalizeWebhook } from './webhook/normalizer.js';
|
|
31
|
+
export type { NormalizedWebhookEvents, NormalizedMessage, NormalizedStatus, NormalizedReaction, } from './webhook/normalizer.js';
|
|
32
|
+
export { MessengerClient, createMessengerClient } from './messenger/index.js';
|
|
33
|
+
export type { MessengerClientConfig } from './messenger/index.js';
|
|
34
|
+
export { InstagramClient, createInstagramClient } from './instagram/index.js';
|
|
35
|
+
export type { InstagramClientConfig } from './instagram/index.js';
|
|
36
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAMH,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACvD,YAAY,EAAE,oBAAoB,EAAE,MAAM,EAAE,MAAM,uBAAuB,CAAC;AAE1E,OAAO,EAAE,UAAU,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AACxE,YAAY,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAExD,OAAO,EAAE,WAAW,EAAE,2BAA2B,EAAE,MAAM,6BAA6B,CAAC;AACvF,YAAY,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAC;AAErE,OAAO,EACL,iBAAiB,EACjB,cAAc,EACd,cAAc,EACd,mBAAmB,EACnB,eAAe,EACf,cAAc,EACd,iBAAiB,EACjB,aAAa,EACb,UAAU,EACV,wBAAwB,GACzB,MAAM,uBAAuB,CAAC;AAC/B,YAAY,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAM/D,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,YAAY,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAC;AAEpE,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAC3D,YAAY,EACV,uBAAuB,EACvB,iBAAiB,EACjB,gBAAgB,EAChB,kBAAkB,GACnB,MAAM,yBAAyB,CAAC;AAOjC,OAAO,EAAE,eAAe,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAC9E,YAAY,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAClE,OAAO,EAAE,eAAe,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAC9E,YAAY,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module @ariaflowagents/messaging-meta
|
|
3
|
+
*
|
|
4
|
+
* Meta platform clients (WhatsApp, Messenger, Instagram) for AriaFlow messaging.
|
|
5
|
+
*
|
|
6
|
+
* This is the main entry point for the package. It re-exports the shared
|
|
7
|
+
* Graph API foundation (client, retry, rate limiter, errors) and the webhook
|
|
8
|
+
* utilities (signature verification, payload normalization).
|
|
9
|
+
*
|
|
10
|
+
* Platform-specific clients are available via subpath imports:
|
|
11
|
+
* - `@ariaflowagents/messaging-meta/whatsapp`
|
|
12
|
+
* - `@ariaflowagents/messaging-meta/messenger`
|
|
13
|
+
* - `@ariaflowagents/messaging-meta/instagram`
|
|
14
|
+
*
|
|
15
|
+
* Server-only utilities (verifier + normalizer without the Graph API client):
|
|
16
|
+
* - `@ariaflowagents/messaging-meta/server`
|
|
17
|
+
*
|
|
18
|
+
* @packageDocumentation
|
|
19
|
+
*/
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// Graph API foundation
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
export { GraphAPIClient } from './graph-api/client.js';
|
|
24
|
+
export { RetryQueue, DEFAULT_RETRY_CONFIG } from './graph-api/retry.js';
|
|
25
|
+
export { RateLimiter, DEFAULT_RATE_LIMITER_CONFIG } from './graph-api/rate-limiter.js';
|
|
26
|
+
export { classifyMetaError, MessagingError, RateLimitError, AuthenticationError, PermissionError, RecipientError, WindowClosedError, TemplateError, MediaError, WebhookVerificationError, } from './graph-api/errors.js';
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// Webhook utilities
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
export { verifySignature } from './webhook/verifier.js';
|
|
31
|
+
export { normalizeWebhook } from './webhook/normalizer.js';
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// Platform clients (placeholder — will be populated as clients are built)
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
// export { WhatsAppClient } from './whatsapp/index.js';
|
|
36
|
+
export { MessengerClient, createMessengerClient } from './messenger/index.js';
|
|
37
|
+
export { InstagramClient, createInstagramClient } from './instagram/index.js';
|
|
38
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,8EAA8E;AAC9E,uBAAuB;AACvB,8EAA8E;AAE9E,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAGvD,OAAO,EAAE,UAAU,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAGxE,OAAO,EAAE,WAAW,EAAE,2BAA2B,EAAE,MAAM,6BAA6B,CAAC;AAGvF,OAAO,EACL,iBAAiB,EACjB,cAAc,EACd,cAAc,EACd,mBAAmB,EACnB,eAAe,EACf,cAAc,EACd,iBAAiB,EACjB,aAAa,EACb,UAAU,EACV,wBAAwB,GACzB,MAAM,uBAAuB,CAAC;AAG/B,8EAA8E;AAC9E,oBAAoB;AACpB,8EAA8E;AAE9E,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAGxD,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAQ3D,8EAA8E;AAC9E,0EAA0E;AAC1E,8EAA8E;AAE9E,wDAAwD;AACxD,OAAO,EAAE,eAAe,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAE9E,OAAO,EAAE,eAAe,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC"}
|