@sakeetech/viva-payments-core 0.2.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.
Files changed (203) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +413 -0
  3. package/dist/auth/http.d.ts +44 -0
  4. package/dist/auth/http.d.ts.map +1 -0
  5. package/dist/auth/http.js +80 -0
  6. package/dist/auth/http.js.map +1 -0
  7. package/dist/auth/index.d.ts +19 -0
  8. package/dist/auth/index.d.ts.map +1 -0
  9. package/dist/auth/index.js +18 -0
  10. package/dist/auth/index.js.map +1 -0
  11. package/dist/auth/oauth2-strategy.d.ts +117 -0
  12. package/dist/auth/oauth2-strategy.d.ts.map +1 -0
  13. package/dist/auth/oauth2-strategy.js +217 -0
  14. package/dist/auth/oauth2-strategy.js.map +1 -0
  15. package/dist/auth/reseller-strategy.d.ts +65 -0
  16. package/dist/auth/reseller-strategy.d.ts.map +1 -0
  17. package/dist/auth/reseller-strategy.js +68 -0
  18. package/dist/auth/reseller-strategy.js.map +1 -0
  19. package/dist/auth/single-flight.d.ts +81 -0
  20. package/dist/auth/single-flight.d.ts.map +1 -0
  21. package/dist/auth/single-flight.js +160 -0
  22. package/dist/auth/single-flight.js.map +1 -0
  23. package/dist/auth/token-cache.d.ts +50 -0
  24. package/dist/auth/token-cache.d.ts.map +1 -0
  25. package/dist/auth/token-cache.js +59 -0
  26. package/dist/auth/token-cache.js.map +1 -0
  27. package/dist/errors/api-error.d.ts +15 -0
  28. package/dist/errors/api-error.d.ts.map +1 -0
  29. package/dist/errors/api-error.js +18 -0
  30. package/dist/errors/api-error.js.map +1 -0
  31. package/dist/errors/auth-error.d.ts +14 -0
  32. package/dist/errors/auth-error.d.ts.map +1 -0
  33. package/dist/errors/auth-error.js +17 -0
  34. package/dist/errors/auth-error.js.map +1 -0
  35. package/dist/errors/base.d.ts +59 -0
  36. package/dist/errors/base.d.ts.map +1 -0
  37. package/dist/errors/base.js +51 -0
  38. package/dist/errors/base.js.map +1 -0
  39. package/dist/errors/index.d.ts +18 -0
  40. package/dist/errors/index.d.ts.map +1 -0
  41. package/dist/errors/index.js +16 -0
  42. package/dist/errors/index.js.map +1 -0
  43. package/dist/errors/mode-mismatch-error.d.ts +19 -0
  44. package/dist/errors/mode-mismatch-error.d.ts.map +1 -0
  45. package/dist/errors/mode-mismatch-error.js +22 -0
  46. package/dist/errors/mode-mismatch-error.js.map +1 -0
  47. package/dist/errors/rate-limit-error.d.ts +20 -0
  48. package/dist/errors/rate-limit-error.d.ts.map +1 -0
  49. package/dist/errors/rate-limit-error.js +20 -0
  50. package/dist/errors/rate-limit-error.js.map +1 -0
  51. package/dist/errors/validation-error.d.ts +14 -0
  52. package/dist/errors/validation-error.d.ts.map +1 -0
  53. package/dist/errors/validation-error.js +17 -0
  54. package/dist/errors/validation-error.js.map +1 -0
  55. package/dist/errors/webhook-error.d.ts +14 -0
  56. package/dist/errors/webhook-error.d.ts.map +1 -0
  57. package/dist/errors/webhook-error.js +17 -0
  58. package/dist/errors/webhook-error.js.map +1 -0
  59. package/dist/index.d.ts +15 -0
  60. package/dist/index.d.ts.map +1 -0
  61. package/dist/index.js +15 -0
  62. package/dist/index.js.map +1 -0
  63. package/dist/isv/accounts.d.ts +38 -0
  64. package/dist/isv/accounts.d.ts.map +1 -0
  65. package/dist/isv/accounts.js +60 -0
  66. package/dist/isv/accounts.js.map +1 -0
  67. package/dist/isv/client.d.ts +187 -0
  68. package/dist/isv/client.d.ts.map +1 -0
  69. package/dist/isv/client.js +465 -0
  70. package/dist/isv/client.js.map +1 -0
  71. package/dist/isv/index.d.ts +52 -0
  72. package/dist/isv/index.d.ts.map +1 -0
  73. package/dist/isv/index.js +53 -0
  74. package/dist/isv/index.js.map +1 -0
  75. package/dist/isv/legacy-basic-client.d.ts +122 -0
  76. package/dist/isv/legacy-basic-client.d.ts.map +1 -0
  77. package/dist/isv/legacy-basic-client.js +281 -0
  78. package/dist/isv/legacy-basic-client.js.map +1 -0
  79. package/dist/isv/payments.d.ts +199 -0
  80. package/dist/isv/payments.d.ts.map +1 -0
  81. package/dist/isv/payments.js +385 -0
  82. package/dist/isv/payments.js.map +1 -0
  83. package/dist/isv/sources.d.ts +80 -0
  84. package/dist/isv/sources.d.ts.map +1 -0
  85. package/dist/isv/sources.js +112 -0
  86. package/dist/isv/sources.js.map +1 -0
  87. package/dist/isv/webhooks-api.d.ts +48 -0
  88. package/dist/isv/webhooks-api.d.ts.map +1 -0
  89. package/dist/isv/webhooks-api.js +66 -0
  90. package/dist/isv/webhooks-api.js.map +1 -0
  91. package/dist/legacy/client.d.ts +199 -0
  92. package/dist/legacy/client.d.ts.map +1 -0
  93. package/dist/legacy/client.js +351 -0
  94. package/dist/legacy/client.js.map +1 -0
  95. package/dist/legacy/index.d.ts +15 -0
  96. package/dist/legacy/index.d.ts.map +1 -0
  97. package/dist/legacy/index.js +14 -0
  98. package/dist/legacy/index.js.map +1 -0
  99. package/dist/observability/context.d.ts +30 -0
  100. package/dist/observability/context.d.ts.map +1 -0
  101. package/dist/observability/context.js +40 -0
  102. package/dist/observability/context.js.map +1 -0
  103. package/dist/observability/index.d.ts +15 -0
  104. package/dist/observability/index.d.ts.map +1 -0
  105. package/dist/observability/index.js +11 -0
  106. package/dist/observability/index.js.map +1 -0
  107. package/dist/observability/logger.d.ts +81 -0
  108. package/dist/observability/logger.d.ts.map +1 -0
  109. package/dist/observability/logger.js +127 -0
  110. package/dist/observability/logger.js.map +1 -0
  111. package/dist/observability/metrics.d.ts +37 -0
  112. package/dist/observability/metrics.d.ts.map +1 -0
  113. package/dist/observability/metrics.js +40 -0
  114. package/dist/observability/metrics.js.map +1 -0
  115. package/dist/observability/redact.d.ts +21 -0
  116. package/dist/observability/redact.d.ts.map +1 -0
  117. package/dist/observability/redact.js +72 -0
  118. package/dist/observability/redact.js.map +1 -0
  119. package/dist/observability/tracer.d.ts +25 -0
  120. package/dist/observability/tracer.d.ts.map +1 -0
  121. package/dist/observability/tracer.js +18 -0
  122. package/dist/observability/tracer.js.map +1 -0
  123. package/dist/payments/client.d.ts +247 -0
  124. package/dist/payments/client.d.ts.map +1 -0
  125. package/dist/payments/client.js +488 -0
  126. package/dist/payments/client.js.map +1 -0
  127. package/dist/payments/index.d.ts +14 -0
  128. package/dist/payments/index.d.ts.map +1 -0
  129. package/dist/payments/index.js +13 -0
  130. package/dist/payments/index.js.map +1 -0
  131. package/dist/refunds/fast-refund-client.d.ts +128 -0
  132. package/dist/refunds/fast-refund-client.d.ts.map +1 -0
  133. package/dist/refunds/fast-refund-client.js +138 -0
  134. package/dist/refunds/fast-refund-client.js.map +1 -0
  135. package/dist/refunds/index.d.ts +19 -0
  136. package/dist/refunds/index.d.ts.map +1 -0
  137. package/dist/refunds/index.js +17 -0
  138. package/dist/refunds/index.js.map +1 -0
  139. package/dist/refunds/strategy.d.ts +78 -0
  140. package/dist/refunds/strategy.d.ts.map +1 -0
  141. package/dist/refunds/strategy.js +75 -0
  142. package/dist/refunds/strategy.js.map +1 -0
  143. package/dist/types/auth.d.ts +80 -0
  144. package/dist/types/auth.d.ts.map +1 -0
  145. package/dist/types/auth.js +12 -0
  146. package/dist/types/auth.js.map +1 -0
  147. package/dist/types/card-types.d.ts +48 -0
  148. package/dist/types/card-types.d.ts.map +1 -0
  149. package/dist/types/card-types.js +62 -0
  150. package/dist/types/card-types.js.map +1 -0
  151. package/dist/types/common.d.ts +160 -0
  152. package/dist/types/common.d.ts.map +1 -0
  153. package/dist/types/common.js +70 -0
  154. package/dist/types/common.js.map +1 -0
  155. package/dist/types/index.d.ts +21 -0
  156. package/dist/types/index.d.ts.map +1 -0
  157. package/dist/types/index.js +21 -0
  158. package/dist/types/index.js.map +1 -0
  159. package/dist/types/isv-accounts.d.ts +109 -0
  160. package/dist/types/isv-accounts.d.ts.map +1 -0
  161. package/dist/types/isv-accounts.js +22 -0
  162. package/dist/types/isv-accounts.js.map +1 -0
  163. package/dist/types/isv-payments.d.ts +262 -0
  164. package/dist/types/isv-payments.d.ts.map +1 -0
  165. package/dist/types/isv-payments.js +19 -0
  166. package/dist/types/isv-payments.js.map +1 -0
  167. package/dist/types/status.d.ts +125 -0
  168. package/dist/types/status.d.ts.map +1 -0
  169. package/dist/types/status.js +19 -0
  170. package/dist/types/status.js.map +1 -0
  171. package/dist/types/webhook-events.d.ts +447 -0
  172. package/dist/types/webhook-events.d.ts.map +1 -0
  173. package/dist/types/webhook-events.js +76 -0
  174. package/dist/types/webhook-events.js.map +1 -0
  175. package/dist/webhooks/challenge-response.d.ts +28 -0
  176. package/dist/webhooks/challenge-response.d.ts.map +1 -0
  177. package/dist/webhooks/challenge-response.js +35 -0
  178. package/dist/webhooks/challenge-response.js.map +1 -0
  179. package/dist/webhooks/event-types.d.ts +44 -0
  180. package/dist/webhooks/event-types.d.ts.map +1 -0
  181. package/dist/webhooks/event-types.js +50 -0
  182. package/dist/webhooks/event-types.js.map +1 -0
  183. package/dist/webhooks/extract-client-ip.d.ts +40 -0
  184. package/dist/webhooks/extract-client-ip.d.ts.map +1 -0
  185. package/dist/webhooks/extract-client-ip.js +72 -0
  186. package/dist/webhooks/extract-client-ip.js.map +1 -0
  187. package/dist/webhooks/hmac-verify.d.ts +38 -0
  188. package/dist/webhooks/hmac-verify.d.ts.map +1 -0
  189. package/dist/webhooks/hmac-verify.js +92 -0
  190. package/dist/webhooks/hmac-verify.js.map +1 -0
  191. package/dist/webhooks/index.d.ts +19 -0
  192. package/dist/webhooks/index.d.ts.map +1 -0
  193. package/dist/webhooks/index.js +19 -0
  194. package/dist/webhooks/index.js.map +1 -0
  195. package/dist/webhooks/ip-allowlist.d.ts +59 -0
  196. package/dist/webhooks/ip-allowlist.d.ts.map +1 -0
  197. package/dist/webhooks/ip-allowlist.js +147 -0
  198. package/dist/webhooks/ip-allowlist.js.map +1 -0
  199. package/dist/webhooks/status-lattice.d.ts +72 -0
  200. package/dist/webhooks/status-lattice.d.ts.map +1 -0
  201. package/dist/webhooks/status-lattice.js +208 -0
  202. package/dist/webhooks/status-lattice.js.map +1 -0
  203. package/package.json +85 -0
@@ -0,0 +1,117 @@
1
+ /**
2
+ * OAuth 2.0 client_credentials strategy for Viva Wallet ISV API calls.
3
+ *
4
+ * Handles the full token lifecycle: obtain, cache, refresh, and single-flight
5
+ * de-duplication to prevent stampede under concurrent load.
6
+ *
7
+ * @see references/viva-docs/md/oauth2-authentication.txt:128 (token endpoint)
8
+ * @see references/viva-docs/md/oauth2-authentication.txt:145 (demo/prod URLs)
9
+ * @see references/viva-docs/md/oauth2-authentication.txt:167 (grant_type, Basic auth header)
10
+ * @see references/viva-docs/md/oauth2-authentication.txt:179 (token response shape, expires_in)
11
+ * @see references/viva-docs/md/oauth2-authentication.txt:192 (3600s expiry)
12
+ */
13
+ import type { Dispatcher } from 'undici';
14
+ import type { AuthStrategy } from '../types/auth.js';
15
+ import type { VivaEnvironment } from '../types/common.js';
16
+ import { AsyncMutex } from './single-flight.js';
17
+ import type { RedisLockClient } from './single-flight.js';
18
+ import type { TokenCache } from './token-cache.js';
19
+ import type { MetricsHook, Logger } from '../observability/index.js';
20
+ export interface OAuth2StrategyOptions {
21
+ /** Target environment — determines token endpoint host. */
22
+ environment: VivaEnvironment;
23
+ /** OAuth2 Client ID. @see references/viva-docs/md/oauth2-authentication.txt:119 */
24
+ clientId: string;
25
+ /** OAuth2 Client Secret. @see references/viva-docs/md/oauth2-authentication.txt:119 */
26
+ clientSecret: string;
27
+ /** Token cache implementation. Defaults to `InMemoryTokenCache`. */
28
+ cache?: TokenCache;
29
+ /** In-process mutex. Defaults to a fresh `AsyncMutex`. */
30
+ mutex?: AsyncMutex;
31
+ /**
32
+ * Redis lock client for multi-worker deployments (plan P11).
33
+ * When provided, single-flight uses Redis instead of the in-proc mutex.
34
+ */
35
+ redisLock?: RedisLockClient;
36
+ /**
37
+ * Milliseconds before expiry at which the token is proactively refreshed.
38
+ * Default: 5 * 60_000 = 300_000 ms (T-5min, per plan P11 line 73).
39
+ */
40
+ refreshSkewMs?: number;
41
+ /** Backoff between retries when used by the caller. Default: 500 ms. */
42
+ retryBackoffMs?: number;
43
+ /** Injectable fetch. Defaults to the global `fetch`. Tests pass MockAgent fetch. */
44
+ fetchImpl?: typeof fetch;
45
+ /** Dispatcher override. Tests inject a MockAgent here. */
46
+ dispatcher?: Dispatcher;
47
+ /** Injectable clock. Defaults to `Date.now`. */
48
+ now?: () => number;
49
+ /** Optional metrics hook. Records viva_auth_token_refresh_duration_seconds. */
50
+ metrics?: MetricsHook;
51
+ /** Optional logger. Emits structured info on each token refresh. */
52
+ logger?: Logger;
53
+ }
54
+ /**
55
+ * OAuth 2.0 `client_credentials` token strategy.
56
+ *
57
+ * Flow:
58
+ * 1. Check cache. If token is fresh (expires_at > now + refreshSkewMs), return it.
59
+ * 2. Acquire single-flight lock. Re-check cache (avoid duplicate fetch after waking).
60
+ * 3. POST `grant_type=client_credentials` to the token endpoint.
61
+ * 4. Cache result with `expires_at = now + (expires_in * 1000) - 60_000`.
62
+ *
63
+ * A1 property: `forceRefresh=true` (used by S3 on 401-recovery) ALSO acquires
64
+ * the single-flight lock, so even during concurrent secret rotation only one
65
+ * worker calls `/connect/token`. This is naturally satisfied because
66
+ * `getBearerToken` unconditionally enters `singleFlight` when the cache check
67
+ * is bypassed or fails.
68
+ */
69
+ export declare class OAuth2ClientCredentialsStrategy implements AuthStrategy {
70
+ readonly name = "oauth2-client-credentials";
71
+ private readonly _env;
72
+ private readonly _clientId;
73
+ private readonly _clientSecret;
74
+ private readonly _cache;
75
+ private readonly _mutex;
76
+ private readonly _redisLock;
77
+ private readonly _refreshSkewMs;
78
+ private readonly _fetchImpl;
79
+ private readonly _dispatcher;
80
+ private readonly _now;
81
+ private readonly _metrics;
82
+ private readonly _logger;
83
+ constructor(opts: OAuth2StrategyOptions);
84
+ /** Exposes the token cache for auth-status inspection. */
85
+ get tokenCache(): TokenCache;
86
+ /** Cache key format: `viva:isv:token:{clientId}:{environment}` */
87
+ private get _cacheKey();
88
+ /**
89
+ * Returns a valid bearer token for use in `Authorization: Bearer <token>`.
90
+ *
91
+ * When `forceRefresh` is true the cache lookup is skipped but the request
92
+ * STILL goes through the single-flight lock — this is the A1 property:
93
+ * multiple workers calling `getBearerToken({ forceRefresh: true })` after
94
+ * receiving a 401 will not stampede the token endpoint.
95
+ * // A1: forceRefresh bypasses cache but not the lock — stampede prevention
96
+ * // holds even during secret rotation. All concurrent 401-recovery calls
97
+ * // will coalesce into one token fetch. (Plan amendment A1.)
98
+ */
99
+ getBearerToken(opts?: {
100
+ forceRefresh?: boolean;
101
+ }): Promise<string>;
102
+ /**
103
+ * Fetches a fresh token from the Viva identity server.
104
+ *
105
+ * Endpoint:
106
+ * Demo: https://demo-accounts.vivapayments.com/connect/token
107
+ * Production: https://accounts.vivapayments.com/connect/token
108
+ *
109
+ * @see references/viva-docs/md/oauth2-authentication.txt:145 (endpoint URLs)
110
+ * @see references/viva-docs/md/oauth2-authentication.txt:167 (curl example: grant_type, Basic header)
111
+ * @see references/viva-docs/md/oauth2-authentication.txt:179 (response: access_token, expires_in)
112
+ * @see references/viva-docs/md/oauth2-authentication.txt:192 (3600s token lifetime)
113
+ */
114
+ private _fetchToken;
115
+ private _doFetchToken;
116
+ }
117
+ //# sourceMappingURL=oauth2-strategy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oauth2-strategy.d.ts","sourceRoot":"","sources":["../../src/auth/oauth2-strategy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AACzC,OAAO,KAAK,EAAE,YAAY,EAAuB,MAAM,kBAAkB,CAAC;AAC1E,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAK1D,OAAO,EAAE,UAAU,EAAgB,MAAM,oBAAoB,CAAC;AAC9D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAE1D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAEnD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,2BAA2B,CAAC;AAOrE,MAAM,WAAW,qBAAqB;IACpC,2DAA2D;IAC3D,WAAW,EAAE,eAAe,CAAC;IAC7B,mFAAmF;IACnF,QAAQ,EAAE,MAAM,CAAC;IACjB,uFAAuF;IACvF,YAAY,EAAE,MAAM,CAAC;IACrB,oEAAoE;IACpE,KAAK,CAAC,EAAE,UAAU,CAAC;IACnB,0DAA0D;IAC1D,KAAK,CAAC,EAAE,UAAU,CAAC;IACnB;;;OAGG;IACH,SAAS,CAAC,EAAE,eAAe,CAAC;IAC5B;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,wEAAwE;IACxE,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,oFAAoF;IACpF,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IACzB,0DAA0D;IAC1D,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,gDAAgD;IAChD,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,+EAA+E;IAC/E,OAAO,CAAC,EAAE,WAAW,CAAC;IACtB,oEAAoE;IACpE,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAMD;;;;;;;;;;;;;;GAcG;AACH,qBAAa,+BAAgC,YAAW,YAAY;IAClE,QAAQ,CAAC,IAAI,+BAA+B;IAE5C,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAkB;IACvC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;IACvC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAa;IACpC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAa;IACpC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAA8B;IACzD,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAS;IACxC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAe;IAC1C,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAyB;IACrD,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAe;IACpC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAc;IACvC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAqB;gBAEjC,IAAI,EAAE,qBAAqB;IAevC,0DAA0D;IAC1D,IAAI,UAAU,IAAI,UAAU,CAE3B;IAED,kEAAkE;IAClE,OAAO,KAAK,SAAS,GAEpB;IAED;;;;;;;;;;OAUG;IACG,cAAc,CAAC,IAAI,CAAC,EAAE;QAAE,YAAY,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,OAAO,CAAC,MAAM,CAAC;IAqCxE;;;;;;;;;;;OAWG;YACW,WAAW;YAOX,aAAa;CAoG5B"}
@@ -0,0 +1,217 @@
1
+ /**
2
+ * OAuth 2.0 client_credentials strategy for Viva Wallet ISV API calls.
3
+ *
4
+ * Handles the full token lifecycle: obtain, cache, refresh, and single-flight
5
+ * de-duplication to prevent stampede under concurrent load.
6
+ *
7
+ * @see references/viva-docs/md/oauth2-authentication.txt:128 (token endpoint)
8
+ * @see references/viva-docs/md/oauth2-authentication.txt:145 (demo/prod URLs)
9
+ * @see references/viva-docs/md/oauth2-authentication.txt:167 (grant_type, Basic auth header)
10
+ * @see references/viva-docs/md/oauth2-authentication.txt:179 (token response shape, expires_in)
11
+ * @see references/viva-docs/md/oauth2-authentication.txt:192 (3600s expiry)
12
+ */
13
+ import { ENVIRONMENT_URLS } from '../types/common.js';
14
+ import { VivaAuthError } from '../errors/auth-error.js';
15
+ import { VivaApiError } from '../errors/api-error.js';
16
+ import { VivaRateLimitError } from '../errors/rate-limit-error.js';
17
+ import { AsyncMutex, singleFlight } from './single-flight.js';
18
+ import { InMemoryTokenCache } from './token-cache.js';
19
+ import { getAuthDispatcher } from './http.js';
20
+ import { NoopMetricsHook } from '../observability/index.js';
21
+ // ---------------------------------------------------------------------------
22
+ // Strategy implementation
23
+ // ---------------------------------------------------------------------------
24
+ /**
25
+ * OAuth 2.0 `client_credentials` token strategy.
26
+ *
27
+ * Flow:
28
+ * 1. Check cache. If token is fresh (expires_at > now + refreshSkewMs), return it.
29
+ * 2. Acquire single-flight lock. Re-check cache (avoid duplicate fetch after waking).
30
+ * 3. POST `grant_type=client_credentials` to the token endpoint.
31
+ * 4. Cache result with `expires_at = now + (expires_in * 1000) - 60_000`.
32
+ *
33
+ * A1 property: `forceRefresh=true` (used by S3 on 401-recovery) ALSO acquires
34
+ * the single-flight lock, so even during concurrent secret rotation only one
35
+ * worker calls `/connect/token`. This is naturally satisfied because
36
+ * `getBearerToken` unconditionally enters `singleFlight` when the cache check
37
+ * is bypassed or fails.
38
+ */
39
+ export class OAuth2ClientCredentialsStrategy {
40
+ name = 'oauth2-client-credentials';
41
+ _env;
42
+ _clientId;
43
+ _clientSecret;
44
+ _cache;
45
+ _mutex;
46
+ _redisLock;
47
+ _refreshSkewMs;
48
+ _fetchImpl;
49
+ _dispatcher;
50
+ _now;
51
+ _metrics;
52
+ _logger;
53
+ constructor(opts) {
54
+ this._env = opts.environment;
55
+ this._clientId = opts.clientId;
56
+ this._clientSecret = opts.clientSecret;
57
+ this._cache = opts.cache ?? new InMemoryTokenCache();
58
+ this._mutex = opts.mutex ?? new AsyncMutex();
59
+ this._redisLock = opts.redisLock;
60
+ this._refreshSkewMs = opts.refreshSkewMs ?? 5 * 60_000;
61
+ this._fetchImpl = opts.fetchImpl ?? globalThis.fetch;
62
+ this._dispatcher = opts.dispatcher;
63
+ this._now = opts.now ?? (() => Date.now());
64
+ this._metrics = opts.metrics ?? new NoopMetricsHook();
65
+ this._logger = opts.logger;
66
+ }
67
+ /** Exposes the token cache for auth-status inspection. */
68
+ get tokenCache() {
69
+ return this._cache;
70
+ }
71
+ /** Cache key format: `viva:isv:token:{clientId}:{environment}` */
72
+ get _cacheKey() {
73
+ return `viva:isv:token:${this._clientId}:${this._env}`;
74
+ }
75
+ /**
76
+ * Returns a valid bearer token for use in `Authorization: Bearer <token>`.
77
+ *
78
+ * When `forceRefresh` is true the cache lookup is skipped but the request
79
+ * STILL goes through the single-flight lock — this is the A1 property:
80
+ * multiple workers calling `getBearerToken({ forceRefresh: true })` after
81
+ * receiving a 401 will not stampede the token endpoint.
82
+ * // A1: forceRefresh bypasses cache but not the lock — stampede prevention
83
+ * // holds even during secret rotation. All concurrent 401-recovery calls
84
+ * // will coalesce into one token fetch. (Plan amendment A1.)
85
+ */
86
+ async getBearerToken(opts) {
87
+ const forceRefresh = opts?.forceRefresh === true;
88
+ // Step 1: fast-path cache hit (skipped on forceRefresh).
89
+ if (!forceRefresh) {
90
+ const cached = await this._cache.get(this._cacheKey);
91
+ if (cached && cached.expires_at > this._now() + this._refreshSkewMs) {
92
+ return cached.access_token;
93
+ }
94
+ }
95
+ // Step 2–4: go through single-flight lock regardless of forceRefresh.
96
+ // A1: forceRefresh bypasses cache but not the lock — stampede prevention
97
+ // holds even during secret rotation. All concurrent 401-recovery calls
98
+ // coalesce into a single /connect/token fetch. (Plan amendment A1.)
99
+ const locks = this._redisLock
100
+ ? { redis: this._redisLock }
101
+ : { local: this._mutex };
102
+ return singleFlight(this._cacheKey, async () => {
103
+ // Re-check cache after acquiring the lock; another waiter may have
104
+ // already refreshed the token while we were queued.
105
+ if (!forceRefresh) {
106
+ const reChecked = await this._cache.get(this._cacheKey);
107
+ if (reChecked && reChecked.expires_at > this._now() + this._refreshSkewMs) {
108
+ return reChecked.access_token;
109
+ }
110
+ }
111
+ return this._fetchToken();
112
+ }, locks);
113
+ }
114
+ /**
115
+ * Fetches a fresh token from the Viva identity server.
116
+ *
117
+ * Endpoint:
118
+ * Demo: https://demo-accounts.vivapayments.com/connect/token
119
+ * Production: https://accounts.vivapayments.com/connect/token
120
+ *
121
+ * @see references/viva-docs/md/oauth2-authentication.txt:145 (endpoint URLs)
122
+ * @see references/viva-docs/md/oauth2-authentication.txt:167 (curl example: grant_type, Basic header)
123
+ * @see references/viva-docs/md/oauth2-authentication.txt:179 (response: access_token, expires_in)
124
+ * @see references/viva-docs/md/oauth2-authentication.txt:192 (3600s token lifetime)
125
+ */
126
+ async _fetchToken() {
127
+ return this._metrics.timeAsync('viva_auth_token_refresh_duration_seconds', () => this._doFetchToken());
128
+ }
129
+ async _doFetchToken() {
130
+ const { authBaseUrl } = ENVIRONMENT_URLS[this._env];
131
+ const url = `${authBaseUrl}/connect/token`;
132
+ // Base64-encode Client_ID:Client_Secret per Viva OAuth2 docs line 153.
133
+ // @see references/viva-docs/md/oauth2-authentication.txt:153
134
+ const credentials = Buffer.from(`${this._clientId}:${this._clientSecret}`).toString('base64');
135
+ const controller = new AbortController();
136
+ const timer = setTimeout(() => controller.abort(), 30_000);
137
+ const dispatcher = this._dispatcher ?? getAuthDispatcher(this._env);
138
+ let response;
139
+ try {
140
+ response = await this._fetchImpl(url, {
141
+ method: 'POST',
142
+ headers: {
143
+ // Authorization: Basic <base64(clientId:clientSecret)>
144
+ // @see references/viva-docs/md/oauth2-authentication.txt:166
145
+ Authorization: `Basic ${credentials}`,
146
+ // Content-Type required for form-encoded body.
147
+ // @see references/viva-docs/md/oauth2-authentication.txt:165
148
+ 'Content-Type': 'application/x-www-form-urlencoded',
149
+ Accept: 'application/json',
150
+ },
151
+ // grant_type=client_credentials
152
+ // @see references/viva-docs/md/oauth2-authentication.txt:167
153
+ body: 'grant_type=client_credentials',
154
+ signal: controller.signal,
155
+ // undici 7 supports the `dispatcher` option on global fetch.
156
+ // @ts-expect-error undici dispatcher option not in standard fetch types
157
+ dispatcher,
158
+ });
159
+ }
160
+ catch (err) {
161
+ clearTimeout(timer);
162
+ // Network / abort error — caller handles retry policy (plan Auth Flow line 316).
163
+ throw new VivaApiError({
164
+ message: `Network error fetching Viva token: ${String(err)}`,
165
+ cause: err,
166
+ });
167
+ }
168
+ clearTimeout(timer);
169
+ const requestId = response.headers.get('CorrelationId') ?? undefined;
170
+ if (response.status === 401 || response.status === 403) {
171
+ throw new VivaAuthError({
172
+ message: `Viva token endpoint returned ${response.status}. Check client credentials.`,
173
+ httpStatus: response.status,
174
+ requestId,
175
+ });
176
+ }
177
+ if (response.status === 429) {
178
+ const retryAfterHeader = response.headers.get('Retry-After');
179
+ const retryAfterMs = retryAfterHeader ? parseFloat(retryAfterHeader) * 1000 : undefined;
180
+ throw new VivaRateLimitError({
181
+ message: 'Viva token endpoint rate-limited (429).',
182
+ httpStatus: 429,
183
+ requestId,
184
+ retryAfterMs,
185
+ });
186
+ }
187
+ if (response.status >= 500) {
188
+ throw new VivaRateLimitError({
189
+ message: `Viva token endpoint server error (${response.status}). Retriable.`,
190
+ httpStatus: response.status,
191
+ requestId,
192
+ });
193
+ }
194
+ if (!response.ok) {
195
+ throw new VivaApiError({
196
+ message: `Unexpected status ${response.status} from Viva token endpoint.`,
197
+ httpStatus: response.status,
198
+ requestId,
199
+ });
200
+ }
201
+ // Parse the OAuth2TokenResponse.
202
+ // @see references/viva-docs/md/oauth2-authentication.txt:185 (response JSON shape)
203
+ const body = (await response.json());
204
+ // Compute expiry: now + (expires_in * 1000) - 60_000 (1-min safety margin).
205
+ // Plan Auth Flow step 4 / line 314: store expires_at = now + expires_in - 60s.
206
+ // @see references/viva-docs/md/oauth2-authentication.txt:192 (expires_in = 3600)
207
+ const expires_at = this._now() + body.expires_in * 1000 - 60_000;
208
+ await this._cache.set(this._cacheKey, {
209
+ access_token: body.access_token,
210
+ expires_at,
211
+ scope: body.scope,
212
+ });
213
+ this._logger?.info('viva:auth:token-refresh', { environment: this._env });
214
+ return body.access_token;
215
+ }
216
+ }
217
+ //# sourceMappingURL=oauth2-strategy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oauth2-strategy.js","sourceRoot":"","sources":["../../src/auth/oauth2-strategy.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Reseller basic-auth strategy for ISV endpoints that require it.
3
+ *
4
+ * Some ISV API calls (e.g. `cancelOrder`) require a Reseller-level basic-auth
5
+ * credential rather than the OAuth2 bearer token. This strategy computes the
6
+ * `Authorization: Basic <base64(merchantId:resellerApiKey)>` header value and
7
+ * returns it from `getBearerToken()` so the S3 HTTP client remains uniform —
8
+ * it always calls `getBearerToken()` and uses the result verbatim.
9
+ *
10
+ * Credential combination used:
11
+ * Username = merchantId, Password = resellerApiKey
12
+ *
13
+ * TODO(impl): verify exact merchantId:resellerApiKey concatenation against
14
+ * live demo. The ISV docs (`payment-isv-api.txt`) confirm Reseller ID +
15
+ * Merchant ID + Reseller API Key are required credentials but the precise
16
+ * Basic-auth encoding (which field is "username" vs "password") should be
17
+ * validated against a live Viva demo response before shipping.
18
+ * @see references/viva-docs/md/payment-isv-api.txt:1
19
+ */
20
+ import type { AuthStrategy } from '../types/auth.js';
21
+ export interface ResellerStrategyOptions {
22
+ /** ISV Partner Reseller ID provided by Viva. */
23
+ resellerId: string;
24
+ /** Target merchant's Merchant ID. */
25
+ merchantId: string;
26
+ /** ISV Partner Reseller API Key provided by Viva. */
27
+ resellerApiKey: string;
28
+ }
29
+ /**
30
+ * Returns a Basic-auth Authorization header value for ISV reseller calls.
31
+ *
32
+ * `getBearerToken()` returns `Basic <base64(merchantId:resellerApiKey)>`.
33
+ * The S3 layer sets `Authorization: <value>` — for this strategy the value
34
+ * includes the `Basic ` prefix so it can be used identically to a bearer value.
35
+ *
36
+ * `getAuthorizationHeader()` is exposed as a convenience alias.
37
+ */
38
+ export declare class ResellerBasicAuthStrategy implements AuthStrategy {
39
+ readonly name = "reseller-basic-auth";
40
+ private readonly _resellerId;
41
+ private readonly _merchantId;
42
+ private readonly _resellerApiKey;
43
+ constructor({ resellerId, merchantId, resellerApiKey }: ResellerStrategyOptions);
44
+ /**
45
+ * Returns the `Authorization` header value.
46
+ *
47
+ * Note: for this strategy the returned string includes the `Basic ` prefix
48
+ * (e.g. `"Basic dXNlcjpwYXNz"`). The S3 client places it verbatim in the
49
+ * Authorization header. This is intentional — the abstraction is "value to
50
+ * put in Authorization header", not "raw bearer token".
51
+ */
52
+ getBearerToken(_opts?: {
53
+ forceRefresh?: boolean;
54
+ }): Promise<string>;
55
+ /**
56
+ * Returns `Basic <base64(merchantId:resellerApiKey)>`.
57
+ *
58
+ * TODO(impl): verify merchantId:resellerApiKey encoding against live demo.
59
+ * @see references/viva-docs/md/payment-isv-api.txt:1
60
+ */
61
+ getAuthorizationHeader(): string;
62
+ /** Reseller ID accessor for S3 correlation (not part of AuthStrategy). */
63
+ get resellerId(): string;
64
+ }
65
+ //# sourceMappingURL=reseller-strategy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reseller-strategy.d.ts","sourceRoot":"","sources":["../../src/auth/reseller-strategy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAErD,MAAM,WAAW,uBAAuB;IACtC,gDAAgD;IAChD,UAAU,EAAE,MAAM,CAAC;IACnB,qCAAqC;IACrC,UAAU,EAAE,MAAM,CAAC;IACnB,qDAAqD;IACrD,cAAc,EAAE,MAAM,CAAC;CACxB;AAED;;;;;;;;GAQG;AACH,qBAAa,yBAA0B,YAAW,YAAY;IAC5D,QAAQ,CAAC,IAAI,yBAAyB;IAEtC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAS;gBAE7B,EAAE,UAAU,EAAE,UAAU,EAAE,cAAc,EAAE,EAAE,uBAAuB;IAM/E;;;;;;;OAOG;IACG,cAAc,CAAC,KAAK,CAAC,EAAE;QAAE,YAAY,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,OAAO,CAAC,MAAM,CAAC;IAIzE;;;;;OAKG;IACH,sBAAsB,IAAI,MAAM;IAUhC,0EAA0E;IAC1E,IAAI,UAAU,IAAI,MAAM,CAEvB;CACF"}
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Reseller basic-auth strategy for ISV endpoints that require it.
3
+ *
4
+ * Some ISV API calls (e.g. `cancelOrder`) require a Reseller-level basic-auth
5
+ * credential rather than the OAuth2 bearer token. This strategy computes the
6
+ * `Authorization: Basic <base64(merchantId:resellerApiKey)>` header value and
7
+ * returns it from `getBearerToken()` so the S3 HTTP client remains uniform —
8
+ * it always calls `getBearerToken()` and uses the result verbatim.
9
+ *
10
+ * Credential combination used:
11
+ * Username = merchantId, Password = resellerApiKey
12
+ *
13
+ * TODO(impl): verify exact merchantId:resellerApiKey concatenation against
14
+ * live demo. The ISV docs (`payment-isv-api.txt`) confirm Reseller ID +
15
+ * Merchant ID + Reseller API Key are required credentials but the precise
16
+ * Basic-auth encoding (which field is "username" vs "password") should be
17
+ * validated against a live Viva demo response before shipping.
18
+ * @see references/viva-docs/md/payment-isv-api.txt:1
19
+ */
20
+ /**
21
+ * Returns a Basic-auth Authorization header value for ISV reseller calls.
22
+ *
23
+ * `getBearerToken()` returns `Basic <base64(merchantId:resellerApiKey)>`.
24
+ * The S3 layer sets `Authorization: <value>` — for this strategy the value
25
+ * includes the `Basic ` prefix so it can be used identically to a bearer value.
26
+ *
27
+ * `getAuthorizationHeader()` is exposed as a convenience alias.
28
+ */
29
+ export class ResellerBasicAuthStrategy {
30
+ name = 'reseller-basic-auth';
31
+ _resellerId;
32
+ _merchantId;
33
+ _resellerApiKey;
34
+ constructor({ resellerId, merchantId, resellerApiKey }) {
35
+ this._resellerId = resellerId;
36
+ this._merchantId = merchantId;
37
+ this._resellerApiKey = resellerApiKey;
38
+ }
39
+ /**
40
+ * Returns the `Authorization` header value.
41
+ *
42
+ * Note: for this strategy the returned string includes the `Basic ` prefix
43
+ * (e.g. `"Basic dXNlcjpwYXNz"`). The S3 client places it verbatim in the
44
+ * Authorization header. This is intentional — the abstraction is "value to
45
+ * put in Authorization header", not "raw bearer token".
46
+ */
47
+ async getBearerToken(_opts) {
48
+ return this.getAuthorizationHeader();
49
+ }
50
+ /**
51
+ * Returns `Basic <base64(merchantId:resellerApiKey)>`.
52
+ *
53
+ * TODO(impl): verify merchantId:resellerApiKey encoding against live demo.
54
+ * @see references/viva-docs/md/payment-isv-api.txt:1
55
+ */
56
+ getAuthorizationHeader() {
57
+ // Encode merchantId:resellerApiKey in Base64 per HTTP Basic-auth spec.
58
+ // resellerId is stored for correlation / logging but is not part of the
59
+ // Basic credential header — it is typically a query or path parameter.
60
+ const encoded = Buffer.from(`${this._merchantId}:${this._resellerApiKey}`).toString('base64');
61
+ return `Basic ${encoded}`;
62
+ }
63
+ /** Reseller ID accessor for S3 correlation (not part of AuthStrategy). */
64
+ get resellerId() {
65
+ return this._resellerId;
66
+ }
67
+ }
68
+ //# sourceMappingURL=reseller-strategy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reseller-strategy.js","sourceRoot":"","sources":["../../src/auth/reseller-strategy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAaH;;;;;;;;GAQG;AACH,MAAM,OAAO,yBAAyB;IAC3B,IAAI,GAAG,qBAAqB,CAAC;IAErB,WAAW,CAAS;IACpB,WAAW,CAAS;IACpB,eAAe,CAAS;IAEzC,YAAY,EAAE,UAAU,EAAE,UAAU,EAAE,cAAc,EAA2B;QAC7E,IAAI,CAAC,WAAW,GAAG,UAAU,CAAC;QAC9B,IAAI,CAAC,WAAW,GAAG,UAAU,CAAC;QAC9B,IAAI,CAAC,eAAe,GAAG,cAAc,CAAC;IACxC,CAAC;IAED;;;;;;;OAOG;IACH,KAAK,CAAC,cAAc,CAAC,KAAkC;QACrD,OAAO,IAAI,CAAC,sBAAsB,EAAE,CAAC;IACvC,CAAC;IAED;;;;;OAKG;IACH,sBAAsB;QACpB,uEAAuE;QACvE,wEAAwE;QACxE,uEAAuE;QACvE,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,CACzB,GAAG,IAAI,CAAC,WAAW,IAAI,IAAI,CAAC,eAAe,EAAE,CAC9C,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;QACrB,OAAO,SAAS,OAAO,EAAE,CAAC;IAC5B,CAAC;IAED,0EAA0E;IAC1E,IAAI,UAAU;QACZ,OAAO,IAAI,CAAC,WAAW,CAAC;IAC1B,CAAC;CACF"}
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Single-flight primitives for token refresh.
3
+ *
4
+ * `AsyncMutex` serializes concurrent in-process callers on a single key.
5
+ * `singleFlight()` ensures that when N callers race, only one fn() executes
6
+ * and all N receive the same resolved value.
7
+ *
8
+ * For multi-worker deployments a Redis-based lock must be injected via
9
+ * `RedisLockClient`. The interface is declared here; no Redis runtime dep
10
+ * is included in this package — the SaaS layer provides the concrete impl.
11
+ *
12
+ * Plan ref (A1): forceRefresh on 401 recovery MUST go through the same
13
+ * single-flight to prevent token stampede during secret rotation.
14
+ */
15
+ export interface RedisLockClient {
16
+ /**
17
+ * Try to acquire a distributed lock for `key` with the given TTL.
18
+ * Returns `{ token }` on success, `null` if the lock is already held.
19
+ */
20
+ acquire(key: string, ttlMs: number): Promise<{
21
+ token: string;
22
+ } | null>;
23
+ /** Release the lock identified by `key` and `token`. */
24
+ release(key: string, token: string): Promise<void>;
25
+ /**
26
+ * Poll `getter` every `intervalMs` until it returns a non-null value
27
+ * (meaning another worker wrote the result to cache) or `timeoutMs` elapses.
28
+ */
29
+ pollCacheUntil<T>(getter: () => Promise<T | null>, intervalMs: number, timeoutMs: number): Promise<T | null>;
30
+ }
31
+ /**
32
+ * A no-op sentinel implementation that throws if actually invoked.
33
+ * Satisfies the type when Redis is not configured; safe to pass as default
34
+ * so callers don't need to guard for undefined.
35
+ */
36
+ export declare const noopRedisLock: RedisLockClient;
37
+ /**
38
+ * A simple in-process serialising mutex.
39
+ *
40
+ * `acquire()` returns a release function. Callers MUST call the release
41
+ * function in a `finally` block to avoid deadlocks.
42
+ *
43
+ * Internally, each acquire appends a promise to a chain. When the current
44
+ * holder calls release it resolves the next waiter.
45
+ */
46
+ export declare class AsyncMutex {
47
+ private _tail;
48
+ /**
49
+ * Acquires the mutex. Resolves when this caller holds the lock.
50
+ * Returns a `release` function that MUST be called in `finally`.
51
+ */
52
+ acquire(): Promise<() => void>;
53
+ }
54
+ type Locks = {
55
+ local: AsyncMutex;
56
+ } | {
57
+ redis: RedisLockClient;
58
+ };
59
+ /**
60
+ * Ensures that concurrent calls with the same `key` execute `fn` exactly once.
61
+ * All concurrent waiters share the same promise and receive the same result.
62
+ *
63
+ * Design: the in-flight map is checked and populated BEFORE acquiring the mutex.
64
+ * - If an entry exists: await it directly (no mutex needed, no fn() call).
65
+ * - If no entry: create the shared promise, store it, then acquire the mutex
66
+ * for the re-check + fn() execution. Other concurrent callers that arrive
67
+ * between "check" and "set" will see the entry in the map on their first
68
+ * check and coalesce.
69
+ *
70
+ * When a `{ local: AsyncMutex }` is provided the mutex is used to serialise
71
+ * the actual fn() execution. The map handles the N-1 waiters.
72
+ *
73
+ * When a `{ redis: RedisLockClient }` is provided the first worker to acquire
74
+ * the Redis lock runs fn(). Other workers (including other processes) poll
75
+ * the cache via `pollCacheUntil` until the result is available.
76
+ *
77
+ * @see plan line 311 — Redis lock key pattern: `viva:isv:token:lock:{client_id}`
78
+ */
79
+ export declare function singleFlight<T>(key: string, fn: () => Promise<T>, locks: Locks): Promise<T>;
80
+ export {};
81
+ //# sourceMappingURL=single-flight.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"single-flight.d.ts","sourceRoot":"","sources":["../../src/auth/single-flight.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAMH,MAAM,WAAW,eAAe;IAC9B;;;OAGG;IACH,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,KAAK,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC,CAAC;IAEvE,wDAAwD;IACxD,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAEnD;;;OAGG;IACH,cAAc,CAAC,CAAC,EACd,MAAM,EAAE,MAAM,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,EAC/B,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;CACtB;AAED;;;;GAIG;AACH,eAAO,MAAM,aAAa,EAAE,eAU3B,CAAC;AAMF;;;;;;;;GAQG;AACH,qBAAa,UAAU;IACrB,OAAO,CAAC,KAAK,CAAoC;IAEjD;;;OAGG;IACH,OAAO,IAAI,OAAO,CAAC,MAAM,IAAI,CAAC;CAiB/B;AAMD,KAAK,KAAK,GAAG;IAAE,KAAK,EAAE,UAAU,CAAA;CAAE,GAAG;IAAE,KAAK,EAAE,eAAe,CAAA;CAAE,CAAC;AAWhE;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,YAAY,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,KAAK,GAAG,OAAO,CAAC,CAAC,CAAC,CAqE3F"}