@augustdigital/sdk 9.0.0 → 9.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.
@@ -8,6 +8,7 @@ const app_name_1 = require("./analytics/app-name");
8
8
  const version_check_1 = require("./version-check");
9
9
  const fetcher_1 = require("./fetcher");
10
10
  const attribution_1 = require("./attribution");
11
+ const curator_alert_1 = require("./logger/curator-alert");
11
12
  /**
12
13
  * Base class providing core SDK functionality including provider management,
13
14
  * network switching, and authentication state.
@@ -74,6 +75,16 @@ class AugustBase {
74
75
  // `publicApiBaseUrl`: called unconditionally so a prior instance's
75
76
  // builder codes never leak into a later instance in the same process.
76
77
  (0, attribution_1.setAttribution)(attribution ?? null);
78
+ // Same reset-on-omit contract, for the same reason: an instance that says
79
+ // nothing about curator alerts gets the default (on in production), not
80
+ // whatever a previously-constructed instance asked for.
81
+ // Reports follow the API this instance reads from — resolved AFTER
82
+ // `setPublicApiBaseUrl` above, so a staging deployment alerts staging.
83
+ (0, curator_alert_1.configureCuratorAlerts)(monitoring?.curatorAlerts ?? null, {
84
+ appName: this.appName,
85
+ environment: environment,
86
+ apiBaseUrl: (0, fetcher_1.getPublicApiBaseUrl)(),
87
+ });
77
88
  }
78
89
  /**
79
90
  * Verify API keys and authorize SDK usage.
@@ -0,0 +1,217 @@
1
+ import type { ChainType, IEnv } from '../../types';
2
+ /**
3
+ * Path on the August public API that accepts a failed-redemption report. Chain
4
+ * lives in the payload, not the path — see {@link IRedeemFailureEvent.chain}.
5
+ */
6
+ export declare const CURATOR_ALERT_PATH = "/alerts/redeem-failure";
7
+ /**
8
+ * Where a redeem failure is reported: the August public API, which is already
9
+ * this SDK's read origin and already serves browsers.
10
+ *
11
+ * Browser delivery depends on CORS, and the API's allowlist is an enumerated
12
+ * list of partner origins (`ALLOWED_ORIGIN_REGEX` in the backend), not a
13
+ * wildcard. This POST sends `Content-Type: application/json`, so it is
14
+ * preflighted; an integration served from an origin that is not on that list
15
+ * loses every alert at the preflight. Onboarding a curator therefore has two
16
+ * halves — enabling the vault and allowlisting the origin their users load.
17
+ *
18
+ * That service is the trust boundary, not the destination: it validates the
19
+ * report, drops any vault it does not know or that has not opted in,
20
+ * rate-limits per wallet and per vault, and only then forwards to the Telegram
21
+ * bot that holds the token and the curator's channel. Nothing
22
+ * credential-shaped ships in this bundle, which is also why this endpoint
23
+ * takes no API key — a key in a browser bundle is a published key
24
+ * (CLAUDE.md §5). See {@link reportRedeemFailure} for the trust model.
25
+ */
26
+ export declare const DEFAULT_CURATOR_ALERT_ENDPOINT: string;
27
+ /** Env equivalent of `monitoring.curatorAlerts.enabled: false`. Node only. */
28
+ export declare const CURATOR_ALERTS_DISABLE_ENV_VAR = "AUGUST_SDK_DISABLE_CURATOR_ALERTS";
29
+ /** Curator-alert reporting. Enabled by default; this is the opt-out. */
30
+ export interface ICuratorAlertsConfig {
31
+ /**
32
+ * `false` stops emitting entirely. `true` forces emitting even in an
33
+ * environment the SDK would otherwise stay quiet in — it bypasses **every**
34
+ * environment gate: `NODE_ENV` of `development`/`test`, a browser served
35
+ * from a loopback or private host, and `monitoring.env` other than `PROD`.
36
+ * It does NOT lift the mainnet-only restriction; only `endpoint` does. A
37
+ * `DEV` or test
38
+ * integration that sets it and then fails a mainnet redeem pages a real
39
+ * curator, so use it only when deliberately exercising the path (pair it
40
+ * with `endpoint` to aim at a test relay). The only thing that still wins is
41
+ * the {@link CURATOR_ALERTS_DISABLE_ENV_VAR} kill switch.
42
+ */
43
+ enabled?: boolean;
44
+ /**
45
+ * Override the relay endpoint. Defaults to
46
+ * {@link DEFAULT_CURATOR_ALERT_ENDPOINT}. Setting it also lifts the
47
+ * mainnet-only restriction, so a staging relay can receive testnet failures.
48
+ */
49
+ endpoint?: string;
50
+ }
51
+ /** Fields every report carries, whatever stage the redemption failed at. */
52
+ interface IRedeemFailureBase {
53
+ /**
54
+ * Chain family the vault lives on. Carried rather than assumed so the
55
+ * receiving endpoint stays one contract for every chain; it decides which
56
+ * families it actually serves, and validates the identifiers below by that
57
+ * family's rules.
58
+ */
59
+ chain: ChainType;
60
+ /**
61
+ * Network within the family, in that family's own vocabulary — Stellar's
62
+ * `mainnet`/`testnet`, Solana's `mainnet-beta`, an EVM network name. A plain
63
+ * string rather than one family's union, because the receiving endpoint
64
+ * validates it against the family named in `chain`; typing it as Stellar's
65
+ * two values would make this the narrower half of a contract the rest of this
66
+ * module describes as chain-agnostic.
67
+ */
68
+ network: string;
69
+ /** Vault identifier in the chain's own format. The endpoint routes on this. */
70
+ contractId: string;
71
+ /** Account that attempted the redemption, in the chain's own format. */
72
+ eoa: string;
73
+ /** Shares requested, in the vault's smallest unit (unscaled). */
74
+ sharesRaw: string;
75
+ /** Failure detail; sanitized and capped before it leaves the process. */
76
+ error: string;
77
+ }
78
+ /**
79
+ * A redemption the vault refused before anything reached chain — a failed
80
+ * simulation, or a vault whose ledger state needs restoring. There is no
81
+ * on-chain artifact, so the receiving endpoint has nothing to verify the claim
82
+ * against and treats it as the weaker of the two. Which build stage raised it
83
+ * stays legible in `error`.
84
+ */
85
+ interface IRedeemBuildFailure extends IRedeemFailureBase {
86
+ phase: 'simulation';
87
+ }
88
+ /**
89
+ * A redemption that executed on chain and failed. `txHash` is what makes this
90
+ * the stronger claim — the endpoint can verify it — so it is required here
91
+ * rather than optional, which is the whole reason these are two types.
92
+ */
93
+ interface IRedeemSubmissionFailure extends IRedeemFailureBase {
94
+ phase: 'submission';
95
+ /** The failed transaction. Verifiable, which is why it is not optional. */
96
+ txHash: string;
97
+ /** Transaction result code, when the result XDR decoded. */
98
+ resultCode?: string;
99
+ }
100
+ /**
101
+ * A redemption failure worth a curator's attention.
102
+ *
103
+ * Two members rather than one shape with optional fields, because `txHash` is
104
+ * only meaningful — and is always available — on the submission path. As a
105
+ * single interface, `{ phase: 'simulation', txHash }` and a submission report
106
+ * with no hash both type-checked, and the receiving endpoint's `is_well_formed`
107
+ * check was the only thing between either and a silently dropped alert. That
108
+ * check stays, since a server cannot trust its input, but it is no longer
109
+ * covering for a shape this SDK could have made unrepresentable. The two call
110
+ * sites in `adapters/stellar/` are the ones this actually constrains, and they
111
+ * are type-checked by `pnpm build`.
112
+ *
113
+ * @internal
114
+ */
115
+ export type IRedeemFailureEvent = IRedeemBuildFailure | IRedeemSubmissionFailure;
116
+ /** Ambient facts about the reporting SDK instance, supplied by `AugustBase`. */
117
+ export interface ICuratorAlertsContext {
118
+ /** Carried on the payload so August can tell partner traffic apart. */
119
+ appName?: string;
120
+ /**
121
+ * `monitoring.env`. Anything other than `'PROD'` keeps the SDK quiet unless
122
+ * `config.enabled` is explicitly `true`.
123
+ */
124
+ environment?: IEnv;
125
+ /**
126
+ * Effective base URL of the August public API. Reports follow the API this
127
+ * instance reads from, so a deployment pointed at a staging backend alerts
128
+ * staging rather than production.
129
+ */
130
+ apiBaseUrl?: string;
131
+ }
132
+ /**
133
+ * Apply constructor config. Called unconditionally by `AugustBase` so an
134
+ * instance that omits `curatorAlerts` RESETS to the defaults rather than
135
+ * inheriting a prior instance's settings — same contract as
136
+ * `setPublicApiBaseUrl` and `setAttribution`.
137
+ *
138
+ * Also the supported opt-out for code that calls the `Stellar` namespace
139
+ * functions directly without constructing an `AugustSDK` (the testnet flow in
140
+ * the docs), and the only one that works in a browser, where the env var is
141
+ * unreadable.
142
+ *
143
+ * @param config - `monitoring.curatorAlerts`, or `null` to restore defaults.
144
+ * @param context - Facts about the reporting instance; omitted entirely by a
145
+ * caller that only wants to disable reporting.
146
+ */
147
+ export declare function configureCuratorAlerts(config: ICuratorAlertsConfig | null, context?: ICuratorAlertsContext): void;
148
+ /**
149
+ * Drop every suppression key. Test-only seam — reconfiguration no longer
150
+ * clears the window on its own when the configuration is unchanged, so a suite
151
+ * that reports the same failure across cases needs an explicit reset.
152
+ * @internal
153
+ */
154
+ export declare function resetCuratorAlertDedupe(): void;
155
+ /**
156
+ * Whether an alert would be sent right now.
157
+ *
158
+ * Quiet by default outside production, mirroring `analytics`: a partner's Jest
159
+ * suite or `next dev` session exercising a failing redeem must not page a real
160
+ * curator. Two gates are needed for that, because they cover different
161
+ * runtimes — `NODE_ENV` is the Node one, and bundlers strip or stub
162
+ * `process.env` in the browser, so a locally-served app is recognised by its
163
+ * hostname instead (CLAUDE.md §5). Without that gate, a browser app run
164
+ * against mainnet from any loopback or private origin — `localhost`, a
165
+ * container on `172.20.0.0/16`, `[::1]` — pages a real curator on every
166
+ * failed redeem. `isLocalhost` in `core/analytics/env` lists the ranges.
167
+ *
168
+ * `enabled: true` overrides both — it is the documented way to exercise the
169
+ * path deliberately, and the SDK's own suite relies on it.
170
+ *
171
+ * The env var is read per call (not once at module load) so a consumer that
172
+ * sets it after import is still honoured, and a test can set and unset it
173
+ * without re-importing. It deliberately outranks an explicit `enabled: true`:
174
+ * it is the kill switch, and an operator setting it should not have to find
175
+ * and edit the construction site to be obeyed.
176
+ */
177
+ export declare function areCuratorAlertsEnabled(): boolean;
178
+ /**
179
+ * Report a failed vault redemption for the curator of that vault.
180
+ *
181
+ * **Why this exists.** Stellar vaults are instant-redeem only — there is no
182
+ * request/queue path — so a curator whose users' redemptions are failing has
183
+ * no signal at all today: the failure surfaces to the user, and to August's
184
+ * internal Sentry, and stops there. Emitting from the SDK rather than from a
185
+ * single frontend means any integration can report one (AUGUST-7162) — subject
186
+ * to the CORS caveat on {@link DEFAULT_CURATOR_ALERT_ENDPOINT}.
187
+ *
188
+ * **Trust model.** The endpoint is public by construction — it ships in this
189
+ * bundle — so the relay treats every event as untrusted input, not as an
190
+ * attested fact. The SDK's job is to report accurately; the relay's job is to
191
+ * verify, rate-limit per `{vault, wallet}`, and drop events naming a vault
192
+ * outside its routing map. `'submission'` events carry a `txHash` the relay can
193
+ * confirm on chain, which makes them unforgeable; `'simulation'` events have no
194
+ * on-chain artifact and are treated as the weaker claim. Nothing
195
+ * credential-shaped is reachable from here: no bot token, no chat id, no
196
+ * curator identity.
197
+ *
198
+ * **Side effects.** At most one `POST` to the relay, fire-and-forget: this runs
199
+ * inside a `catch` on a money path, so it never awaits, never throws back into
200
+ * the caller, and never alters the error the caller sees. Suppressed to one
201
+ * send per `{contractId, eoa, phase, resultCode, error class}` per 10 minutes,
202
+ * so a wallet retrying a deterministic failure costs a single request. Note the
203
+ * receiving service applies its own, coarser window on top. A no-op when
204
+ * reporting is disabled (see {@link areCuratorAlertsEnabled}) and, unless a
205
+ * custom endpoint is configured, for any network other than mainnet — a
206
+ * testnet failure is not a curator's problem.
207
+ *
208
+ * @param event - The failed attempt. `error` is sanitized (secrets scrubbed via
209
+ * `sanitizeString`) and capped at 1500 characters before it leaves the
210
+ * process; `sharesRaw` is relayed unscaled, and the receiving service scales
211
+ * it for display with the decimals it holds for that vault — which for an
212
+ * offset vault are the asset's, not the share token's, so the figure a curator
213
+ * sees is a magnitude rather than an exact amount (see AUGUST-6381).
214
+ * @internal
215
+ */
216
+ export declare function reportRedeemFailure(event: IRedeemFailureEvent): void;
217
+ export {};
@@ -0,0 +1,276 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CURATOR_ALERTS_DISABLE_ENV_VAR = exports.DEFAULT_CURATOR_ALERT_ENDPOINT = exports.CURATOR_ALERT_PATH = void 0;
4
+ exports.configureCuratorAlerts = configureCuratorAlerts;
5
+ exports.resetCuratorAlertDedupe = resetCuratorAlertDedupe;
6
+ exports.areCuratorAlertsEnabled = areCuratorAlertsEnabled;
7
+ exports.reportRedeemFailure = reportRedeemFailure;
8
+ const lru_cache_1 = require("lru-cache");
9
+ const env_1 = require("../analytics/env");
10
+ const sanitize_1 = require("../analytics/sanitize");
11
+ const core_1 = require("../constants/core");
12
+ /**
13
+ * Path on the August public API that accepts a failed-redemption report. Chain
14
+ * lives in the payload, not the path — see {@link IRedeemFailureEvent.chain}.
15
+ */
16
+ exports.CURATOR_ALERT_PATH = '/alerts/redeem-failure';
17
+ /**
18
+ * Where a redeem failure is reported: the August public API, which is already
19
+ * this SDK's read origin and already serves browsers.
20
+ *
21
+ * Browser delivery depends on CORS, and the API's allowlist is an enumerated
22
+ * list of partner origins (`ALLOWED_ORIGIN_REGEX` in the backend), not a
23
+ * wildcard. This POST sends `Content-Type: application/json`, so it is
24
+ * preflighted; an integration served from an origin that is not on that list
25
+ * loses every alert at the preflight. Onboarding a curator therefore has two
26
+ * halves — enabling the vault and allowlisting the origin their users load.
27
+ *
28
+ * That service is the trust boundary, not the destination: it validates the
29
+ * report, drops any vault it does not know or that has not opted in,
30
+ * rate-limits per wallet and per vault, and only then forwards to the Telegram
31
+ * bot that holds the token and the curator's channel. Nothing
32
+ * credential-shaped ships in this bundle, which is also why this endpoint
33
+ * takes no API key — a key in a browser bundle is a published key
34
+ * (CLAUDE.md §5). See {@link reportRedeemFailure} for the trust model.
35
+ */
36
+ exports.DEFAULT_CURATOR_ALERT_ENDPOINT = `${core_1.WEBSERVER_URL.public}${exports.CURATOR_ALERT_PATH}`;
37
+ /** Env equivalent of `monitoring.curatorAlerts.enabled: false`. Node only. */
38
+ exports.CURATOR_ALERTS_DISABLE_ENV_VAR = 'AUGUST_SDK_DISABLE_CURATOR_ALERTS';
39
+ /** Suppression window per `{vault, wallet, phase, result code, error class}`. */
40
+ const ALERT_DEDUPE_TTL_MS = 10 * 60 * 1000;
41
+ /**
42
+ * Diagnostic tail kept on the relayed error. Bounds a field that is unbounded
43
+ * by nature (a Soroban diagnostic event log) before it leaves the process. The
44
+ * receiving service applies its own, smaller display cap for the Telegram
45
+ * message, so this is the transport ceiling, not the one a curator sees.
46
+ */
47
+ const MAX_ERROR_CHARS = 1500;
48
+ const SEND_TIMEOUT_MS = 5000;
49
+ /**
50
+ * Dedupe keys only — deliberately NOT the shared `core/cache.ts` LRU. That one
51
+ * is a 1000-entry cache shared with RPC and metadata memoization, so ordinary
52
+ * traffic could evict a suppression key mid-window (re-opening the flood this
53
+ * exists to stop) and a vault-wide incident's keys could evict useful reads.
54
+ */
55
+ const alertDedupe = new lru_cache_1.LRUCache({
56
+ max: 200,
57
+ ttl: ALERT_DEDUPE_TTL_MS,
58
+ });
59
+ let explicitEnabled;
60
+ let endpoint = exports.DEFAULT_CURATOR_ALERT_ENDPOINT;
61
+ let endpointOverridden = false;
62
+ let reporterAppName;
63
+ let reporterEnv;
64
+ let configSignature;
65
+ /**
66
+ * Apply constructor config. Called unconditionally by `AugustBase` so an
67
+ * instance that omits `curatorAlerts` RESETS to the defaults rather than
68
+ * inheriting a prior instance's settings — same contract as
69
+ * `setPublicApiBaseUrl` and `setAttribution`.
70
+ *
71
+ * Also the supported opt-out for code that calls the `Stellar` namespace
72
+ * functions directly without constructing an `AugustSDK` (the testnet flow in
73
+ * the docs), and the only one that works in a browser, where the env var is
74
+ * unreadable.
75
+ *
76
+ * @param config - `monitoring.curatorAlerts`, or `null` to restore defaults.
77
+ * @param context - Facts about the reporting instance; omitted entirely by a
78
+ * caller that only wants to disable reporting.
79
+ */
80
+ function configureCuratorAlerts(config, context = {}) {
81
+ const override = config?.endpoint?.trim();
82
+ const nextEndpoint = override ||
83
+ (context.apiBaseUrl
84
+ ? `${context.apiBaseUrl.replace(/\/+$/, '')}${exports.CURATOR_ALERT_PATH}`
85
+ : exports.DEFAULT_CURATOR_ALERT_ENDPOINT);
86
+ // Suppression state belongs to the configuration that produced it, so a
87
+ // redirected endpoint or a flipped switch starts with a clean window. But
88
+ // this runs on EVERY `AugustBase` construction, and an app that builds the
89
+ // SDK inside a render rather than a memo would otherwise reset flood control
90
+ // on every attempt — so an identical reconfiguration keeps the window.
91
+ const signature = `${nextEndpoint}|${config?.enabled}|${context.environment}`;
92
+ if (signature !== configSignature) {
93
+ alertDedupe.clear();
94
+ configSignature = signature;
95
+ }
96
+ explicitEnabled = config?.enabled;
97
+ endpointOverridden = Boolean(override);
98
+ endpoint = nextEndpoint;
99
+ reporterAppName = context.appName;
100
+ reporterEnv = context.environment;
101
+ }
102
+ /**
103
+ * Drop every suppression key. Test-only seam — reconfiguration no longer
104
+ * clears the window on its own when the configuration is unchanged, so a suite
105
+ * that reports the same failure across cases needs an explicit reset.
106
+ * @internal
107
+ */
108
+ function resetCuratorAlertDedupe() {
109
+ alertDedupe.clear();
110
+ }
111
+ function isTruthyEnv(value) {
112
+ if (!value)
113
+ return false;
114
+ const v = value.toLowerCase();
115
+ return v === '1' || v === 'true' || v === 'yes' || v === 'on';
116
+ }
117
+ /**
118
+ * Whether an alert would be sent right now.
119
+ *
120
+ * Quiet by default outside production, mirroring `analytics`: a partner's Jest
121
+ * suite or `next dev` session exercising a failing redeem must not page a real
122
+ * curator. Two gates are needed for that, because they cover different
123
+ * runtimes — `NODE_ENV` is the Node one, and bundlers strip or stub
124
+ * `process.env` in the browser, so a locally-served app is recognised by its
125
+ * hostname instead (CLAUDE.md §5). Without that gate, a browser app run
126
+ * against mainnet from any loopback or private origin — `localhost`, a
127
+ * container on `172.20.0.0/16`, `[::1]` — pages a real curator on every
128
+ * failed redeem. `isLocalhost` in `core/analytics/env` lists the ranges.
129
+ *
130
+ * `enabled: true` overrides both — it is the documented way to exercise the
131
+ * path deliberately, and the SDK's own suite relies on it.
132
+ *
133
+ * The env var is read per call (not once at module load) so a consumer that
134
+ * sets it after import is still honoured, and a test can set and unset it
135
+ * without re-importing. It deliberately outranks an explicit `enabled: true`:
136
+ * it is the kill switch, and an operator setting it should not have to find
137
+ * and edit the construction site to be obeyed.
138
+ */
139
+ function areCuratorAlertsEnabled() {
140
+ if (explicitEnabled === false)
141
+ return false;
142
+ if (isTruthyEnv((0, env_1.readEnv)(exports.CURATOR_ALERTS_DISABLE_ENV_VAR)))
143
+ return false;
144
+ if (explicitEnabled === true)
145
+ return true;
146
+ if (reporterEnv && reporterEnv !== 'PROD')
147
+ return false;
148
+ return !(0, env_1.isNodeDevOrTestEnv)() && !(0, env_1.isLocalhost)();
149
+ }
150
+ /**
151
+ * Collapse an error to the failure mode it represents, so one wallet retrying
152
+ * the same failing redeem produces one alert — but two *different* failures on
153
+ * that vault still produce two.
154
+ *
155
+ * Volatile or semantic is decided by shape:
156
+ * - long hex (XDR blobs, hashes) and 56-char Stellar strkeys → volatile;
157
+ * - runs of 4+ digits (amounts, balances, ledger sequences) → volatile;
158
+ * - a `#`-prefixed number → semantic, kept whatever its length. A Soroban
159
+ * contract code IS the failure's identity: merging `Error(Contract, #408)`
160
+ * (insufficient shares) with `#10` (balance out of range) — or two 4-digit
161
+ * codes with each other — would hide the second for the whole window.
162
+ */
163
+ function errorSignature(error) {
164
+ return error
165
+ .replace(/0x[0-9a-fA-F]{8,}/g, '0x')
166
+ .replace(/\b[A-Z2-7]{56}\b/g, 'S')
167
+ .replace(/(?<![0-9a-fA-Fx#-])\d{4,}/g, 'N')
168
+ .replace(/\s+/g, ' ')
169
+ .trim()
170
+ .slice(0, 200);
171
+ }
172
+ /**
173
+ * Report a failed vault redemption for the curator of that vault.
174
+ *
175
+ * **Why this exists.** Stellar vaults are instant-redeem only — there is no
176
+ * request/queue path — so a curator whose users' redemptions are failing has
177
+ * no signal at all today: the failure surfaces to the user, and to August's
178
+ * internal Sentry, and stops there. Emitting from the SDK rather than from a
179
+ * single frontend means any integration can report one (AUGUST-7162) — subject
180
+ * to the CORS caveat on {@link DEFAULT_CURATOR_ALERT_ENDPOINT}.
181
+ *
182
+ * **Trust model.** The endpoint is public by construction — it ships in this
183
+ * bundle — so the relay treats every event as untrusted input, not as an
184
+ * attested fact. The SDK's job is to report accurately; the relay's job is to
185
+ * verify, rate-limit per `{vault, wallet}`, and drop events naming a vault
186
+ * outside its routing map. `'submission'` events carry a `txHash` the relay can
187
+ * confirm on chain, which makes them unforgeable; `'simulation'` events have no
188
+ * on-chain artifact and are treated as the weaker claim. Nothing
189
+ * credential-shaped is reachable from here: no bot token, no chat id, no
190
+ * curator identity.
191
+ *
192
+ * **Side effects.** At most one `POST` to the relay, fire-and-forget: this runs
193
+ * inside a `catch` on a money path, so it never awaits, never throws back into
194
+ * the caller, and never alters the error the caller sees. Suppressed to one
195
+ * send per `{contractId, eoa, phase, resultCode, error class}` per 10 minutes,
196
+ * so a wallet retrying a deterministic failure costs a single request. Note the
197
+ * receiving service applies its own, coarser window on top. A no-op when
198
+ * reporting is disabled (see {@link areCuratorAlertsEnabled}) and, unless a
199
+ * custom endpoint is configured, for any network other than mainnet — a
200
+ * testnet failure is not a curator's problem.
201
+ *
202
+ * @param event - The failed attempt. `error` is sanitized (secrets scrubbed via
203
+ * `sanitizeString`) and capped at 1500 characters before it leaves the
204
+ * process; `sharesRaw` is relayed unscaled, and the receiving service scales
205
+ * it for display with the decimals it holds for that vault — which for an
206
+ * offset vault are the asset's, not the share token's, so the figure a curator
207
+ * sees is a magnitude rather than an exact amount (see AUGUST-6381).
208
+ * @internal
209
+ */
210
+ function reportRedeemFailure(event) {
211
+ if (!areCuratorAlertsEnabled())
212
+ return;
213
+ if (!event.contractId || !event.eoa || !event.error || !event.sharesRaw) {
214
+ return;
215
+ }
216
+ // Production networks only. Every family names its production network
217
+ // `mainnet` except Solana (`mainnet-beta`), which is why this is a prefix
218
+ // test rather than an equality one — a testnet failure is not a curator's
219
+ // problem, and reporting it would spend a real vault's alert budget.
220
+ if (!event.network.startsWith('mainnet') && !endpointOverridden)
221
+ return;
222
+ const safeError = (0, sanitize_1.sanitizeString)(String(event.error)).slice(0, MAX_ERROR_CHARS);
223
+ // The two fields that exist only on the submission member, read once through
224
+ // the discriminant so everything below stays shape-agnostic.
225
+ const submission = event.phase === 'submission' ? event : undefined;
226
+ const key = [
227
+ event.contractId,
228
+ event.eoa,
229
+ event.phase,
230
+ submission?.resultCode ?? '',
231
+ errorSignature(safeError),
232
+ ].join('|');
233
+ if (alertDedupe.has(key))
234
+ return;
235
+ alertDedupe.set(key, true);
236
+ // Coerced rather than trusted: this type is `@internal` but deep-importable,
237
+ // so an untyped consumer can reach it with a `bigint`, which `JSON.stringify`
238
+ // rejects. Emptiness is already handled by the guard above.
239
+ const body = JSON.stringify({
240
+ event: 'vault.redeem.failed',
241
+ phase: event.phase,
242
+ chain: event.chain,
243
+ network: event.network,
244
+ contractId: event.contractId,
245
+ eoa: event.eoa,
246
+ sharesRaw: String(event.sharesRaw),
247
+ txHash: submission?.txHash,
248
+ resultCode: submission?.resultCode,
249
+ error: safeError,
250
+ timestamp: new Date().toISOString(),
251
+ appName: reporterAppName,
252
+ });
253
+ (async () => {
254
+ const res = await fetch(endpoint, {
255
+ method: 'POST',
256
+ headers: { 'Content-Type': 'application/json' },
257
+ body,
258
+ signal: AbortSignal.timeout(SEND_TIMEOUT_MS),
259
+ });
260
+ const status = res?.status ?? 0;
261
+ if (status >= 200 && status < 300)
262
+ return;
263
+ // A 4xx is the endpoint answering: the report was rejected (422), or we are
264
+ // being throttled (429). Keep the suppression key — retrying a rejected
265
+ // report cannot make it acceptable, and answering a rate limit by removing
266
+ // our own backpressure is the wrong direction (CLAUDE.md §4.2). A 5xx or a
267
+ // transport failure means the report never landed, so release it.
268
+ if (status < 400 || status >= 500)
269
+ alertDedupe.delete(key);
270
+ console.warn('#CuratorAlert.stellarRedeemFailure:', status, res?.statusText);
271
+ })().catch((e) => {
272
+ alertDedupe.delete(key);
273
+ console.warn('#CuratorAlert.redeemFailure.unreachable:', (0, sanitize_1.sanitizeString)(String(e?.message ?? e)));
274
+ });
275
+ }
276
+ //# sourceMappingURL=curator-alert.js.map
@@ -96,3 +96,4 @@ declare const Logger: {
96
96
  };
97
97
  import * as slack from './slack';
98
98
  export { slack as Slack, Logger };
99
+ export { configureCuratorAlerts, areCuratorAlertsEnabled, CURATOR_ALERTS_DISABLE_ENV_VAR, type ICuratorAlertsConfig, type ICuratorAlertsContext, } from './curator-alert';
@@ -33,7 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.Logger = exports.Slack = void 0;
36
+ exports.CURATOR_ALERTS_DISABLE_ENV_VAR = exports.areCuratorAlertsEnabled = exports.configureCuratorAlerts = exports.Logger = exports.Slack = void 0;
37
37
  const sanitize_1 = require("../analytics/sanitize");
38
38
  let logger = null;
39
39
  let structuredLogger = null;
@@ -141,4 +141,23 @@ const Logger = {
141
141
  exports.Logger = Logger;
142
142
  const slack = __importStar(require("./slack"));
143
143
  exports.Slack = slack;
144
+ // Only the opt-out is public: code that drives the `Stellar` namespace directly
145
+ // never constructs an `AugustSDK`, so `configureCuratorAlerts` is its sole way
146
+ // to turn curator alerts off (and the only one that works in a browser, where
147
+ // the env var is unreadable). The emitter itself stays deep-import only — a
148
+ // partner has no reason to inject events into the relay.
149
+ //
150
+ // Safe to re-export from this barrel at all because `curator-alert` imports only
151
+ // `types/`, `constants/` and leaf helpers under `core/analytics/` — never this
152
+ // file — so there is no cycle back through the logger. `madge --circular` in CI
153
+ // is the enforcement; this note is why it passes (CLAUDE.md §9).
154
+ //
155
+ // Both parameter types travel with the function: `ICuratorAlertsContext` is the
156
+ // second argument's type, so without it a consumer cannot type a wrapper around
157
+ // `configureCuratorAlerts` — and API Extractor flags it as a forgotten export
158
+ // (CLAUDE.md §6).
159
+ var curator_alert_1 = require("./curator-alert");
160
+ Object.defineProperty(exports, "configureCuratorAlerts", { enumerable: true, get: function () { return curator_alert_1.configureCuratorAlerts; } });
161
+ Object.defineProperty(exports, "areCuratorAlertsEnabled", { enumerable: true, get: function () { return curator_alert_1.areCuratorAlertsEnabled; } });
162
+ Object.defineProperty(exports, "CURATOR_ALERTS_DISABLE_ENV_VAR", { enumerable: true, get: function () { return curator_alert_1.CURATOR_ALERTS_DISABLE_ENV_VAR; } });
144
163
  //# sourceMappingURL=index.js.map