@riocrypto/common-server 1.0.2934 → 1.0.2936

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/build/bot/gcp-logging-client.d.ts +26 -0
  2. package/build/bot/gcp-logging-client.js +127 -0
  3. package/build/bot/git-repo-cache.d.ts +33 -0
  4. package/build/bot/git-repo-cache.js +244 -0
  5. package/build/bot/github-app-auth.d.ts +15 -0
  6. package/build/bot/github-app-auth.js +93 -0
  7. package/build/bot/injection-markers.d.ts +1 -0
  8. package/build/bot/injection-markers.js +16 -0
  9. package/build/bot/k8s-client.d.ts +45 -0
  10. package/build/bot/k8s-client.js +151 -0
  11. package/build/bot/redactor.d.ts +11 -0
  12. package/build/bot/redactor.js +186 -0
  13. package/build/bot/slack-notifier.d.ts +15 -0
  14. package/build/bot/slack-notifier.js +54 -0
  15. package/build/bot/tool.d.ts +26 -0
  16. package/build/bot/tool.js +60 -0
  17. package/build/index.d.ts +15 -1
  18. package/build/index.js +15 -1
  19. package/build/models/inbound-deposit-intake.d.ts +43 -0
  20. package/build/models/inbound-deposit-intake.js +106 -0
  21. package/build/models/security-baseline.d.ts +13 -0
  22. package/build/models/security-baseline.js +23 -0
  23. package/build/models/security-bot-audit-event.d.ts +36 -0
  24. package/build/models/security-bot-audit-event.js +51 -0
  25. package/build/models/security-bot-controls.d.ts +23 -0
  26. package/build/models/security-bot-controls.js +42 -0
  27. package/build/models/security-finding.d.ts +38 -0
  28. package/build/models/security-finding.js +59 -0
  29. package/build/models/security-known-pattern.d.ts +12 -0
  30. package/build/models/security-known-pattern.js +28 -0
  31. package/build/models/security-sweep-run.d.ts +41 -0
  32. package/build/models/security-sweep-run.js +67 -0
  33. package/package.json +3 -2
  34. package/build/models/fintoc-deposit-intake.d.ts +0 -39
  35. package/build/models/fintoc-deposit-intake.js +0 -95
@@ -0,0 +1,151 @@
1
+ "use strict";
2
+ // Minimal, GET-only client for the in-cluster Kubernetes API.
3
+ //
4
+ // Deliberately not the official @kubernetes/client-node package. That
5
+ // library is ESM only, pulls in websockets, tar, socks and OIDC, and exposes
6
+ // exec/attach/delete on the same object we would hand to the tool layer. We
7
+ // need four GET endpoints. Fewer moving parts, no write surface in the code
8
+ // at all, and the RBAC Role bound to the pod is the second lock.
9
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
10
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
11
+ return new (P || (P = Promise))(function (resolve, reject) {
12
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
13
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
14
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
15
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
16
+ });
17
+ };
18
+ var __importDefault = (this && this.__importDefault) || function (mod) {
19
+ return (mod && mod.__esModule) ? mod : { "default": mod };
20
+ };
21
+ Object.defineProperty(exports, "__esModule", { value: true });
22
+ exports.K8sClient = void 0;
23
+ const axios_1 = __importDefault(require("axios"));
24
+ const fs_1 = __importDefault(require("fs"));
25
+ const https_1 = __importDefault(require("https"));
26
+ const SA_DIR = "/var/run/secrets/kubernetes.io/serviceaccount";
27
+ const minutesSince = (iso) => {
28
+ if (!iso)
29
+ return -1;
30
+ const t = new Date(iso).getTime();
31
+ return Number.isNaN(t) ? -1 : Math.floor((Date.now() - t) / 60000);
32
+ };
33
+ class K8sClient {
34
+ constructor(baseURL, ca, timeoutMs) {
35
+ this.http = axios_1.default.create({
36
+ baseURL,
37
+ timeout: timeoutMs,
38
+ httpsAgent: ca ? new https_1.default.Agent({ ca }) : undefined,
39
+ // Only GET is ever issued. Kept explicit so a future refactor cannot
40
+ // silently add a body-carrying method through this instance.
41
+ maxRedirects: 0,
42
+ validateStatus: (s) => s >= 200 && s < 300,
43
+ });
44
+ }
45
+ static fromInCluster(timeoutMs) {
46
+ const host = process.env.KUBERNETES_SERVICE_HOST;
47
+ const port = process.env.KUBERNETES_SERVICE_PORT || "443";
48
+ if (!host || !fs_1.default.existsSync(`${SA_DIR}/token`)) {
49
+ return null;
50
+ }
51
+ const ca = fs_1.default.existsSync(`${SA_DIR}/ca.crt`)
52
+ ? fs_1.default.readFileSync(`${SA_DIR}/ca.crt`)
53
+ : undefined;
54
+ return new K8sClient(`https://${host}:${port}`, ca, timeoutMs);
55
+ }
56
+ // Projected tokens rotate, so read on every call rather than caching.
57
+ headers() {
58
+ const token = fs_1.default.readFileSync(`${SA_DIR}/token`, "utf8").trim();
59
+ return { Authorization: `Bearer ${token}`, Accept: "application/json" };
60
+ }
61
+ get(path, params) {
62
+ return __awaiter(this, void 0, void 0, function* () {
63
+ const res = yield this.http.get(path, { params, headers: this.headers() });
64
+ return res.data;
65
+ });
66
+ }
67
+ listPods(namespace) {
68
+ return __awaiter(this, void 0, void 0, function* () {
69
+ const data = yield this.get(`/api/v1/namespaces/${namespace}/pods`, {
70
+ limit: 500,
71
+ });
72
+ return (data.items || []).map((pod) => {
73
+ var _a, _b, _c, _d, _e, _f, _g;
74
+ const statuses = ((_a = pod.status) === null || _a === void 0 ? void 0 : _a.containerStatuses) || [];
75
+ const readyCount = statuses.filter((s) => s.ready).length;
76
+ const restarts = statuses.reduce((n, s) => n + (s.restartCount || 0), 0);
77
+ const lastState = statuses
78
+ .map((s) => {
79
+ var _a;
80
+ const term = (_a = s.lastState) === null || _a === void 0 ? void 0 : _a.terminated;
81
+ if (!term)
82
+ return null;
83
+ return `${s.name}: ${term.reason || "terminated"} exit=${term.exitCode} at ${term.finishedAt}`;
84
+ })
85
+ .filter(Boolean)
86
+ .join("; ");
87
+ return Object.assign({ name: (_b = pod.metadata) === null || _b === void 0 ? void 0 : _b.name, phase: (_c = pod.status) === null || _c === void 0 ? void 0 : _c.phase, ready: `${readyCount}/${(((_d = pod.spec) === null || _d === void 0 ? void 0 : _d.containers) || []).length}`, restarts, ageMinutes: minutesSince((_e = pod.metadata) === null || _e === void 0 ? void 0 : _e.creationTimestamp), containers: (((_f = pod.spec) === null || _f === void 0 ? void 0 : _f.containers) || []).map((c) => c.name), node: (_g = pod.spec) === null || _g === void 0 ? void 0 : _g.nodeName }, (lastState ? { lastState } : {}));
88
+ });
89
+ });
90
+ }
91
+ getPodLog(namespace, pod, options) {
92
+ return __awaiter(this, void 0, void 0, function* () {
93
+ const params = {
94
+ tailLines: options.tailLines,
95
+ sinceSeconds: options.sinceSeconds,
96
+ timestamps: true,
97
+ };
98
+ if (options.container)
99
+ params.container = options.container;
100
+ if (options.previous)
101
+ params.previous = true;
102
+ const res = yield this.http.get(`/api/v1/namespaces/${namespace}/pods/${pod}/log`, {
103
+ params,
104
+ headers: Object.assign(Object.assign({}, this.headers()), { Accept: "text/plain" }),
105
+ responseType: "text",
106
+ transformResponse: (d) => d,
107
+ });
108
+ return typeof res.data === "string" ? res.data : String(res.data);
109
+ });
110
+ }
111
+ listDeployments(namespace) {
112
+ return __awaiter(this, void 0, void 0, function* () {
113
+ const data = yield this.get(`/apis/apps/v1/namespaces/${namespace}/deployments`, { limit: 500 });
114
+ return (data.items || []).map((d) => {
115
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o;
116
+ return ({
117
+ name: (_a = d.metadata) === null || _a === void 0 ? void 0 : _a.name,
118
+ desired: (_c = (_b = d.spec) === null || _b === void 0 ? void 0 : _b.replicas) !== null && _c !== void 0 ? _c : 0,
119
+ ready: (_e = (_d = d.status) === null || _d === void 0 ? void 0 : _d.readyReplicas) !== null && _e !== void 0 ? _e : 0,
120
+ available: (_g = (_f = d.status) === null || _f === void 0 ? void 0 : _f.availableReplicas) !== null && _g !== void 0 ? _g : 0,
121
+ updated: (_j = (_h = d.status) === null || _h === void 0 ? void 0 : _h.updatedReplicas) !== null && _j !== void 0 ? _j : 0,
122
+ images: (((_m = (_l = (_k = d.spec) === null || _k === void 0 ? void 0 : _k.template) === null || _l === void 0 ? void 0 : _l.spec) === null || _m === void 0 ? void 0 : _m.containers) || []).map((c) => c.image),
123
+ conditions: (((_o = d.status) === null || _o === void 0 ? void 0 : _o.conditions) || []).map((c) => `${c.type}=${c.status}${c.reason ? ` (${c.reason})` : ""}`),
124
+ });
125
+ });
126
+ });
127
+ }
128
+ listEvents(namespace, involvedObjectName) {
129
+ return __awaiter(this, void 0, void 0, function* () {
130
+ const params = { limit: 500 };
131
+ if (involvedObjectName) {
132
+ params.fieldSelector = `involvedObject.name=${involvedObjectName}`;
133
+ }
134
+ const data = yield this.get(`/api/v1/namespaces/${namespace}/events`, params);
135
+ return (data.items || [])
136
+ .map((e) => {
137
+ var _a, _b, _c;
138
+ return ({
139
+ lastSeen: e.lastTimestamp || e.eventTime || ((_a = e.metadata) === null || _a === void 0 ? void 0 : _a.creationTimestamp) || "",
140
+ type: e.type,
141
+ reason: e.reason,
142
+ object: `${(_b = e.involvedObject) === null || _b === void 0 ? void 0 : _b.kind}/${(_c = e.involvedObject) === null || _c === void 0 ? void 0 : _c.name}`,
143
+ count: e.count || 1,
144
+ message: e.message || "",
145
+ });
146
+ })
147
+ .sort((a, b) => (a.lastSeen < b.lastSeen ? 1 : -1));
148
+ });
149
+ }
150
+ }
151
+ exports.K8sClient = K8sClient;
@@ -0,0 +1,11 @@
1
+ export type RedactionCategory = "privateKey" | "urlCredentials" | "cookieHeader" | "credentialField" | "jwt" | "authHeader" | "apiKey" | "curp" | "rfc" | "clabe" | "phone" | "card" | "email";
2
+ export interface RedactionResult {
3
+ text: string;
4
+ counts: Partial<Record<RedactionCategory, number>>;
5
+ total: number;
6
+ }
7
+ export declare function redact(input: string): RedactionResult;
8
+ export declare const CREDENTIAL_CATEGORIES: RedactionCategory[];
9
+ export declare function redactCredentials(input: string): RedactionResult;
10
+ export declare function countCredentialRedactions(counts: Partial<Record<RedactionCategory, number>>): number;
11
+ export declare function summarizeRedactions(counts: Partial<Record<RedactionCategory, number>>): string;
@@ -0,0 +1,186 @@
1
+ "use strict";
2
+ // Deterministic scrubber applied to every tool result BEFORE it is placed in
3
+ // the model context, and again to the model's final answer before it is
4
+ // returned to the engineer. Nothing here depends on the model. If a value is
5
+ // caught by a rule it never reaches Anthropic and never reaches the browser.
6
+ //
7
+ // Rule order matters. Structured secrets (key blocks, URLs with credentials,
8
+ // named credential fields) run first so that a later, looser pattern does
9
+ // not chew up part of a match and leave a fragment behind.
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.summarizeRedactions = exports.countCredentialRedactions = exports.redactCredentials = exports.CREDENTIAL_CATEGORIES = exports.redact = void 0;
12
+ const REDACTED = "[REDACTED]";
13
+ // Key names whose values are always secrets. Matched as the tail of the key
14
+ // so `accessToken`, `x-api-key`, `db_password` and `Authorization` all hit,
15
+ // while identifiers like `authId` or `tokenId` do not.
16
+ const CREDENTIAL_KEY = "(?:[A-Za-z0-9_.\\-]*(?:password|passwd|secret|token|api[_-]?key|apikey|authorization|cookie|private[_-]?key|mnemonic|seed[_-]?phrase|cvv|cvc|access[_-]?key|signing[_-]?key|credentials?)|seed|otp|pin|pwd|pass)";
17
+ const PHONE_KEY = "(?:phone|phone[_-]?number|tel|telefono|teléfono|celular|mobile|whatsapp)";
18
+ const keepLast4 = (digits, label) => `${label} ****${digits.slice(-4)}`;
19
+ const luhnValid = (digits) => {
20
+ let sum = 0;
21
+ let alternate = false;
22
+ for (let i = digits.length - 1; i >= 0; i--) {
23
+ let n = digits.charCodeAt(i) - 48;
24
+ if (alternate) {
25
+ n *= 2;
26
+ if (n > 9)
27
+ n -= 9;
28
+ }
29
+ sum += n;
30
+ alternate = !alternate;
31
+ }
32
+ return sum % 10 === 0;
33
+ };
34
+ const RULES = [
35
+ {
36
+ category: "privateKey",
37
+ pattern: /-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z0-9 ]*PRIVATE KEY-----/g,
38
+ replace: () => "[REDACTED_PRIVATE_KEY]",
39
+ },
40
+ {
41
+ category: "urlCredentials",
42
+ pattern: /(\b[a-zA-Z][a-zA-Z0-9+.-]*:\/\/)([^\s/:@"']+):([^\s/@"']+)@/g,
43
+ replace: (_m, scheme, user) => `${scheme}${user}:${REDACTED}@`,
44
+ },
45
+ {
46
+ category: "cookieHeader",
47
+ pattern: /\b(cookie|set-cookie)(["']?\s*[:=]\s*)(?:"[^"\n]*"|'[^'\n]*'|[^\n"']+)/gi,
48
+ replace: (_m, header, sep) => `${header}${sep}${REDACTED}`,
49
+ },
50
+ {
51
+ // JSON style: "password": "hunter2" | "token": 'abc' | "otp": 123456
52
+ category: "credentialField",
53
+ pattern: new RegExp(`("${CREDENTIAL_KEY}"\\s*:\\s*)("(?:[^"\\\\]|\\\\.)*"|'[^']*'|[^,}\\]\\s]+)`, "gi"),
54
+ replace: (_m, prefix) => `${prefix}"${REDACTED}"`,
55
+ },
56
+ {
57
+ // Text style: password=hunter2 | Authorization: Bearer xyz | token: abc
58
+ category: "credentialField",
59
+ pattern: new RegExp(`\\b(${CREDENTIAL_KEY})(\\s*[=:]\\s*)((?:Bearer|Basic)\\s+[^\\s,;&"']+|"[^"\\n]*"|'[^'\\n]*'|[^\\s,;&"']+)`, "gi"),
60
+ replace: (_m, key, sep) => `${key}${sep}${REDACTED}`,
61
+ },
62
+ {
63
+ category: "jwt",
64
+ pattern: /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/g,
65
+ replace: () => "[REDACTED_JWT]",
66
+ },
67
+ {
68
+ category: "authHeader",
69
+ pattern: /\b(Bearer|Basic)\s+[A-Za-z0-9\-._~+/]{16,}=*/g,
70
+ replace: (_m, scheme) => `${scheme} ${REDACTED}`,
71
+ },
72
+ {
73
+ category: "apiKey",
74
+ pattern: /\b(?:sk-ant-[A-Za-z0-9_-]{20,}|sk-[A-Za-z0-9_-]{20,}|sk_live_[A-Za-z0-9]{10,}|rk_live_[A-Za-z0-9]{10,}|AKIA[0-9A-Z]{16}|AIza[0-9A-Za-z_-]{35}|gh[pousr]_[A-Za-z0-9]{36,}|xox[abprs]-[A-Za-z0-9-]{10,}|glpat-[A-Za-z0-9_-]{20,})\b/g,
75
+ replace: () => "[REDACTED_API_KEY]",
76
+ },
77
+ {
78
+ category: "curp",
79
+ pattern: /\b[A-Z]{4}\d{6}[HM][A-Z]{2}[B-DF-HJ-NP-TV-Z]{3}[A-Z0-9]\d\b/g,
80
+ replace: () => "[REDACTED_CURP]",
81
+ },
82
+ {
83
+ // Field-aware only. A bare RFC shape (3-4 letters, 6 digits, 3 alnum)
84
+ // collides with payment tracking keys, which are exactly what this bot
85
+ // is used to search for.
86
+ category: "rfc",
87
+ pattern: /("?\b(?:rfc|tax[_-]?id)"?\s*[:=]\s*"?)([A-ZÑ&]{3,4}\d{6}[A-Z0-9]{3})\b/gi,
88
+ replace: (_m, prefix) => `${prefix}[REDACTED_RFC]`,
89
+ },
90
+ {
91
+ category: "clabe",
92
+ pattern: /\b\d{18}\b/g,
93
+ replace: (m) => keepLast4(m, "CLABE"),
94
+ },
95
+ {
96
+ // Field-aware phone masking. A bare 10 digit run is far more likely to be
97
+ // a unix timestamp than a phone number, so we only mask when the number
98
+ // is labelled or carries an international prefix.
99
+ category: "phone",
100
+ pattern: new RegExp(`("?\\b${PHONE_KEY}"?\\s*[:=]\\s*"?)(\\+?[\\d][\\d\\s\\-()]{8,18}\\d)`, "gi"),
101
+ replace: (_m, prefix, number) => `${prefix}${keepLast4(number.replace(/\D/g, ""), "PHONE")}`,
102
+ },
103
+ {
104
+ category: "phone",
105
+ pattern: /\+\d{1,3}[\s-]?\(?\d{2,4}\)?[\s-]?\d{3,4}[\s-]?\d{4}\b/g,
106
+ replace: (m) => keepLast4(m.replace(/\D/g, ""), "PHONE"),
107
+ },
108
+ {
109
+ category: "card",
110
+ pattern: /\b(?:\d[ -]?){12,18}\d\b/g,
111
+ replace: (m) => {
112
+ const digits = m.replace(/\D/g, "");
113
+ if (digits.length < 13 || digits.length > 19 || !luhnValid(digits)) {
114
+ return m;
115
+ }
116
+ // 13 digit runs starting with 1 are millisecond timestamps, which
117
+ // pass Luhn one time in ten. Real 13 digit cards are old Visa (4...).
118
+ if (digits.length === 13 && digits.startsWith("1")) {
119
+ return m;
120
+ }
121
+ return keepLast4(digits, "CARD");
122
+ },
123
+ },
124
+ {
125
+ category: "email",
126
+ pattern: /\b([A-Za-z0-9._%+-])[A-Za-z0-9._%+-]*@([A-Za-z0-9.-]+\.[A-Za-z]{2,})\b/g,
127
+ replace: (_m, first, domain) => `${first}***@${domain}`,
128
+ },
129
+ ];
130
+ const applyRules = (input, rules) => {
131
+ const counts = {};
132
+ let text = input;
133
+ let total = 0;
134
+ for (const rule of rules) {
135
+ let hits = 0;
136
+ text = text.replace(rule.pattern, (match, ...rest) => {
137
+ // String.replace passes (match, ...groups, offset, string). Drop the
138
+ // trailing offset and source so the rule only sees its groups.
139
+ const groups = rest.slice(0, -2);
140
+ const replaced = rule.replace(match, ...groups);
141
+ if (replaced !== match)
142
+ hits++;
143
+ return replaced;
144
+ });
145
+ if (hits > 0) {
146
+ counts[rule.category] = (counts[rule.category] || 0) + hits;
147
+ total += hits;
148
+ }
149
+ }
150
+ return { text, counts, total };
151
+ };
152
+ function redact(input) {
153
+ return applyRules(input, RULES);
154
+ }
155
+ exports.redact = redact;
156
+ // Categories that mean "a secret was present". PII categories (email, phone,
157
+ // CLABE, card, CURP, RFC) are masked for hygiene but are normal debugging
158
+ // data; only credential hits are treated as security signals.
159
+ exports.CREDENTIAL_CATEGORIES = [
160
+ "privateKey",
161
+ "urlCredentials",
162
+ "cookieHeader",
163
+ "credentialField",
164
+ "jwt",
165
+ "authHeader",
166
+ "apiKey",
167
+ ];
168
+ // For the engineer's own question. Emails, ids, CLABEs and phone numbers in
169
+ // a question are what the engineer is asking about and must survive, so
170
+ // only credential material is scrubbed before the question is stored or
171
+ // sent to the model.
172
+ function redactCredentials(input) {
173
+ return applyRules(input, RULES.filter((rule) => exports.CREDENTIAL_CATEGORIES.includes(rule.category)));
174
+ }
175
+ exports.redactCredentials = redactCredentials;
176
+ function countCredentialRedactions(counts) {
177
+ return exports.CREDENTIAL_CATEGORIES.reduce((n, c) => n + (counts[c] || 0), 0);
178
+ }
179
+ exports.countCredentialRedactions = countCredentialRedactions;
180
+ function summarizeRedactions(counts) {
181
+ const parts = Object.entries(counts)
182
+ .filter(([, n]) => (n || 0) > 0)
183
+ .map(([category, n]) => `${category}: ${n}`);
184
+ return parts.length > 0 ? parts.join(", ") : "none";
185
+ }
186
+ exports.summarizeRedactions = summarizeRedactions;
@@ -0,0 +1,15 @@
1
+ export interface SlackNotifierOptions {
2
+ channelId: string;
3
+ prefix: string;
4
+ localToken?: string;
5
+ log?: (message: string) => void;
6
+ }
7
+ export declare class SlackNotifier {
8
+ private readonly options;
9
+ private queue;
10
+ private readonly log;
11
+ constructor(options: SlackNotifierOptions);
12
+ get enabled(): boolean;
13
+ notify(text: string): void;
14
+ private postWithLocalToken;
15
+ }
@@ -0,0 +1,54 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.SlackNotifier = void 0;
16
+ const axios_1 = __importDefault(require("axios"));
17
+ const slack_client_1 = require("../clients/slack-client");
18
+ // Posts are chained so they reach Slack in the order they were raised (the
19
+ // signal that caused a lockout before the lockout itself). Callers never
20
+ // wait on the chain. An empty channel id disables sending entirely.
21
+ class SlackNotifier {
22
+ constructor(options) {
23
+ this.options = options;
24
+ this.queue = Promise.resolve();
25
+ this.log = options.log || (() => undefined);
26
+ }
27
+ get enabled() {
28
+ return this.options.channelId.length > 0;
29
+ }
30
+ notify(text) {
31
+ if (!this.enabled)
32
+ return;
33
+ const prefixed = `${this.options.prefix} ${text}`;
34
+ this.queue = this.queue.then(() => (this.options.localToken
35
+ ? this.postWithLocalToken(prefixed)
36
+ : slack_client_1.slackClient.sendMessage(prefixed, this.options.channelId))
37
+ .then(() => this.log(`Slack notify sent to ${this.options.channelId}`))
38
+ .catch((error) => this.log(`Slack notify failed: ${(error === null || error === void 0 ? void 0 : error.message) || error}`)));
39
+ }
40
+ postWithLocalToken(text) {
41
+ var _a, _b;
42
+ return __awaiter(this, void 0, void 0, function* () {
43
+ const response = yield axios_1.default.post("https://slack.com/api/chat.postMessage", { channel: this.options.channelId, text }, {
44
+ headers: { Authorization: `Bearer ${this.options.localToken}` },
45
+ timeout: 10000,
46
+ maxRedirects: 0,
47
+ });
48
+ if (!((_a = response.data) === null || _a === void 0 ? void 0 : _a.ok)) {
49
+ throw new Error(((_b = response.data) === null || _b === void 0 ? void 0 : _b.error) || "unknown Slack error");
50
+ }
51
+ });
52
+ }
53
+ }
54
+ exports.SlackNotifier = SlackNotifier;
@@ -0,0 +1,26 @@
1
+ export declare class ToolInputError extends Error {
2
+ }
3
+ export declare class ToolAccessError extends ToolInputError {
4
+ }
5
+ export interface ToolInputSchema {
6
+ type: "object";
7
+ properties?: Record<string, unknown>;
8
+ required?: string[];
9
+ [key: string]: unknown;
10
+ }
11
+ export interface ToolExecution {
12
+ summary: string;
13
+ output: string;
14
+ }
15
+ export interface BotTool {
16
+ name: string;
17
+ description: string;
18
+ inputSchema: ToolInputSchema;
19
+ isSourceCode?: boolean;
20
+ execute(input: Record<string, unknown>): Promise<ToolExecution>;
21
+ }
22
+ export declare const requireString: (input: Record<string, unknown>, key: string, maxLength: number) => string;
23
+ export declare const optionalString: (input: Record<string, unknown>, key: string, maxLength: number) => string | undefined;
24
+ export declare const clampInt: (input: Record<string, unknown>, key: string, fallback: number, min: number, max: number) => number;
25
+ export declare const optionalBool: (input: Record<string, unknown>, key: string) => boolean;
26
+ export declare const requireK8sName: (input: Record<string, unknown>, key: string) => string;
@@ -0,0 +1,60 @@
1
+ "use strict";
2
+ // Shared primitives for model-facing tools. Every tool built on these is a
3
+ // read; the helpers exist so model input is validated the same way in every
4
+ // bot before it touches GCP, Kubernetes, git or Mongo.
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.requireK8sName = exports.optionalBool = exports.clampInt = exports.optionalString = exports.requireString = exports.ToolAccessError = exports.ToolInputError = void 0;
7
+ // Raised when the model passes something a tool refuses to act on (bad type,
8
+ // out of range, namespace not allowed). Sent back to the model as an
9
+ // is_error tool_result so it can correct itself.
10
+ class ToolInputError extends Error {
11
+ }
12
+ exports.ToolInputError = ToolInputError;
13
+ // A ToolInputError that means "not allowed" rather than "malformed": a repo
14
+ // outside the allowlist, a denied path, a namespace out of scope. Bots count
15
+ // these as security signals; ordinary input mistakes are not.
16
+ class ToolAccessError extends ToolInputError {
17
+ }
18
+ exports.ToolAccessError = ToolAccessError;
19
+ const requireString = (input, key, maxLength) => {
20
+ const value = input[key];
21
+ if (typeof value !== "string" || value.trim().length === 0) {
22
+ throw new ToolInputError(`"${key}" must be a non-empty string`);
23
+ }
24
+ if (value.length > maxLength) {
25
+ throw new ToolInputError(`"${key}" exceeds ${maxLength} characters`);
26
+ }
27
+ return value.trim();
28
+ };
29
+ exports.requireString = requireString;
30
+ const optionalString = (input, key, maxLength) => {
31
+ if (input[key] === undefined || input[key] === null || input[key] === "") {
32
+ return undefined;
33
+ }
34
+ return (0, exports.requireString)(input, key, maxLength);
35
+ };
36
+ exports.optionalString = optionalString;
37
+ const clampInt = (input, key, fallback, min, max) => {
38
+ const raw = input[key];
39
+ if (raw === undefined || raw === null)
40
+ return fallback;
41
+ const n = typeof raw === "number" ? raw : Number(raw);
42
+ if (!Number.isFinite(n)) {
43
+ throw new ToolInputError(`"${key}" must be a number`);
44
+ }
45
+ return Math.min(max, Math.max(min, Math.floor(n)));
46
+ };
47
+ exports.clampInt = clampInt;
48
+ const optionalBool = (input, key) => input[key] === true || input[key] === "true";
49
+ exports.optionalBool = optionalBool;
50
+ // Kubernetes names: lowercase DNS labels. Anything else is refused before it
51
+ // is interpolated into an API path.
52
+ const K8S_NAME = /^[a-z0-9]([-a-z0-9.]{0,251}[a-z0-9])?$/;
53
+ const requireK8sName = (input, key) => {
54
+ const value = (0, exports.requireString)(input, key, 253);
55
+ if (!K8S_NAME.test(value)) {
56
+ throw new ToolInputError(`"${key}" is not a valid Kubernetes name`);
57
+ }
58
+ return value;
59
+ };
60
+ exports.requireK8sName = requireK8sName;
package/build/index.d.ts CHANGED
@@ -90,7 +90,7 @@ export * from "./models/STP-deposit-CLABE";
90
90
  export * from "./models/STP-settings";
91
91
  export * from "./models/fintoc-mxn-withdrawal";
92
92
  export * from "./models/fintoc-deposit-CLABE";
93
- export * from "./models/fintoc-deposit-intake";
93
+ export * from "./models/inbound-deposit-intake";
94
94
  export * from "./models/processor-readiness";
95
95
  export * from "./models/payment-address-verification";
96
96
  export * from "./models/alfin-virtual-cci";
@@ -138,6 +138,20 @@ export * from "./models/internal-support-chat-message";
138
138
  export * from "./models/internal-support-audit-event";
139
139
  export * from "./models/internal-support-case";
140
140
  export * from "./models/internal-support-security";
141
+ export * from "./models/security-finding";
142
+ export * from "./models/security-sweep-run";
143
+ export * from "./models/security-baseline";
144
+ export * from "./models/security-known-pattern";
145
+ export * from "./models/security-bot-controls";
146
+ export * from "./models/security-bot-audit-event";
147
+ export * from "./bot/redactor";
148
+ export * from "./bot/injection-markers";
149
+ export * from "./bot/tool";
150
+ export * from "./bot/gcp-logging-client";
151
+ export * from "./bot/k8s-client";
152
+ export * from "./bot/github-app-auth";
153
+ export * from "./bot/git-repo-cache";
154
+ export * from "./bot/slack-notifier";
141
155
  export * from "./models/compliance-bot-rule";
142
156
  export * from "./models/compliance-bot-rule-result";
143
157
  export * from "./models/compliance-rule-set";
package/build/index.js CHANGED
@@ -106,7 +106,7 @@ __exportStar(require("./models/STP-deposit-CLABE"), exports);
106
106
  __exportStar(require("./models/STP-settings"), exports);
107
107
  __exportStar(require("./models/fintoc-mxn-withdrawal"), exports);
108
108
  __exportStar(require("./models/fintoc-deposit-CLABE"), exports);
109
- __exportStar(require("./models/fintoc-deposit-intake"), exports);
109
+ __exportStar(require("./models/inbound-deposit-intake"), exports);
110
110
  __exportStar(require("./models/processor-readiness"), exports);
111
111
  __exportStar(require("./models/payment-address-verification"), exports);
112
112
  __exportStar(require("./models/alfin-virtual-cci"), exports);
@@ -154,6 +154,20 @@ __exportStar(require("./models/internal-support-chat-message"), exports);
154
154
  __exportStar(require("./models/internal-support-audit-event"), exports);
155
155
  __exportStar(require("./models/internal-support-case"), exports);
156
156
  __exportStar(require("./models/internal-support-security"), exports);
157
+ __exportStar(require("./models/security-finding"), exports);
158
+ __exportStar(require("./models/security-sweep-run"), exports);
159
+ __exportStar(require("./models/security-baseline"), exports);
160
+ __exportStar(require("./models/security-known-pattern"), exports);
161
+ __exportStar(require("./models/security-bot-controls"), exports);
162
+ __exportStar(require("./models/security-bot-audit-event"), exports);
163
+ __exportStar(require("./bot/redactor"), exports);
164
+ __exportStar(require("./bot/injection-markers"), exports);
165
+ __exportStar(require("./bot/tool"), exports);
166
+ __exportStar(require("./bot/gcp-logging-client"), exports);
167
+ __exportStar(require("./bot/k8s-client"), exports);
168
+ __exportStar(require("./bot/github-app-auth"), exports);
169
+ __exportStar(require("./bot/git-repo-cache"), exports);
170
+ __exportStar(require("./bot/slack-notifier"), exports);
157
171
  __exportStar(require("./models/compliance-bot-rule"), exports);
158
172
  __exportStar(require("./models/compliance-bot-rule-result"), exports);
159
173
  __exportStar(require("./models/compliance-rule-set"), exports);
@@ -0,0 +1,43 @@
1
+ import { Mongoose, Model, Document, HydratedDocument } from "mongoose";
2
+ import { TreasuryProvider } from "@riocrypto/common";
3
+ export declare enum InboundDepositIntakeStatus {
4
+ /** Recorded, waiting for the worker. */
5
+ Pending = "pending",
6
+ /** A worker has taken it. Reset to pending if left here too long. */
7
+ Processing = "processing",
8
+ /** Matching ran to completion, whatever it concluded. See `outcome`. */
9
+ Done = "done",
10
+ /** Matching threw and retries are exhausted. Needs a person. */
11
+ Failed = "failed"
12
+ }
13
+ export type InboundDepositIntakeSource = "webhook" | "poller";
14
+ interface InboundDepositIntakeAttrs {
15
+ provider: TreasuryProvider;
16
+ /** The provider's id for the deposit, e.g. a Fintoc transfer id. */
17
+ externalId: string;
18
+ source: InboundDepositIntakeSource;
19
+ /** The provider's webhook event id, for tracing back to their dashboard. */
20
+ externalEventId?: string;
21
+ receivedAt: Date;
22
+ }
23
+ interface InboundDepositIntakeDoc extends Document {
24
+ provider: TreasuryProvider;
25
+ externalId: string;
26
+ source: InboundDepositIntakeSource;
27
+ externalEventId?: string;
28
+ status: InboundDepositIntakeStatus;
29
+ attempts: number;
30
+ /** The reason the provider's matcher returned, once it has run. */
31
+ outcome?: string;
32
+ /** Whether that run published a payment, as opposed to skipping or rejecting. */
33
+ processed?: boolean;
34
+ lastError?: string;
35
+ receivedAt: Date;
36
+ startedAt?: Date;
37
+ finishedAt?: Date;
38
+ }
39
+ interface InboundDepositIntakeModel extends Model<InboundDepositIntakeDoc> {
40
+ build(attrs: InboundDepositIntakeAttrs): HydratedDocument<InboundDepositIntakeDoc>;
41
+ }
42
+ declare const buildInboundDepositIntake: (mongoose: Mongoose) => InboundDepositIntakeModel;
43
+ export { buildInboundDepositIntake, InboundDepositIntakeDoc, InboundDepositIntakeAttrs, };