@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.
- package/CHANGELOG.md +44 -0
- package/cookbook/16-acp-checkout-with-factura.ts +168 -0
- package/cookbook/17-usa-llc-companion.ts +117 -0
- package/cookbook/18-usa-llc-self-incorporates-ar.ts +208 -0
- package/cookbook/19-forensic-compliance-dashboard.ts +320 -0
- package/cookbook/20-multi-tenant-marketplace.ts +274 -0
- package/cookbook/21-cross-jurisdictional-ap2.ts +298 -0
- package/cookbook/22-mp-webhook-afip-reconciliation.ts +374 -0
- package/cookbook/23-astro-arg-reference-customer.ts +187 -0
- package/cookbook/24-sociedad-ia-disaster-recovery.ts +350 -0
- package/cookbook/25-sociedad-ia-quarterly-compliance.ts +545 -0
- package/cookbook/26-certify-by-fetch.ts +536 -0
- package/cookbook/27-live-conformance-monitoring.ts +260 -0
- package/cookbook/28-operator-onboarding-checklist.ts +315 -0
- package/cookbook/29-publish-your-keys.ts +193 -0
- package/cookbook/30-submit-to-registry.ts +257 -0
- package/cookbook/README.md +2 -0
- package/dist/index.cjs +27 -14
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +21 -5
- package/dist/index.d.ts +21 -5
- package/dist/index.js +27 -14
- package/dist/index.js.map +1 -1
- package/package.json +5 -3
- package/tools.manifest.json +1 -1
|
@@ -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
|
+
}
|