@ar-agents/mercadopago 0.17.1 → 0.18.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.
@@ -0,0 +1,260 @@
1
+ /**
2
+ * Recipe 27 — Live conformance monitoring with time-series + alerting.
3
+ *
4
+ * # Pattern
5
+ *
6
+ * Many sociedades-IA operate in production. They want to know: am I
7
+ * still conformant? If my score drops, when did it drop? Can I be
8
+ * notified before the regulator sees it?
9
+ *
10
+ * Recipe 27 is the monitoring loop:
11
+ *
12
+ * 1. Every N minutes (cron), POST /api/conformance-history?url=YOUR_URL
13
+ * to the public ar-agents.ar endpoint. This re-runs the
14
+ * certifier + appends the new point to a 365-entry time-series.
15
+ *
16
+ * 2. Compare the latest point against a sliding-window baseline
17
+ * (default: median of last 24 points = ~1 day at 1h intervals).
18
+ * If the new score is N% below baseline, fire an alert.
19
+ *
20
+ * 3. Optional Slack / email / webhook destinations.
21
+ *
22
+ * Properties:
23
+ * - The history endpoint already does the storage + capping. Recipe
24
+ * 27 is just the orchestration + alert logic.
25
+ * - Designed to run on Vercel cron, GitHub Actions schedule, or any
26
+ * other scheduler. Idempotent: re-running it appends another point.
27
+ * - Threshold is a flat percentage drop, not a fancy CUSUM. Easy to
28
+ * reason about; tune to taste.
29
+ * - Reports drift, not just regression: if score IMPROVED unexpectedly,
30
+ * that's interesting too (operator made a change worth noting in
31
+ * the audit log).
32
+ *
33
+ * # When to use
34
+ *
35
+ * - In production. The day after you deploy your sociedad-IA.
36
+ * - For regulator-facing reporting: "I monitored continuously, here
37
+ * are the 90 days of scores, here's when drift was detected and
38
+ * how it was remediated."
39
+ * - For multi-tenant marketplaces (recipe 20): run for each tenant
40
+ * sociedad-IA in parallel.
41
+ *
42
+ * # Edge Runtime
43
+ *
44
+ * Pure fetch + JSON shaping. Runs anywhere Node 18+ / Edge / browser
45
+ * has `fetch`. No filesystem.
46
+ */
47
+
48
+ // ─────────────────────────────────────────────────────────────────────────────
49
+ // Types
50
+ // ─────────────────────────────────────────────────────────────────────────────
51
+
52
+ interface Point {
53
+ ts: string;
54
+ score: number;
55
+ rating: "A" | "B" | "C" | "D" | "F" | "N/A";
56
+ }
57
+
58
+ interface HistoryResponse {
59
+ target: { baseUrl: string };
60
+ points: Point[];
61
+ latest?: Point;
62
+ }
63
+
64
+ interface MonitorResult {
65
+ url: string;
66
+ latest: Point | null;
67
+ baseline: number | null;
68
+ baselineWindow: number;
69
+ drift: "regression" | "improvement" | "stable" | "no-baseline" | "no-data";
70
+ driftPct: number | null;
71
+ threshold: number;
72
+ totalPoints: number;
73
+ alertFired: boolean;
74
+ alertMessage: string | null;
75
+ }
76
+
77
+ interface MonitorOptions {
78
+ /** Public ar-agents endpoint (allow override for self-hosted). */
79
+ apiBaseUrl?: string;
80
+ /** How many recent points form the baseline. Default 24. */
81
+ baselineWindow?: number;
82
+ /** Percent drop that triggers an alert. Default 10. */
83
+ threshold?: number;
84
+ /** Override fetch impl (testing). */
85
+ fetchImpl?: typeof fetch;
86
+ /** Optional alert destination URLs. Slack-style webhook expected. */
87
+ alertWebhooks?: string[];
88
+ }
89
+
90
+ const DEFAULT_API_BASE = "https://ar-agents.ar";
91
+ const DEFAULT_BASELINE_WINDOW = 24;
92
+ const DEFAULT_THRESHOLD_PCT = 10;
93
+
94
+ // ─────────────────────────────────────────────────────────────────────────────
95
+ // Core monitoring loop
96
+ // ─────────────────────────────────────────────────────────────────────────────
97
+
98
+ function median(nums: number[]): number {
99
+ if (nums.length === 0) return 0;
100
+ const sorted = [...nums].sort((a, b) => a - b);
101
+ const m = Math.floor(sorted.length / 2);
102
+ return sorted.length % 2 === 0 ? (sorted[m - 1] + sorted[m]) / 2 : sorted[m];
103
+ }
104
+
105
+ export async function monitorConformance(
106
+ url: string,
107
+ options: MonitorOptions = {},
108
+ ): Promise<MonitorResult> {
109
+ const api = options.apiBaseUrl ?? DEFAULT_API_BASE;
110
+ const baselineWindow = options.baselineWindow ?? DEFAULT_BASELINE_WINDOW;
111
+ const threshold = options.threshold ?? DEFAULT_THRESHOLD_PCT;
112
+ const fetchImpl = options.fetchImpl ?? fetch;
113
+
114
+ // 1. Append a new point + read the full history.
115
+ const postUrl = `${api}/api/conformance-history?url=${encodeURIComponent(url)}`;
116
+ let history: HistoryResponse | null = null;
117
+ try {
118
+ const r = await fetchImpl(postUrl, {
119
+ method: "POST",
120
+ headers: { "user-agent": "ar-agents-recipe-27-monitor" },
121
+ });
122
+ if (r.ok) history = (await r.json()) as HistoryResponse;
123
+ } catch {
124
+ // fall through
125
+ }
126
+ if (!history) {
127
+ return {
128
+ url,
129
+ latest: null,
130
+ baseline: null,
131
+ baselineWindow,
132
+ drift: "no-data",
133
+ driftPct: null,
134
+ threshold,
135
+ totalPoints: 0,
136
+ alertFired: false,
137
+ alertMessage: null,
138
+ };
139
+ }
140
+
141
+ const points = history.points;
142
+ const latest = history.latest ?? points[points.length - 1] ?? null;
143
+ if (!latest) {
144
+ return {
145
+ url,
146
+ latest: null,
147
+ baseline: null,
148
+ baselineWindow,
149
+ drift: "no-data",
150
+ driftPct: null,
151
+ threshold,
152
+ totalPoints: 0,
153
+ alertFired: false,
154
+ alertMessage: null,
155
+ };
156
+ }
157
+
158
+ // 2. Compute baseline (excluding the latest point so it's "before").
159
+ const beforeLatest = points.slice(0, -1);
160
+ const baselineSlice = beforeLatest.slice(-baselineWindow);
161
+ if (baselineSlice.length === 0) {
162
+ return {
163
+ url,
164
+ latest,
165
+ baseline: null,
166
+ baselineWindow,
167
+ drift: "no-baseline",
168
+ driftPct: null,
169
+ threshold,
170
+ totalPoints: points.length,
171
+ alertFired: false,
172
+ alertMessage: null,
173
+ };
174
+ }
175
+
176
+ const baseline = median(baselineSlice.map((p) => p.score));
177
+ const driftPct = baseline > 0 ? ((latest.score - baseline) / baseline) * 100 : 0;
178
+
179
+ let drift: MonitorResult["drift"];
180
+ if (Math.abs(driftPct) < threshold / 2) drift = "stable";
181
+ else if (driftPct < 0) drift = "regression";
182
+ else drift = "improvement";
183
+
184
+ // 3. Alert if regression exceeded threshold.
185
+ const alertFired = drift === "regression" && Math.abs(driftPct) >= threshold;
186
+ let alertMessage: string | null = null;
187
+ if (alertFired) {
188
+ alertMessage = `RFC conformance regression: ${url} dropped ${driftPct.toFixed(1)}% (from baseline ${baseline.toFixed(1)} to ${latest.score}/${latest.rating}). Baseline window: ${baselineSlice.length} points.`;
189
+
190
+ // Fire webhooks in parallel; don't fail the function if a webhook fails.
191
+ if (options.alertWebhooks && options.alertWebhooks.length > 0) {
192
+ await Promise.allSettled(
193
+ options.alertWebhooks.map((hook) =>
194
+ fetchImpl(hook, {
195
+ method: "POST",
196
+ headers: { "content-type": "application/json" },
197
+ body: JSON.stringify({ text: alertMessage }),
198
+ }),
199
+ ),
200
+ );
201
+ }
202
+ }
203
+
204
+ return {
205
+ url,
206
+ latest,
207
+ baseline,
208
+ baselineWindow,
209
+ drift,
210
+ driftPct,
211
+ threshold,
212
+ totalPoints: points.length,
213
+ alertFired,
214
+ alertMessage,
215
+ };
216
+ }
217
+
218
+ // ─────────────────────────────────────────────────────────────────────────────
219
+ // CLI: tsx 27-live-conformance-monitoring.ts <url> [--threshold=N] [--webhook=URL ...]
220
+ // ─────────────────────────────────────────────────────────────────────────────
221
+
222
+ declare const process: { argv: string[]; env: Record<string, string | undefined> } | undefined;
223
+
224
+ async function main() {
225
+ if (typeof process === "undefined") return;
226
+ const args = process.argv.slice(2);
227
+ const url = args.find((a) => !a.startsWith("--"));
228
+ if (!url) {
229
+ console.error("usage: tsx 27-live-conformance-monitoring.ts <url> [--threshold=N] [--webhook=URL]");
230
+ return;
231
+ }
232
+ const threshold = (() => {
233
+ const arg = args.find((a) => a.startsWith("--threshold="));
234
+ return arg ? parseFloat(arg.split("=")[1]) : DEFAULT_THRESHOLD_PCT;
235
+ })();
236
+ const alertWebhooks = args.filter((a) => a.startsWith("--webhook=")).map((a) => a.split("=")[1]);
237
+
238
+ const result = await monitorConformance(url, {
239
+ threshold,
240
+ alertWebhooks,
241
+ });
242
+
243
+ console.log(JSON.stringify(result, null, 2));
244
+
245
+ if (typeof process !== "undefined" && "exit" in process) {
246
+ (process as unknown as { exit: (code: number) => void }).exit(
247
+ result.alertFired ? 1 : 0,
248
+ );
249
+ }
250
+ }
251
+
252
+ const isMain = typeof require !== "undefined" && require.main === module;
253
+ if (isMain) {
254
+ main().catch((e) => {
255
+ console.error(e);
256
+ if (typeof process !== "undefined" && "exit" in process) {
257
+ (process as unknown as { exit: (code: number) => void }).exit(1);
258
+ }
259
+ });
260
+ }
@@ -0,0 +1,315 @@
1
+ /**
2
+ * Recipe 28 — Operator onboarding checklist + automated verification.
3
+ *
4
+ * # Pattern
5
+ *
6
+ * You're an operator about to launch your first sociedad-IA. There are
7
+ * 18 items to wire up across MercadoPago, AFIP/ARCA, WhatsApp, banking,
8
+ * KV storage, HMAC signing, Ed25519 signing (optional v2), env vars,
9
+ * domain DNS, /.well-known publication.
10
+ *
11
+ * Recipe 28 is the inventory + auto-verifier: a single function
12
+ * `checkOperatorReadiness(baseUrl)` that walks the checklist
13
+ * programmatically and returns a per-item pass/fail/skip with
14
+ * remediation links.
15
+ *
16
+ * The output is a deterministic JSON readiness report — the operator's
17
+ * pre-launch sign-off. Used internally by the auto-incorporation wizard
18
+ * (/api/auto-incorporate) to confirm a freshly-deployed sociedad-IA is
19
+ * production-ready before adding it to /registro.
20
+ *
21
+ * # When to use
22
+ *
23
+ * - Right after running `vercel deploy` of a freshly-generated
24
+ * sociedad-IA from the sociedad-ia-starter template.
25
+ * - Before listing in /registro (the registry rejects entries with a
26
+ * readiness < B).
27
+ * - As a pre-merge gate in the operator's own CI (analogous to recipe 26
28
+ * but covers operator-wiring, not just RFC conformance).
29
+ *
30
+ * # Edge Runtime
31
+ *
32
+ * Pure fetch + JSON shaping. Runs anywhere fetch is available.
33
+ */
34
+
35
+ // ─────────────────────────────────────────────────────────────────────────────
36
+ // Types
37
+ // ─────────────────────────────────────────────────────────────────────────────
38
+
39
+ export interface Item {
40
+ id: string;
41
+ category:
42
+ | "discovery"
43
+ | "audit"
44
+ | "providers"
45
+ | "security"
46
+ | "legal"
47
+ | "ops"
48
+ | "tooling";
49
+ label: string;
50
+ status: "pass" | "fail" | "skip" | "warn";
51
+ detail: string;
52
+ remediation?: string;
53
+ ref?: string;
54
+ }
55
+
56
+ export interface Readiness {
57
+ $schema: string;
58
+ generatedAt: string;
59
+ target: { baseUrl: string };
60
+ readiness: "ready" | "almost" | "blocked" | "not-deployed";
61
+ passedCount: number;
62
+ totalCount: number;
63
+ items: Item[];
64
+ }
65
+
66
+ // ─────────────────────────────────────────────────────────────────────────────
67
+ // The checklist
68
+ // ─────────────────────────────────────────────────────────────────────────────
69
+
70
+ interface Check {
71
+ id: string;
72
+ category: Item["category"];
73
+ label: string;
74
+ ref?: string;
75
+ /** Returns the Item details. */
76
+ run: (base: string, fetchImpl: typeof fetch) => Promise<Omit<Item, "id" | "category" | "label" | "ref">>;
77
+ }
78
+
79
+ async function fetchOk(
80
+ url: string,
81
+ fetchImpl: typeof fetch,
82
+ ): Promise<{ ok: boolean; status: number; body?: unknown }> {
83
+ try {
84
+ const r = await fetchImpl(url, { signal: AbortSignal.timeout(8000) });
85
+ if (!r.ok) return { ok: false, status: r.status };
86
+ let body: unknown;
87
+ try {
88
+ const ct = r.headers.get("content-type") || "";
89
+ body = ct.includes("application/json") ? await r.json() : await r.text();
90
+ } catch {
91
+ body = null;
92
+ }
93
+ return { ok: true, status: r.status, body };
94
+ } catch {
95
+ return { ok: false, status: 0 };
96
+ }
97
+ }
98
+
99
+ const CHECKS: ReadonlyArray<Check> = [
100
+ {
101
+ id: "discovery-well-known",
102
+ category: "discovery",
103
+ label: "/.well-known/agents.json serves manifest with issuer.jurisdiction",
104
+ ref: "https://ar-agents.ar/rfcs/002",
105
+ run: async (base, fetchImpl) => {
106
+ const r = await fetchOk(`${base}/.well-known/agents.json`, fetchImpl);
107
+ if (!r.ok) return { status: "fail", detail: `HTTP ${r.status || "network error"}`, remediation: "Add apps/landing/public/.well-known/agents.json per RFC-002 schema." };
108
+ const m = r.body as Record<string, unknown> | null;
109
+ const j = m?.issuer as Record<string, unknown> | undefined;
110
+ if (!j?.jurisdiction) return { status: "fail", detail: "Missing issuer.jurisdiction.", remediation: "Add issuer.jurisdiction to your agents.json." };
111
+ return { status: "pass", detail: `jurisdiction=${j.jurisdiction}.` };
112
+ },
113
+ },
114
+ {
115
+ id: "discovery-rfc-conformance",
116
+ category: "discovery",
117
+ label: "Manifest declares rfcConformance",
118
+ run: async (base, fetchImpl) => {
119
+ const r = await fetchOk(`${base}/.well-known/agents.json`, fetchImpl);
120
+ if (!r.ok) return { status: "skip", detail: "Manifest fetch failed." };
121
+ const arr = (r.body as Record<string, unknown> | null)?.rfcConformance;
122
+ if (Array.isArray(arr) && arr.length > 0) {
123
+ return { status: "pass", detail: `Claims: ${(arr as string[]).join(", ")}.` };
124
+ }
125
+ return { status: "warn", detail: "No rfcConformance array.", remediation: "Declare which RFCs your impl conforms to (e.g. ['rfc-001-v1', 'rfc-002-v1', 'rfc-004-draft'])." };
126
+ },
127
+ },
128
+ {
129
+ id: "audit-read",
130
+ category: "audit",
131
+ label: "Audit-read endpoint /api/play/audit/{sessionId} responds",
132
+ ref: "https://ar-agents.ar/rfcs/004",
133
+ run: async (base, fetchImpl) => {
134
+ const r = await fetchOk(`${base}/api/play/audit/demo-public-ar-001`, fetchImpl);
135
+ if (!r.ok) return { status: "fail", detail: `HTTP ${r.status}.`, remediation: "Verify your audit-log route is deployed + has the {sessionId} dynamic segment." };
136
+ return { status: "pass", detail: "Endpoint responds 200." };
137
+ },
138
+ },
139
+ {
140
+ id: "audit-verify",
141
+ category: "audit",
142
+ label: "Audit-verify (?verify=1) returns HMAC verification counts",
143
+ ref: "https://ar-agents.ar/rfcs/004",
144
+ run: async (base, fetchImpl) => {
145
+ const r = await fetchOk(`${base}/api/play/audit/demo-public-ar-001?verify=1`, fetchImpl);
146
+ if (!r.ok) return { status: "fail", detail: `HTTP ${r.status}.` };
147
+ const d = r.body as Record<string, unknown> | null;
148
+ const v = (d?.verification ?? d) as Record<string, unknown> | undefined;
149
+ if (typeof v?.hmacWired === "boolean") {
150
+ return v.hmacWired
151
+ ? { status: "pass", detail: `hmacWired=true.` }
152
+ : { status: "warn", detail: "hmacWired=false (production must wire AUDIT_HMAC_SECRET).", remediation: "Set AUDIT_HMAC_SECRET env var in your Vercel project." };
153
+ }
154
+ return { status: "warn", detail: "Response missing verification counts." };
155
+ },
156
+ },
157
+ {
158
+ id: "audit-csv",
159
+ category: "audit",
160
+ label: "CSV export endpoint returns text/csv",
161
+ run: async (base, fetchImpl) => {
162
+ try {
163
+ const r = await fetchImpl(`${base}/api/play/audit/demo-public-ar-001/csv`, { signal: AbortSignal.timeout(8000) });
164
+ if (!r.ok) return { status: "fail", detail: `HTTP ${r.status}.` };
165
+ const ct = r.headers.get("content-type") || "";
166
+ return ct.includes("text/csv")
167
+ ? { status: "pass", detail: `Content-Type: ${ct}.` }
168
+ : { status: "warn", detail: `Content-Type is ${ct}, expected text/csv.` };
169
+ } catch (e) {
170
+ return { status: "fail", detail: `Network error: ${(e as Error).message}` };
171
+ }
172
+ },
173
+ },
174
+ {
175
+ id: "rfc-005-keys",
176
+ category: "audit",
177
+ label: "RFC-005 /.well-known/sociedad-ia/keys publishes Ed25519 key",
178
+ ref: "https://ar-agents.ar/rfcs/005",
179
+ run: async (base, fetchImpl) => {
180
+ const r = await fetchOk(`${base}/.well-known/sociedad-ia/keys`, fetchImpl);
181
+ if (!r.ok) return { status: "skip", detail: `Not advertised (HTTP ${r.status}). v1 HMAC-only is OK.`, remediation: "Optional: publish your Ed25519 public key per RFC-005 § 4 for asymmetric verifier support." };
182
+ const keys = (r.body as Record<string, unknown> | null)?.keys;
183
+ return Array.isArray(keys) && keys.length > 0
184
+ ? { status: "pass", detail: `${keys.length} key(s) advertised.` }
185
+ : { status: "warn", detail: "Endpoint exists but no keys advertised." };
186
+ },
187
+ },
188
+ {
189
+ id: "tooling-openapi",
190
+ category: "tooling",
191
+ label: "/api/openapi returns OpenAPI 3.x spec",
192
+ run: async (base, fetchImpl) => {
193
+ const r = await fetchOk(`${base}/api/openapi`, fetchImpl);
194
+ if (!r.ok) return { status: "skip", detail: `Not advertised (HTTP ${r.status}).`, remediation: "Optional but recommended for tooling generators." };
195
+ const d = r.body as Record<string, unknown> | null;
196
+ return typeof d?.openapi === "string" && (d.openapi as string).startsWith("3.")
197
+ ? { status: "pass", detail: `OpenAPI ${d.openapi}.` }
198
+ : { status: "warn", detail: "Not an OpenAPI 3.x doc." };
199
+ },
200
+ },
201
+ {
202
+ id: "security-hsts",
203
+ category: "security",
204
+ label: "HSTS header present on root response",
205
+ run: async (base, fetchImpl) => {
206
+ try {
207
+ const r = await fetchImpl(base, { signal: AbortSignal.timeout(5000) });
208
+ const hsts = r.headers.get("strict-transport-security");
209
+ return hsts
210
+ ? { status: "pass", detail: hsts }
211
+ : { status: "warn", detail: "HSTS missing.", remediation: "Vercel sets HSTS by default on .vercel.app domains; on custom domains, ensure the redirect is configured." };
212
+ } catch (e) {
213
+ return { status: "fail", detail: `Network error: ${(e as Error).message}` };
214
+ }
215
+ },
216
+ },
217
+ {
218
+ id: "tooling-sitemap",
219
+ category: "tooling",
220
+ label: "Sitemap.xml is published",
221
+ run: async (base, fetchImpl) => {
222
+ const r = await fetchOk(`${base}/sitemap.xml`, fetchImpl);
223
+ return r.ok
224
+ ? { status: "pass", detail: "sitemap.xml present." }
225
+ : { status: "warn", detail: "No sitemap.xml advertised." };
226
+ },
227
+ },
228
+ {
229
+ id: "tooling-llms-txt",
230
+ category: "tooling",
231
+ label: "/llms.txt is published for AI crawlers",
232
+ run: async (base, fetchImpl) => {
233
+ const r = await fetchOk(`${base}/llms.txt`, fetchImpl);
234
+ return r.ok
235
+ ? { status: "pass", detail: "/llms.txt present." }
236
+ : { status: "warn", detail: "No /llms.txt advertised.", remediation: "Publish /llms.txt per the llmstxt.org convention so AI crawlers can ingest your sociedad's surface." };
237
+ },
238
+ },
239
+ ];
240
+
241
+ // ─────────────────────────────────────────────────────────────────────────────
242
+ // Public API
243
+ // ─────────────────────────────────────────────────────────────────────────────
244
+
245
+ export async function checkOperatorReadiness(
246
+ baseUrl: string,
247
+ options: { fetchImpl?: typeof fetch } = {},
248
+ ): Promise<Readiness> {
249
+ const parsed = new URL(baseUrl);
250
+ const base = parsed.origin;
251
+ const fetchImpl = options.fetchImpl ?? fetch;
252
+
253
+ const items: Item[] = await Promise.all(
254
+ CHECKS.map(async (c): Promise<Item> => {
255
+ const r = await c.run(base, fetchImpl);
256
+ return {
257
+ id: c.id,
258
+ category: c.category,
259
+ label: c.label,
260
+ ref: c.ref,
261
+ ...r,
262
+ };
263
+ }),
264
+ );
265
+
266
+ const passing = items.filter((i) => i.status === "pass").length;
267
+ const failing = items.filter((i) => i.status === "fail").length;
268
+
269
+ let readiness: Readiness["readiness"];
270
+ if (failing > 2) readiness = "blocked";
271
+ else if (failing > 0 || passing < items.length - 2) readiness = "almost";
272
+ else readiness = "ready";
273
+
274
+ return {
275
+ $schema: "https://ar-agents.ar/schemas/readiness.v1.json",
276
+ generatedAt: new Date().toISOString(),
277
+ target: { baseUrl: base },
278
+ readiness,
279
+ passedCount: passing,
280
+ totalCount: items.length,
281
+ items,
282
+ };
283
+ }
284
+
285
+ // ─────────────────────────────────────────────────────────────────────────────
286
+ // CLI: `tsx 28-operator-onboarding-checklist.ts <baseUrl>`
287
+ // ─────────────────────────────────────────────────────────────────────────────
288
+
289
+ declare const process: { argv: string[] } | undefined;
290
+
291
+ async function main() {
292
+ if (typeof process === "undefined") return;
293
+ const baseUrl = process.argv[2];
294
+ if (!baseUrl) {
295
+ console.error("usage: tsx 28-operator-onboarding-checklist.ts <baseUrl>");
296
+ return;
297
+ }
298
+ const r = await checkOperatorReadiness(baseUrl);
299
+ console.log(JSON.stringify(r, null, 2));
300
+ if (typeof process !== "undefined" && "exit" in process) {
301
+ (process as unknown as { exit: (code: number) => void }).exit(
302
+ r.readiness === "blocked" ? 1 : 0,
303
+ );
304
+ }
305
+ }
306
+
307
+ const isMain = typeof require !== "undefined" && require.main === module;
308
+ if (isMain) {
309
+ main().catch((e) => {
310
+ console.error(e);
311
+ if (typeof process !== "undefined" && "exit" in process) {
312
+ (process as unknown as { exit: (code: number) => void }).exit(1);
313
+ }
314
+ });
315
+ }