@meffecta/agent 0.0.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.
@@ -0,0 +1,557 @@
1
+ #!/usr/bin/env node
2
+ // Credential doctor: exercises every configured credential the agent's jobs and
3
+ // poller depend on, the same way its consumer uses it, and prints ok/FAIL/skip
4
+ // per surface. Run locally with `pnpm creds` (.env via dotenv), or on Cloud Run
5
+ // (through /test: "run node /app/scripts/verify-credentials.mjs and return the
6
+ // output" — absolute path, since a run's cwd is the content clone and this file
7
+ // ships in the image) where the metadata-server surfaces also become testable.
8
+ // Never prints a credential value. Exits 1 if anything configured actually fails.
9
+
10
+ import { execFile } from "node:child_process";
11
+
12
+ const env = process.env;
13
+ const results = [];
14
+ const record = (name, status, detail = "") => results.push({ name, status, detail });
15
+ const ok = (name, detail) => record(name, "ok", detail);
16
+ const fail = (name, detail) => record(name, "FAIL", detail);
17
+ const skip = (name, detail) => record(name, "skip", detail);
18
+
19
+ async function http(url, { headers = {}, method = "GET", body, timeoutMs = 10_000 } = {}) {
20
+ const res = await fetch(url, { method, headers, body, signal: AbortSignal.timeout(timeoutMs) });
21
+ let json;
22
+ try {
23
+ json = await res.json();
24
+ } catch {
25
+ json = undefined;
26
+ }
27
+ return { status: res.status, json };
28
+ }
29
+
30
+ // --- Metadata server (Cloud Run) --------------------------------------------
31
+
32
+ let onCloudRun = false;
33
+ async function metadataToken(scopes) {
34
+ const url = `http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token${
35
+ scopes ? `?scopes=${encodeURIComponent(scopes.join(","))}` : ""
36
+ }`;
37
+ const res = await fetch(url, { headers: { "Metadata-Flavor": "Google" }, signal: AbortSignal.timeout(3000) });
38
+ if (!res.ok) {
39
+ throw new Error(`metadata server ${res.status}`);
40
+ }
41
+ return (await res.json()).access_token;
42
+ }
43
+
44
+ // --- Checks ------------------------------------------------------------------
45
+
46
+ async function checkGithub() {
47
+ if (!env.GITHUB_TOKEN) {
48
+ return skip("GITHUB_TOKEN", "unset");
49
+ }
50
+ // The content repo (from GIT_REPO_URL, when it's a GitHub URL), plus the working
51
+ // repos this deployment's jobs clone (deployment-specific — derive from jobs/ one day).
52
+ const contentRepo = env.GIT_REPO_URL?.match(/github\.com\/([^/]+\/[^/.]+)/)?.[1];
53
+ const repos = [...(contentRepo ? [contentRepo] : []), "meffecta/sweepos"];
54
+ const codes = await Promise.all(
55
+ repos.map(
56
+ async (r) =>
57
+ (await http(`https://api.github.com/repos/${r}`, { headers: { Authorization: `Bearer ${env.GITHUB_TOKEN}` } }))
58
+ .status,
59
+ ),
60
+ );
61
+ const bad = repos.filter((_, i) => codes[i] !== 200);
62
+ bad.length
63
+ ? fail("GITHUB_TOKEN", `cannot see ${bad.join(", ")} (${codes.join("/")})`)
64
+ : ok("GITHUB_TOKEN", `clones ${repos.join(", ")}`);
65
+ }
66
+
67
+ async function checkGrafana() {
68
+ if (!env.GRAFANA_API_KEY || !env.GRAFANA_LOGS_HOST || !env.GRAFANA_LOGS_USERNAME) {
69
+ return skip("GRAFANA (Loki)", "GRAFANA_LOGS_HOST/USERNAME/API_KEY not all set");
70
+ }
71
+ const auth = Buffer.from(`${env.GRAFANA_LOGS_USERNAME}:${env.GRAFANA_API_KEY}`).toString("base64");
72
+ const { status } = await http(`${env.GRAFANA_LOGS_HOST}/loki/api/v1/labels`, {
73
+ headers: { Authorization: `Basic ${auth}` },
74
+ });
75
+ status === 200 ? ok("GRAFANA (Loki)") : fail("GRAFANA (Loki)", `labels query → ${status}`);
76
+ }
77
+
78
+ async function checkResend() {
79
+ if (!env.RESEND_API_KEY) {
80
+ return skip("RESEND_API_KEY", "unset");
81
+ }
82
+ const { status, json } = await http("https://api.resend.com/domains", {
83
+ headers: { Authorization: `Bearer ${env.RESEND_API_KEY}` },
84
+ });
85
+ status === 200
86
+ ? ok("RESEND_API_KEY", `${json?.data?.length ?? 0} domains`)
87
+ : fail("RESEND_API_KEY", `domains → ${status}`);
88
+ }
89
+
90
+ async function checkInternalApi() {
91
+ if (!env.SWEEPOS_API_URL || !env.INTERNAL_API_KEY) {
92
+ return skip("SWEEPOS internal API", "SWEEPOS_API_URL/INTERNAL_API_KEY not set");
93
+ }
94
+ const { status } = await http(`${env.SWEEPOS_API_URL}/v1/internal/leads?limit=1`, {
95
+ headers: { Authorization: `Bearer ${env.INTERNAL_API_KEY}` },
96
+ });
97
+ status === 200 ? ok("SWEEPOS internal API") : fail("SWEEPOS internal API", `leads → ${status}`);
98
+ }
99
+
100
+ function checkPostgres() {
101
+ return new Promise((resolve) => {
102
+ if (!env.SWEEPOS_POSTGRES_URL_READONLY) {
103
+ skip("SWEEPOS_POSTGRES_URL_READONLY", "unset");
104
+ return resolve();
105
+ }
106
+ execFile("psql", [env.SWEEPOS_POSTGRES_URL_READONLY, "-Atc", "SELECT 1"], { timeout: 10_000 }, (err, stdout) => {
107
+ if (err) {
108
+ err.code === "ENOENT"
109
+ ? skip("SWEEPOS_POSTGRES_URL_READONLY", "psql not installed here")
110
+ : fail("SWEEPOS_POSTGRES_URL_READONLY", err.message.split("\n")[0].slice(0, 120));
111
+ } else {
112
+ stdout.trim() === "1"
113
+ ? ok("SWEEPOS_POSTGRES_URL_READONLY")
114
+ : fail("SWEEPOS_POSTGRES_URL_READONLY", "unexpected result");
115
+ }
116
+ resolve();
117
+ });
118
+ });
119
+ }
120
+
121
+ async function checkPosthog() {
122
+ if (!env.POSTHOG_HOST || !env.POSTHOG_API_KEY) {
123
+ return skip("POSTHOG", "POSTHOG_HOST/POSTHOG_API_KEY not set");
124
+ }
125
+ const worlds = Object.keys(env).filter((k) => k.endsWith("_POSTHOG_PROJECT_ID") && env[k]);
126
+ if (worlds.length === 0) {
127
+ return skip("POSTHOG", "no *_POSTHOG_PROJECT_ID set");
128
+ }
129
+ for (const k of worlds) {
130
+ const { status } = await http(`${env.POSTHOG_HOST}/api/projects/${env[k]}/`, {
131
+ headers: { Authorization: `Bearer ${env.POSTHOG_API_KEY}` },
132
+ });
133
+ status === 200 ? ok(`POSTHOG ${k}`) : fail(`POSTHOG ${k}`, `project → ${status}`);
134
+ }
135
+ }
136
+
137
+ async function checkAhrefs() {
138
+ if (!env.AHREFS_API_KEY) {
139
+ return skip("AHREFS_API_KEY", "unset");
140
+ }
141
+ const { status, json } = await http("https://api.ahrefs.com/v3/public/domain-rating-free?target=sweepos.app", {
142
+ headers: { Authorization: `Bearer ${env.AHREFS_API_KEY}` },
143
+ });
144
+ status === 200
145
+ ? ok("AHREFS_API_KEY", `sweepos.app DR ${json?.domain_rating?.domain_rating ?? "?"}`)
146
+ : fail("AHREFS_API_KEY", `domain-rating-free → ${status}`);
147
+ }
148
+
149
+ async function checkCloudflare() {
150
+ if (!env.CLOUDFLARE_API_KEY || !env.CLOUDFLARE_ACCOUNT_ID) {
151
+ return skip("CLOUDFLARE", "CLOUDFLARE_API_KEY/ACCOUNT_ID not set");
152
+ }
153
+ const headers = { Authorization: `Bearer ${env.CLOUDFLARE_API_KEY}` };
154
+ const verify = await http(
155
+ `https://api.cloudflare.com/client/v4/accounts/${env.CLOUDFLARE_ACCOUNT_ID}/tokens/verify`,
156
+ { headers },
157
+ );
158
+ if (!verify.json?.success) {
159
+ return fail("CLOUDFLARE", `token verify → ${verify.status}`);
160
+ }
161
+ const zones = await http("https://api.cloudflare.com/client/v4/zones?per_page=50", { headers });
162
+ zones.json?.success
163
+ ? ok("CLOUDFLARE", `${zones.json.result.length} zones`)
164
+ : fail("CLOUDFLARE", `zones → ${zones.status}`);
165
+ }
166
+
167
+ async function checkElevenlabs() {
168
+ if (!env.ELEVENLABS_API_KEY) {
169
+ return skip("ELEVENLABS_API_KEY", "unset");
170
+ }
171
+ const { status } = await http("https://api.elevenlabs.io/v1/user", {
172
+ headers: { "xi-api-key": env.ELEVENLABS_API_KEY },
173
+ });
174
+ status === 200 ? ok("ELEVENLABS_API_KEY") : fail("ELEVENLABS_API_KEY", `user → ${status}`);
175
+ }
176
+
177
+ async function checkPlaces() {
178
+ if (!env.GOOGLE_API_KEY) {
179
+ return skip("GOOGLE_API_KEY", "unset");
180
+ }
181
+ const { status, json } = await http("https://places.googleapis.com/v1/places:searchText", {
182
+ method: "POST",
183
+ headers: {
184
+ "Content-Type": "application/json",
185
+ "X-Goog-Api-Key": env.GOOGLE_API_KEY,
186
+ "X-Goog-FieldMask": "places.id",
187
+ },
188
+ body: JSON.stringify({ textQuery: "städfirma Stockholm", pageSize: 1 }),
189
+ });
190
+ status === 200
191
+ ? ok("GOOGLE_API_KEY (Places)")
192
+ : fail("GOOGLE_API_KEY (Places)", json?.error?.message?.slice(0, 100) ?? `→ ${status}`);
193
+ }
194
+
195
+ async function gmailRefresh(refreshToken) {
196
+ const { status, json } = await http("https://oauth2.googleapis.com/token", {
197
+ method: "POST",
198
+ headers: { "content-type": "application/x-www-form-urlencoded" },
199
+ body: new URLSearchParams({
200
+ client_id: env.GMAIL_CLIENT_ID,
201
+ client_secret: env.GMAIL_CLIENT_SECRET,
202
+ refresh_token: refreshToken,
203
+ grant_type: "refresh_token",
204
+ }),
205
+ });
206
+ if (status !== 200 || !json?.access_token) {
207
+ throw new Error(json?.error ?? `→ ${status}`);
208
+ }
209
+ return json.access_token;
210
+ }
211
+
212
+ async function checkGmailAccounts() {
213
+ if (!env.GMAIL_CLIENT_ID || !env.GMAIL_CLIENT_SECRET) {
214
+ return skip("GMAIL (OAuth)", "GMAIL_CLIENT_ID/SECRET not set");
215
+ }
216
+ const accounts = Object.keys(env)
217
+ .filter((k) => /^GMAIL(_[A-Z0-9]+)?_REFRESH_TOKEN$/.test(k) || k === "GMAIL_REFRESH_TOKEN")
218
+ .filter((k) => env[k]);
219
+ if (accounts.length === 0) {
220
+ return skip("GMAIL (OAuth)", "no refresh tokens set");
221
+ }
222
+ for (const k of accounts) {
223
+ const label = `GMAIL ${k === "GMAIL_REFRESH_TOKEN" ? "default" : k.replace(/^GMAIL_|_REFRESH_TOKEN$/g, "")}`;
224
+ try {
225
+ const token = await gmailRefresh(env[k]);
226
+ const { json } = await http("https://gmail.googleapis.com/gmail/v1/users/me/profile", {
227
+ headers: { Authorization: `Bearer ${token}` },
228
+ });
229
+ json?.emailAddress ? ok(label, json.emailAddress) : fail(label, "profile fetch failed");
230
+ } catch (err) {
231
+ fail(label, String(err.message).slice(0, 120));
232
+ }
233
+ }
234
+ }
235
+
236
+ // Microsoft Graph: same shape as the Gmail check — refresh, then use the token the
237
+ // way query-outlook does. Reads the mailbox profile only; never touches a message.
238
+ async function checkGraphAccounts() {
239
+ if (!env.MSGRAPH_TENANT_ID || !env.MSGRAPH_CLIENT_ID || !env.MSGRAPH_CLIENT_SECRET) {
240
+ return skip("MSGRAPH (Graph)", "MSGRAPH_TENANT_ID/CLIENT_ID/CLIENT_SECRET not set");
241
+ }
242
+ const accounts = Object.keys(env)
243
+ .filter((k) => /^MSGRAPH_[A-Z0-9]+_REFRESH_TOKEN$/.test(k))
244
+ .filter((k) => env[k]);
245
+ if (accounts.length === 0) {
246
+ return skip("MSGRAPH (Graph)", "no refresh tokens set");
247
+ }
248
+ for (const k of accounts) {
249
+ const label = `MSGRAPH ${k.replace(/^MSGRAPH_|_REFRESH_TOKEN$/g, "")}`;
250
+ try {
251
+ const { status, json } = await http(
252
+ `https://login.microsoftonline.com/${env.MSGRAPH_TENANT_ID}/oauth2/v2.0/token`,
253
+ {
254
+ method: "POST",
255
+ headers: { "content-type": "application/x-www-form-urlencoded" },
256
+ body: new URLSearchParams({
257
+ client_id: env.MSGRAPH_CLIENT_ID,
258
+ client_secret: env.MSGRAPH_CLIENT_SECRET,
259
+ refresh_token: env[k],
260
+ grant_type: "refresh_token",
261
+ scope: "offline_access https://graph.microsoft.com/Mail.Read https://graph.microsoft.com/Calendars.Read",
262
+ }),
263
+ },
264
+ );
265
+ if (status !== 200 || !json?.access_token) {
266
+ // A conditional-access refusal looks nothing like an expired token; say which.
267
+ const detail = String(json?.error_description ?? `→ ${status}`);
268
+ fail(
269
+ label,
270
+ /AADSTS50076|AADSTS53003/.test(detail)
271
+ ? "conditional access demands interactive sign-in"
272
+ : detail.slice(0, 120),
273
+ );
274
+ continue;
275
+ }
276
+ // Which identity consented, read from the token's own claims: /me needs
277
+ // User.Read, which this token deliberately doesn't hold.
278
+ const claims = JSON.parse(Buffer.from(json.access_token.split(".")[1], "base64url").toString());
279
+ const identity = claims.upn ?? claims.unique_name ?? claims.preferred_username ?? "unknown identity";
280
+ // Probe a mail endpoint, which is what the skill actually uses. Counts only.
281
+ const { status: mailStatus, json: inbox } = await http(
282
+ "https://graph.microsoft.com/v1.0/me/mailFolders/inbox?$select=totalItemCount,unreadItemCount",
283
+ { headers: { Authorization: `Bearer ${json.access_token}` } },
284
+ );
285
+ if (mailStatus === 200) {
286
+ ok(label, `${identity} — inbox reachable, ${inbox?.unreadItemCount ?? "?"} unread`);
287
+ } else if (/inactive, soft-deleted, or is hosted on-premise/.test(String(inbox?.error?.message))) {
288
+ // The classic trap: consenting as an admin account, which has no mailbox.
289
+ fail(label, `${identity} has no Exchange mailbox — re-mint signed in as the everyday account`);
290
+ } else {
291
+ fail(label, `${identity} — inbox → ${mailStatus} ${String(inbox?.error?.message ?? "").slice(0, 60)}`);
292
+ }
293
+ } catch (err) {
294
+ fail(label, String(err.message).slice(0, 120));
295
+ }
296
+ }
297
+ }
298
+
299
+ async function dwdToken(mailbox, scope) {
300
+ const saToken = await metadataToken();
301
+ const saEmail = (
302
+ await (
303
+ await fetch("http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/email", {
304
+ headers: { "Metadata-Flavor": "Google" },
305
+ })
306
+ ).text()
307
+ ).trim();
308
+ const now = Math.floor(Date.now() / 1000);
309
+ const sign = await http(
310
+ `https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/${encodeURIComponent(saEmail)}:signJwt`,
311
+ {
312
+ method: "POST",
313
+ headers: { authorization: `Bearer ${saToken}`, "content-type": "application/json" },
314
+ body: JSON.stringify({
315
+ payload: JSON.stringify({
316
+ iss: saEmail,
317
+ sub: mailbox,
318
+ scope,
319
+ aud: "https://oauth2.googleapis.com/token",
320
+ iat: now,
321
+ exp: now + 600,
322
+ }),
323
+ }),
324
+ },
325
+ );
326
+ const exch = await http("https://oauth2.googleapis.com/token", {
327
+ method: "POST",
328
+ headers: { "content-type": "application/x-www-form-urlencoded" },
329
+ body: new URLSearchParams({
330
+ grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
331
+ assertion: sign.json.signedJwt,
332
+ }),
333
+ });
334
+ if (!exch.json?.access_token) {
335
+ throw new Error(exch.json?.error ?? "token exchange failed");
336
+ }
337
+ return exch.json.access_token;
338
+ }
339
+
340
+ async function checkPersonalDrive() {
341
+ if (!env.GMAIL_CLIENT_ID || !env.GMAIL_CLIENT_SECRET || !env.GMAIL_REFRESH_TOKEN) {
342
+ return skip("PERSONAL drive scope", "default GMAIL account not configured");
343
+ }
344
+ try {
345
+ const token = await gmailRefresh(env.GMAIL_REFRESH_TOKEN);
346
+ const { status } = await http("https://www.googleapis.com/drive/v3/about?fields=user", {
347
+ headers: { Authorization: `Bearer ${token}` },
348
+ });
349
+ status === 200
350
+ ? ok("PERSONAL drive scope")
351
+ : fail("PERSONAL drive scope", `about → ${status} (token minted without drive scopes — re-mint)`);
352
+ } catch (err) {
353
+ fail("PERSONAL drive scope", String(err.message).slice(0, 120));
354
+ }
355
+ }
356
+
357
+ async function checkDwdMailbox() {
358
+ if (!env.AGENT_INBOX_MAILBOX) {
359
+ return skip("DWD inbox mailbox", "AGENT_INBOX_MAILBOX unset");
360
+ }
361
+ if (!onCloudRun) {
362
+ return skip("DWD inbox mailbox", "needs the metadata server — run on Cloud Run");
363
+ }
364
+ try {
365
+ const token = await dwdToken(env.AGENT_INBOX_MAILBOX, "https://www.googleapis.com/auth/gmail.readonly");
366
+ const prof = await http("https://gmail.googleapis.com/gmail/v1/users/me/profile", {
367
+ headers: { Authorization: `Bearer ${token}` },
368
+ });
369
+ prof.json?.emailAddress
370
+ ? ok("DWD inbox mailbox", prof.json.emailAddress)
371
+ : fail("DWD inbox mailbox", "profile fetch failed");
372
+ } catch (err) {
373
+ fail("DWD inbox mailbox", String(err.message).slice(0, 120));
374
+ }
375
+ }
376
+
377
+ async function checkDwdDrive() {
378
+ if (!env.AGENT_INBOX_MAILBOX) {
379
+ return skip("DWD drive+docs+slides", "AGENT_INBOX_MAILBOX unset");
380
+ }
381
+ if (!onCloudRun) {
382
+ return skip("DWD drive+docs+slides", "needs the metadata server — run on Cloud Run");
383
+ }
384
+ try {
385
+ const token = await dwdToken(
386
+ env.AGENT_INBOX_MAILBOX,
387
+ "https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/spreadsheets https://www.googleapis.com/auth/documents https://www.googleapis.com/auth/presentations",
388
+ );
389
+ const { status } = await http("https://www.googleapis.com/drive/v3/files?pageSize=1", {
390
+ headers: { Authorization: `Bearer ${token}` },
391
+ });
392
+ status === 200 ? ok("DWD drive+docs+slides") : fail("DWD drive+docs+slides", `files → ${status}`);
393
+ } catch (err) {
394
+ fail("DWD drive+docs+slides", String(err.message).slice(0, 120));
395
+ }
396
+ }
397
+
398
+ async function checkDwdCalendar() {
399
+ if (!env.AGENT_INBOX_MAILBOX) {
400
+ return skip("DWD calendar (write scope)", "AGENT_INBOX_MAILBOX unset");
401
+ }
402
+ if (!onCloudRun) {
403
+ return skip("DWD calendar (write scope)", "needs the metadata server — run on Cloud Run");
404
+ }
405
+ try {
406
+ const token = await dwdToken(env.AGENT_INBOX_MAILBOX, "https://www.googleapis.com/auth/calendar");
407
+ const { status } = await http("https://www.googleapis.com/calendar/v3/users/me/calendarList?maxResults=1", {
408
+ headers: { Authorization: `Bearer ${token}` },
409
+ });
410
+ status === 200 ? ok("DWD calendar (write scope)") : fail("DWD calendar (write scope)", `calendarList → ${status}`);
411
+ } catch (err) {
412
+ fail("DWD calendar (write scope)", String(err.message).slice(0, 120));
413
+ }
414
+ }
415
+
416
+ async function checkSearchConsole() {
417
+ if (!onCloudRun) {
418
+ return skip("SEARCH CONSOLE", "needs the metadata server — run on Cloud Run");
419
+ }
420
+ try {
421
+ const token = await metadataToken(["https://www.googleapis.com/auth/webmasters.readonly"]);
422
+ const { status, json } = await http("https://www.googleapis.com/webmasters/v3/sites", {
423
+ headers: { Authorization: `Bearer ${token}` },
424
+ });
425
+ status === 200
426
+ ? ok("SEARCH CONSOLE", `${json?.siteEntry?.length ?? 0} properties`)
427
+ : fail("SEARCH CONSOLE", `sites → ${status} (SA not invited?)`);
428
+ } catch (err) {
429
+ fail("SEARCH CONSOLE", String(err.message).slice(0, 120));
430
+ }
431
+ }
432
+
433
+ async function checkGa4() {
434
+ const worlds = Object.keys(env).filter((k) => k.endsWith("GA4_PROPERTY_ID") && env[k]);
435
+ if (worlds.length === 0) {
436
+ return skip("GA4", "no *_GA4_PROPERTY_ID set");
437
+ }
438
+ if (!onCloudRun) {
439
+ return skip("GA4", "needs the metadata server — run on Cloud Run");
440
+ }
441
+ const token = await metadataToken(["https://www.googleapis.com/auth/analytics.readonly"]);
442
+ for (const k of worlds) {
443
+ const { status } = await http(`https://analyticsdata.googleapis.com/v1beta/properties/${env[k]}/metadata`, {
444
+ headers: { Authorization: `Bearer ${token}` },
445
+ });
446
+ status === 200 ? ok(`GA4 ${k}`) : fail(`GA4 ${k}`, `metadata → ${status} (SA not invited?)`);
447
+ }
448
+ }
449
+
450
+ async function checkGoogleAds() {
451
+ if (!env.GOOGLE_ADS_DEVELOPER_TOKEN || !env.SWEEPOS_GOOGLE_ADS_CUSTOMER_ID) {
452
+ return skip("GOOGLE ADS", "developer token / customer id not set");
453
+ }
454
+ if (!onCloudRun) {
455
+ return skip("GOOGLE ADS", "needs the metadata server — run on Cloud Run");
456
+ }
457
+ try {
458
+ const token = await metadataToken(["https://www.googleapis.com/auth/adwords"]);
459
+ const headers = {
460
+ Authorization: `Bearer ${token}`,
461
+ "developer-token": env.GOOGLE_ADS_DEVELOPER_TOKEN,
462
+ "Content-Type": "application/json",
463
+ };
464
+ if (env.GOOGLE_ADS_LOGIN_CUSTOMER_ID) {
465
+ headers["login-customer-id"] = env.GOOGLE_ADS_LOGIN_CUSTOMER_ID;
466
+ }
467
+ const { status, json } = await http(
468
+ `https://googleads.googleapis.com/v25/customers/${env.SWEEPOS_GOOGLE_ADS_CUSTOMER_ID}/googleAds:searchStream`,
469
+ { method: "POST", headers, body: JSON.stringify({ query: "SELECT customer.id FROM customer LIMIT 1" }) },
470
+ );
471
+ status === 200
472
+ ? ok("GOOGLE ADS")
473
+ : fail("GOOGLE ADS", (json?.[0]?.error?.message ?? json?.error?.message ?? `→ ${status}`).slice(0, 120));
474
+ } catch (err) {
475
+ fail("GOOGLE ADS", String(err.message).slice(0, 120));
476
+ }
477
+ }
478
+
479
+ async function checkDeploymentsContext() {
480
+ const project = env.CONTEXT_DEPLOYMENTS_PROJECT;
481
+ if (!project) {
482
+ return skip("Deployments context (run.viewer)", "CONTEXT_DEPLOYMENTS_PROJECT unset");
483
+ }
484
+ if (!onCloudRun) {
485
+ return skip("Deployments context (run.viewer)", "needs the metadata server — run on Cloud Run");
486
+ }
487
+ const region = env.CONTEXT_DEPLOYMENTS_REGION || "europe-west1";
488
+ const token = await metadataToken();
489
+ const { status } = await http(`https://run.googleapis.com/v2/projects/${project}/locations/${region}/services`, {
490
+ headers: { Authorization: `Bearer ${token}` },
491
+ });
492
+ status === 200
493
+ ? ok("Deployments context (run.viewer)", project)
494
+ : fail("Deployments context (run.viewer)", `list → ${status}`);
495
+ }
496
+
497
+ function checkPresence() {
498
+ for (const k of ["CLAUDE_CODE_OAUTH_TOKEN", "AGENT_WEBHOOK_SECRET"]) {
499
+ env[k] ? ok(k, "present (not remotely testable)") : skip(k, "unset");
500
+ }
501
+ }
502
+
503
+ // --- Run ---------------------------------------------------------------------
504
+
505
+ try {
506
+ await metadataToken();
507
+ onCloudRun = true;
508
+ } catch {
509
+ onCloudRun = false;
510
+ }
511
+
512
+ checkPresence();
513
+
514
+ // A check that throws (a timeout, an unreachable host) becomes its own FAIL line —
515
+ // one dead surface must never take the rest of the report down with it.
516
+ const CHECKS = [
517
+ ["GITHUB_TOKEN", checkGithub],
518
+ ["GRAFANA (Loki)", checkGrafana],
519
+ ["RESEND_API_KEY", checkResend],
520
+ ["SWEEPOS internal API", checkInternalApi],
521
+ ["SWEEPOS_POSTGRES_URL_READONLY", checkPostgres],
522
+ ["POSTHOG", checkPosthog],
523
+ ["AHREFS_API_KEY", checkAhrefs],
524
+ ["CLOUDFLARE", checkCloudflare],
525
+ ["ELEVENLABS_API_KEY", checkElevenlabs],
526
+ ["GOOGLE_API_KEY (Places)", checkPlaces],
527
+ ["GMAIL (OAuth)", checkGmailAccounts],
528
+ ["MSGRAPH (Graph)", checkGraphAccounts],
529
+ ["PERSONAL drive scope", checkPersonalDrive],
530
+ ["DWD inbox mailbox", checkDwdMailbox],
531
+ ["DWD drive+docs+slides", checkDwdDrive],
532
+ ["DWD calendar (write scope)", checkDwdCalendar],
533
+ ["SEARCH CONSOLE", checkSearchConsole],
534
+ ["GA4", checkGa4],
535
+ ["GOOGLE ADS", checkGoogleAds],
536
+ ["Deployments context (run.viewer)", checkDeploymentsContext],
537
+ ];
538
+ await Promise.all(
539
+ CHECKS.map(([label, check]) =>
540
+ check().catch((e) => fail(label, `check threw: ${e?.name === "TimeoutError" ? "timed out" : (e?.message ?? e)}`)),
541
+ ),
542
+ );
543
+
544
+ results.sort((a, b) => a.name.localeCompare(b.name));
545
+ const pad = Math.max(...results.map((r) => r.name.length)) + 2;
546
+ console.log(
547
+ `\nCredential check — ${onCloudRun ? "Cloud Run (all surfaces)" : "local (metadata-server surfaces skipped)"}\n`,
548
+ );
549
+ for (const r of results) {
550
+ const icon = r.status === "ok" ? "✅" : r.status === "FAIL" ? "❌" : "⏭️ ";
551
+ console.log(`${icon} ${r.name.padEnd(pad)} ${r.status === "ok" ? r.detail : r.detail && `— ${r.detail}`}`);
552
+ }
553
+ const failures = results.filter((r) => r.status === "FAIL");
554
+ console.log(
555
+ `\n${results.filter((r) => r.status === "ok").length} ok, ${failures.length} failed, ${results.filter((r) => r.status === "skip").length} skipped`,
556
+ );
557
+ process.exit(failures.length ? 1 : 0);