@anomira/node-sdk 0.1.5 → 0.1.7
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/README.md +28 -6
- package/dist/cli.cjs +54 -25
- package/dist/index.cjs +67 -33
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +45 -22
- package/dist/index.d.ts +45 -22
- package/dist/index.js +65 -32
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -11,9 +11,9 @@ npm install @anomira/node-sdk
|
|
|
11
11
|
## Quick Start
|
|
12
12
|
|
|
13
13
|
```js
|
|
14
|
-
import {
|
|
14
|
+
import { Anomira } from "@anomira/node-sdk";
|
|
15
15
|
|
|
16
|
-
const sentinel = new
|
|
16
|
+
const sentinel = new Anomira({
|
|
17
17
|
apiKey: process.env.SENTINEL_API_KEY,
|
|
18
18
|
appId: process.env.SENTINEL_APP_ID,
|
|
19
19
|
ingestUrl: process.env.SENTINEL_INGEST_URL,
|
|
@@ -25,11 +25,11 @@ const sentinel = new SentinelAPI({
|
|
|
25
25
|
|
|
26
26
|
```js
|
|
27
27
|
import express from "express";
|
|
28
|
-
import {
|
|
28
|
+
import { Anomira } from "@anomira/node-sdk";
|
|
29
29
|
|
|
30
30
|
const app = express();
|
|
31
31
|
|
|
32
|
-
const sentinel = new
|
|
32
|
+
const sentinel = new Anomira({
|
|
33
33
|
apiKey: process.env.SENTINEL_API_KEY,
|
|
34
34
|
appId: process.env.SENTINEL_APP_ID,
|
|
35
35
|
ingestUrl: process.env.SENTINEL_INGEST_URL,
|
|
@@ -56,11 +56,11 @@ app.listen(3000);
|
|
|
56
56
|
|
|
57
57
|
```js
|
|
58
58
|
import Fastify from "fastify";
|
|
59
|
-
import {
|
|
59
|
+
import { Anomira } from "@anomira/node-sdk";
|
|
60
60
|
|
|
61
61
|
const app = Fastify();
|
|
62
62
|
|
|
63
|
-
const sentinel = new
|
|
63
|
+
const sentinel = new Anomira({
|
|
64
64
|
apiKey: process.env.SENTINEL_API_KEY,
|
|
65
65
|
appId: process.env.SENTINEL_APP_ID,
|
|
66
66
|
ingestUrl: process.env.SENTINEL_INGEST_URL,
|
|
@@ -116,6 +116,28 @@ if (match?.rule.action === "block") {
|
|
|
116
116
|
}
|
|
117
117
|
```
|
|
118
118
|
|
|
119
|
+
## Shadow Endpoint Detection
|
|
120
|
+
|
|
121
|
+
Register your known API routes on startup so Anomira can flag any undeclared endpoint that appears in live traffic. Endpoints that receive requests but were never declared show up in the **API Surface Map** as shadow endpoints.
|
|
122
|
+
|
|
123
|
+
```js
|
|
124
|
+
// Call once after your routes are registered
|
|
125
|
+
await sentinel.declareEndpoints([
|
|
126
|
+
{ method: "POST", path: "/api/auth/login", auth: false },
|
|
127
|
+
{ method: "POST", path: "/api/auth/register", auth: false },
|
|
128
|
+
{ method: "GET", path: "/api/users/:id", auth: true },
|
|
129
|
+
{ method: "GET", path: "/api/orders", auth: true },
|
|
130
|
+
{ method: "POST", path: "/api/orders", auth: true },
|
|
131
|
+
{ method: "GET", path: "/api/health", auth: false },
|
|
132
|
+
]);
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Express-style `:param` segments are normalized automatically — `/users/:id` and `/users/:userId` both map to `/users/{id}` to match discovered traffic.
|
|
136
|
+
|
|
137
|
+
Use `auth: false` for public endpoints. Authenticated endpoints default to `auth: true`.
|
|
138
|
+
|
|
139
|
+
Shadow detection from declared endpoints only activates once your org has registered at least one endpoint, so it won't produce noise on fresh deployments before you call this method.
|
|
140
|
+
|
|
119
141
|
## Secret Scanner CLI
|
|
120
142
|
|
|
121
143
|
Scan your codebase for hardcoded secrets, API keys, BVN/NIN numbers, and PII before they reach production.
|
package/dist/cli.cjs
CHANGED
|
@@ -5606,17 +5606,15 @@ var creator = {
|
|
|
5606
5606
|
|
|
5607
5607
|
// src/sensitive.ts
|
|
5608
5608
|
var PATTERNS = [
|
|
5609
|
-
// ── Cryptographic keys
|
|
5609
|
+
// ── Cryptographic private keys ────────────────────────────────────────────
|
|
5610
|
+
// NOTE: -----BEGIN CERTIFICATE----- is intentionally excluded — public
|
|
5611
|
+
// certificates are designed to be public and committing them is correct.
|
|
5612
|
+
// Only PRIVATE keys are dangerous.
|
|
5610
5613
|
{
|
|
5611
5614
|
type: "private_key",
|
|
5612
5615
|
label: "Private Key",
|
|
5613
5616
|
regex: /-----BEGIN (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----/
|
|
5614
5617
|
},
|
|
5615
|
-
{
|
|
5616
|
-
type: "certificate",
|
|
5617
|
-
label: "Certificate / Public Key",
|
|
5618
|
-
regex: /-----BEGIN CERTIFICATE-----/
|
|
5619
|
-
},
|
|
5620
5618
|
// ── Cloud provider credentials ────────────────────────────────────────────
|
|
5621
5619
|
{
|
|
5622
5620
|
// AWS access key ID — highly specific, almost no false positives
|
|
@@ -5731,26 +5729,30 @@ var PATTERNS = [
|
|
|
5731
5729
|
regex: /eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/
|
|
5732
5730
|
},
|
|
5733
5731
|
{
|
|
5734
|
-
// Generic API key / token
|
|
5732
|
+
// Generic API key / token — only flag QUOTED literals, not variable/env references.
|
|
5733
|
+
// Skips: api_key = process.env.KEY, token = myVar
|
|
5734
|
+
// Matches: api_key = "sk-abc123...", bearer: "eyJhb..."
|
|
5735
5735
|
type: "api_key",
|
|
5736
5736
|
label: "API Key / Token",
|
|
5737
|
-
regex: /\b(?:api[_-]?key|access[_-]?token|auth[_-]?token|bearer|client[_-]?secret)\s*[:=]\s*["']
|
|
5737
|
+
regex: /\b(?:api[_-]?key|access[_-]?token|auth[_-]?token|bearer|client[_-]?secret)\s*[:=]\s*["'][A-Za-z0-9_.\/+\-]{20,}["']/i
|
|
5738
5738
|
},
|
|
5739
5739
|
// ── Password fields ───────────────────────────────────────────────────────
|
|
5740
5740
|
{
|
|
5741
|
-
//
|
|
5742
|
-
//
|
|
5741
|
+
// Only flag QUOTED string literals after a password/secret keyword.
|
|
5742
|
+
// Skips: process.env.*, variable references, undefined/null, template literals.
|
|
5743
|
+
// Matches: password: "hunter2", secret: 'abc123def', pass="hardcoded!"
|
|
5743
5744
|
type: "password",
|
|
5744
|
-
label: "Password
|
|
5745
|
-
regex: /\b(?:password|passwd|pwd|pass|secret|credentials?)\s*[:=]\s*["']
|
|
5745
|
+
label: "Hardcoded Password",
|
|
5746
|
+
regex: /\b(?:password|passwd|pwd|pass|secret|credentials?)\s*[:=]\s*["'][^"'$\s]{6,}["']/i
|
|
5746
5747
|
},
|
|
5747
5748
|
// ── Nigeria-specific PII ──────────────────────────────────────────────────
|
|
5748
5749
|
{
|
|
5749
|
-
// BVN / NIN: 11 digits, first digit 1-9 (not a phone starting with 0)
|
|
5750
|
-
//
|
|
5750
|
+
// BVN / NIN: 11 digits, first digit 1-9 (not a phone number starting with 0)
|
|
5751
|
+
// Exclude: inside URLs (preceded by / : @ - %), hex strings (followed by a-f),
|
|
5752
|
+
// and UUIDs/hashes (surrounded by alphanumeric chars)
|
|
5751
5753
|
type: "bvn",
|
|
5752
5754
|
label: "BVN / NIN",
|
|
5753
|
-
regex: /(
|
|
5755
|
+
regex: /(?<![/\-:@%=a-fA-F\w])[1-9]\d{10}(?![a-fA-F\d])/
|
|
5754
5756
|
},
|
|
5755
5757
|
{
|
|
5756
5758
|
// Nigerian phone numbers: 080x, 081x, 070x, 090x, 091x — or with +234 prefix
|
|
@@ -5843,8 +5845,28 @@ var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
|
5843
5845
|
".turbo",
|
|
5844
5846
|
".cache",
|
|
5845
5847
|
"tmp",
|
|
5846
|
-
"temp"
|
|
5848
|
+
"temp",
|
|
5849
|
+
// Test directories — skipped by default, use --include-tests to scan them
|
|
5850
|
+
"tests",
|
|
5851
|
+
"test",
|
|
5852
|
+
"__tests__",
|
|
5853
|
+
"spec",
|
|
5854
|
+
"__spec__",
|
|
5855
|
+
"fixtures",
|
|
5856
|
+
"__fixtures__",
|
|
5857
|
+
"mocks",
|
|
5858
|
+
"__mocks__"
|
|
5847
5859
|
]);
|
|
5860
|
+
var TEST_FILE_PATTERNS = [
|
|
5861
|
+
".test.js",
|
|
5862
|
+
".test.ts",
|
|
5863
|
+
".test.jsx",
|
|
5864
|
+
".test.tsx",
|
|
5865
|
+
".spec.js",
|
|
5866
|
+
".spec.ts",
|
|
5867
|
+
".spec.jsx",
|
|
5868
|
+
".spec.tsx"
|
|
5869
|
+
];
|
|
5848
5870
|
function shannonEntropy(str) {
|
|
5849
5871
|
const freq = {};
|
|
5850
5872
|
for (const ch of str) freq[ch] = (freq[ch] ?? 0) + 1;
|
|
@@ -5876,7 +5898,12 @@ var SECRETLINT_CONFIG = {
|
|
|
5876
5898
|
}
|
|
5877
5899
|
]
|
|
5878
5900
|
};
|
|
5879
|
-
function
|
|
5901
|
+
function isTestFile(filePath) {
|
|
5902
|
+
const base = path2__default.default.basename(filePath);
|
|
5903
|
+
return TEST_FILE_PATTERNS.some((p) => base.endsWith(p));
|
|
5904
|
+
}
|
|
5905
|
+
function shouldScan(filePath, includeTests) {
|
|
5906
|
+
if (!includeTests && isTestFile(filePath)) return false;
|
|
5880
5907
|
const ext = path2__default.default.extname(filePath);
|
|
5881
5908
|
const base = path2__default.default.basename(filePath);
|
|
5882
5909
|
if (base.startsWith(".env")) return true;
|
|
@@ -5957,14 +5984,14 @@ async function scanFile(filePath, strict) {
|
|
|
5957
5984
|
}
|
|
5958
5985
|
return violations;
|
|
5959
5986
|
}
|
|
5960
|
-
function* walkDir(dir) {
|
|
5987
|
+
function* walkDir(dir, includeTests) {
|
|
5961
5988
|
const entries = fs__default.default.readdirSync(dir, { withFileTypes: true });
|
|
5962
5989
|
for (const entry of entries) {
|
|
5963
5990
|
if (SKIP_DIRS.has(entry.name)) continue;
|
|
5964
5991
|
const full = path2__default.default.join(dir, entry.name);
|
|
5965
5992
|
if (entry.isDirectory()) {
|
|
5966
|
-
yield* walkDir(full);
|
|
5967
|
-
} else if (entry.isFile() && shouldScan(full)) {
|
|
5993
|
+
yield* walkDir(full, includeTests);
|
|
5994
|
+
} else if (entry.isFile() && shouldScan(full, includeTests)) {
|
|
5968
5995
|
yield full;
|
|
5969
5996
|
}
|
|
5970
5997
|
}
|
|
@@ -5976,6 +6003,7 @@ async function main() {
|
|
|
5976
6003
|
const quiet = args.includes("--quiet") || args.includes("-q");
|
|
5977
6004
|
const jsonOut = args.includes("--json");
|
|
5978
6005
|
const strict = args.includes("--strict");
|
|
6006
|
+
const includeTests = args.includes("--include-tests");
|
|
5979
6007
|
if (args.includes("--help") || args.includes("-h") || args[0] === "help") {
|
|
5980
6008
|
console.log(`
|
|
5981
6009
|
${c.bold}Anomira Secret Scanner${c.reset}
|
|
@@ -5989,10 +6017,11 @@ Detection layers:
|
|
|
5989
6017
|
Layer 3 Entropy \u2014 High-entropy string detection (--strict only)
|
|
5990
6018
|
|
|
5991
6019
|
Options:
|
|
5992
|
-
--strict
|
|
5993
|
-
--
|
|
5994
|
-
--
|
|
5995
|
-
--
|
|
6020
|
+
--strict Enable entropy analysis (catches unknown secrets, more noise)
|
|
6021
|
+
--include-tests Also scan test files (*.test.*, *.spec.*, tests/ dirs \u2014 skipped by default)
|
|
6022
|
+
--quiet, -q Only print violations (suppress summary header)
|
|
6023
|
+
--json Machine-readable JSON output (for CI pipelines)
|
|
6024
|
+
--help, -h Show this help
|
|
5996
6025
|
|
|
5997
6026
|
Examples:
|
|
5998
6027
|
npx @anomira/node-sdk scan ./src
|
|
@@ -6016,7 +6045,7 @@ ${c.bold}${c.cyan}Anomira Secret Scanner${c.reset}`);
|
|
|
6016
6045
|
console.log(`${c.grey}Layers: secretlint + custom patterns${strict ? " + entropy" : ""}${c.reset}
|
|
6017
6046
|
`);
|
|
6018
6047
|
}
|
|
6019
|
-
const files = fs__default.default.statSync(target).isDirectory() ? [...walkDir(target)] : [target];
|
|
6048
|
+
const files = fs__default.default.statSync(target).isDirectory() ? [...walkDir(target, includeTests)] : [target];
|
|
6020
6049
|
const allViolations = [];
|
|
6021
6050
|
let fileCount = 0;
|
|
6022
6051
|
for (const file of files) {
|
package/dist/index.cjs
CHANGED
|
@@ -109,10 +109,10 @@ var EventBuffer = class {
|
|
|
109
109
|
}
|
|
110
110
|
}
|
|
111
111
|
log(...args) {
|
|
112
|
-
if (this.opts.debug) console.log("[
|
|
112
|
+
if (this.opts.debug) console.log("[Anomira]", ...args);
|
|
113
113
|
}
|
|
114
114
|
warn(...args) {
|
|
115
|
-
console.warn("[
|
|
115
|
+
console.warn("[Anomira]", ...args);
|
|
116
116
|
}
|
|
117
117
|
};
|
|
118
118
|
function sleep(ms) {
|
|
@@ -199,17 +199,15 @@ async function checkGeoVelocity(userId, ip, tsMs, lookupUrl) {
|
|
|
199
199
|
|
|
200
200
|
// src/sensitive.ts
|
|
201
201
|
var PATTERNS = [
|
|
202
|
-
// ── Cryptographic keys
|
|
202
|
+
// ── Cryptographic private keys ────────────────────────────────────────────
|
|
203
|
+
// NOTE: -----BEGIN CERTIFICATE----- is intentionally excluded — public
|
|
204
|
+
// certificates are designed to be public and committing them is correct.
|
|
205
|
+
// Only PRIVATE keys are dangerous.
|
|
203
206
|
{
|
|
204
207
|
type: "private_key",
|
|
205
208
|
label: "Private Key",
|
|
206
209
|
regex: /-----BEGIN (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----/
|
|
207
210
|
},
|
|
208
|
-
{
|
|
209
|
-
type: "certificate",
|
|
210
|
-
label: "Certificate / Public Key",
|
|
211
|
-
regex: /-----BEGIN CERTIFICATE-----/
|
|
212
|
-
},
|
|
213
211
|
// ── Cloud provider credentials ────────────────────────────────────────────
|
|
214
212
|
{
|
|
215
213
|
// AWS access key ID — highly specific, almost no false positives
|
|
@@ -324,26 +322,30 @@ var PATTERNS = [
|
|
|
324
322
|
regex: /eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/
|
|
325
323
|
},
|
|
326
324
|
{
|
|
327
|
-
// Generic API key / token
|
|
325
|
+
// Generic API key / token — only flag QUOTED literals, not variable/env references.
|
|
326
|
+
// Skips: api_key = process.env.KEY, token = myVar
|
|
327
|
+
// Matches: api_key = "sk-abc123...", bearer: "eyJhb..."
|
|
328
328
|
type: "api_key",
|
|
329
329
|
label: "API Key / Token",
|
|
330
|
-
regex: /\b(?:api[_-]?key|access[_-]?token|auth[_-]?token|bearer|client[_-]?secret)\s*[:=]\s*["']
|
|
330
|
+
regex: /\b(?:api[_-]?key|access[_-]?token|auth[_-]?token|bearer|client[_-]?secret)\s*[:=]\s*["'][A-Za-z0-9_.\/+\-]{20,}["']/i
|
|
331
331
|
},
|
|
332
332
|
// ── Password fields ───────────────────────────────────────────────────────
|
|
333
333
|
{
|
|
334
|
-
//
|
|
335
|
-
//
|
|
334
|
+
// Only flag QUOTED string literals after a password/secret keyword.
|
|
335
|
+
// Skips: process.env.*, variable references, undefined/null, template literals.
|
|
336
|
+
// Matches: password: "hunter2", secret: 'abc123def', pass="hardcoded!"
|
|
336
337
|
type: "password",
|
|
337
|
-
label: "Password
|
|
338
|
-
regex: /\b(?:password|passwd|pwd|pass|secret|credentials?)\s*[:=]\s*["']
|
|
338
|
+
label: "Hardcoded Password",
|
|
339
|
+
regex: /\b(?:password|passwd|pwd|pass|secret|credentials?)\s*[:=]\s*["'][^"'$\s]{6,}["']/i
|
|
339
340
|
},
|
|
340
341
|
// ── Nigeria-specific PII ──────────────────────────────────────────────────
|
|
341
342
|
{
|
|
342
|
-
// BVN / NIN: 11 digits, first digit 1-9 (not a phone starting with 0)
|
|
343
|
-
//
|
|
343
|
+
// BVN / NIN: 11 digits, first digit 1-9 (not a phone number starting with 0)
|
|
344
|
+
// Exclude: inside URLs (preceded by / : @ - %), hex strings (followed by a-f),
|
|
345
|
+
// and UUIDs/hashes (surrounded by alphanumeric chars)
|
|
344
346
|
type: "bvn",
|
|
345
347
|
label: "BVN / NIN",
|
|
346
|
-
regex: /(
|
|
348
|
+
regex: /(?<![/\-:@%=a-fA-F\w])[1-9]\d{10}(?![a-fA-F\d])/
|
|
347
349
|
},
|
|
348
350
|
{
|
|
349
351
|
// Nigerian phone numbers: 080x, 081x, 070x, 090x, 091x — or with +234 prefix
|
|
@@ -410,7 +412,7 @@ var DEFAULT_INGEST_URL = "https://ingest.anomira.io/v1/events";
|
|
|
410
412
|
var DEFAULT_BATCH_SIZE = 100;
|
|
411
413
|
var DEFAULT_FLUSH_MS = 5e3;
|
|
412
414
|
var DEFAULT_MAX_RETRIES = 3;
|
|
413
|
-
var
|
|
415
|
+
var AnomiraClient = class {
|
|
414
416
|
constructor(config) {
|
|
415
417
|
this.logBuffer = [];
|
|
416
418
|
this.logFlushTimer = null;
|
|
@@ -509,7 +511,7 @@ var SentinelClient = class {
|
|
|
509
511
|
console[method] = (...args) => {
|
|
510
512
|
original(...args);
|
|
511
513
|
const first = args[0];
|
|
512
|
-
if (typeof first === "string" && first.startsWith("[
|
|
514
|
+
if (typeof first === "string" && first.startsWith("[Anomira]")) return;
|
|
513
515
|
const message = args.map((a) => typeof a === "string" ? a : JSON.stringify(a)).join(" ");
|
|
514
516
|
const ctx = requestContext.getStore();
|
|
515
517
|
this.log(level, message, {
|
|
@@ -533,7 +535,7 @@ var SentinelClient = class {
|
|
|
533
535
|
const data = await res.json();
|
|
534
536
|
this.blockedIpCache = new Set(data.ips ?? []);
|
|
535
537
|
if (this.config.debug && this.blockedIpCache.size > 0) {
|
|
536
|
-
this._origLog(`[
|
|
538
|
+
this._origLog(`[Anomira] blocklist refreshed \u2014 ${this.blockedIpCache.size} blocked IPs`);
|
|
537
539
|
}
|
|
538
540
|
} catch {
|
|
539
541
|
}
|
|
@@ -561,7 +563,7 @@ var SentinelClient = class {
|
|
|
561
563
|
})() : void 0
|
|
562
564
|
}));
|
|
563
565
|
if (this.config.debug && this.compiledRules.length > 0) {
|
|
564
|
-
this._origLog(`[
|
|
566
|
+
this._origLog(`[Anomira] firewall rules refreshed \u2014 ${this.compiledRules.length} active rules`);
|
|
565
567
|
}
|
|
566
568
|
} catch {
|
|
567
569
|
}
|
|
@@ -612,7 +614,7 @@ var SentinelClient = class {
|
|
|
612
614
|
signal: AbortSignal.timeout(8e3)
|
|
613
615
|
});
|
|
614
616
|
if (this.config.debug) {
|
|
615
|
-
this._origLog(`[
|
|
617
|
+
this._origLog(`[Anomira] [logs] \u2705 sent ${batch.length} log entries (${res.status})`);
|
|
616
618
|
}
|
|
617
619
|
} catch {
|
|
618
620
|
this.logBuffer.unshift(...batch);
|
|
@@ -631,24 +633,24 @@ var SentinelClient = class {
|
|
|
631
633
|
signal: AbortSignal.timeout(5e3)
|
|
632
634
|
});
|
|
633
635
|
if (res.status >= 300 && res.status < 400) {
|
|
634
|
-
this._origWarn(`[
|
|
636
|
+
this._origWarn(`[Anomira] \u274C Wrong ingest URL \u2014 got redirect to ${res.headers.get("location")}. Check SENTINEL_INGEST_URL.`);
|
|
635
637
|
return;
|
|
636
638
|
}
|
|
637
639
|
if (res.ok) {
|
|
638
|
-
this._origLog(`[
|
|
640
|
+
this._origLog(`[Anomira] \u2705 Connected (appId: ${this.config.appId.slice(0, 8)}\u2026)`);
|
|
639
641
|
return;
|
|
640
642
|
}
|
|
641
643
|
if (res.status === 401) {
|
|
642
|
-
this._origWarn("[
|
|
644
|
+
this._origWarn("[Anomira] \u274C Invalid API key \u2014 check your SENTINEL_API_KEY");
|
|
643
645
|
return;
|
|
644
646
|
}
|
|
645
647
|
if (res.status === 403) {
|
|
646
|
-
this._origWarn("[
|
|
648
|
+
this._origWarn("[Anomira] \u274C App not found or appId mismatch \u2014 check your SENTINEL_APP_ID");
|
|
647
649
|
return;
|
|
648
650
|
}
|
|
649
|
-
this._origWarn(`[
|
|
651
|
+
this._origWarn(`[Anomira] \u26A0\uFE0F Ingest returned HTTP ${res.status} \u2014 check your configuration`);
|
|
650
652
|
} catch {
|
|
651
|
-
this._origWarn("[
|
|
653
|
+
this._origWarn("[Anomira] \u26A0\uFE0F Could not reach ingest endpoint \u2014 check SENTINEL_INGEST_URL (current: " + this.config.ingestUrl + ")");
|
|
652
654
|
}
|
|
653
655
|
}
|
|
654
656
|
// ─── Public API ────────────────────────────────────────────────────────────
|
|
@@ -745,7 +747,7 @@ var SentinelClient = class {
|
|
|
745
747
|
});
|
|
746
748
|
}
|
|
747
749
|
/**
|
|
748
|
-
* Send a structured log entry to the
|
|
750
|
+
* Send a structured log entry to the Anomira Logs dashboard.
|
|
749
751
|
*
|
|
750
752
|
* ```ts
|
|
751
753
|
* sentinel.log("info", "User registered", { userId: user.id });
|
|
@@ -761,16 +763,47 @@ var SentinelClient = class {
|
|
|
761
763
|
rest["sensitiveLeaks"] = leaks.map((l) => l.type);
|
|
762
764
|
if (this.config.debug) {
|
|
763
765
|
this._origWarn(
|
|
764
|
-
`[
|
|
766
|
+
`[Anomira] \u26A0\uFE0F Sensitive data in log (${leaks.map((l) => l.label).join(", ")}): "${message.slice(0, 60)}\u2026"`
|
|
765
767
|
);
|
|
766
768
|
}
|
|
767
769
|
}
|
|
768
770
|
this.logBuffer.push({ level, service: service ?? this.config.service, message, meta: rest, ts: Date.now() });
|
|
769
771
|
if (this.config.debug && leaks.length === 0) {
|
|
770
|
-
this._origLog(`[
|
|
772
|
+
this._origLog(`[Anomira] log:${level} ${message}`);
|
|
771
773
|
}
|
|
772
774
|
if (this.logBuffer.length >= 50) void this.#flushLogs();
|
|
773
775
|
}
|
|
776
|
+
/**
|
|
777
|
+
* Declare your API's known endpoints so Anomira can flag undiscovered
|
|
778
|
+
* traffic as shadow endpoints.
|
|
779
|
+
*
|
|
780
|
+
* Call this once on startup after your routes are registered:
|
|
781
|
+
*
|
|
782
|
+
* ```ts
|
|
783
|
+
* await sentinel.declareEndpoints([
|
|
784
|
+
* { method: "GET", path: "/api/users/:id", auth: true },
|
|
785
|
+
* { method: "POST", path: "/api/orders", auth: true },
|
|
786
|
+
* { method: "GET", path: "/api/health", auth: false },
|
|
787
|
+
* ]);
|
|
788
|
+
* ```
|
|
789
|
+
*/
|
|
790
|
+
async declareEndpoints(endpoints) {
|
|
791
|
+
if (this.disabled || endpoints.length === 0) return;
|
|
792
|
+
const url = this.config.ingestUrl.replace(/\/v1\/events$/, "/v1/declare-endpoints");
|
|
793
|
+
try {
|
|
794
|
+
await fetch(url, {
|
|
795
|
+
method: "POST",
|
|
796
|
+
headers: {
|
|
797
|
+
Authorization: `Bearer ${this.config.apiKey}`,
|
|
798
|
+
"Content-Type": "application/json",
|
|
799
|
+
"User-Agent": `@anomira/node-sdk/0.1.0`
|
|
800
|
+
},
|
|
801
|
+
body: JSON.stringify({ appId: this.config.appId, endpoints }),
|
|
802
|
+
signal: AbortSignal.timeout(1e4)
|
|
803
|
+
});
|
|
804
|
+
} catch {
|
|
805
|
+
}
|
|
806
|
+
}
|
|
774
807
|
/**
|
|
775
808
|
* Flush all pending events immediately.
|
|
776
809
|
* Useful before a graceful shutdown outside of the process lifecycle hooks.
|
|
@@ -1111,10 +1144,11 @@ function createFastifyPlugin2(client) {
|
|
|
1111
1144
|
};
|
|
1112
1145
|
}
|
|
1113
1146
|
|
|
1147
|
+
exports.Anomira = AnomiraClient;
|
|
1114
1148
|
exports.EventName = EventName;
|
|
1115
|
-
exports.SentinelAPI =
|
|
1149
|
+
exports.SentinelAPI = AnomiraClient;
|
|
1116
1150
|
exports.createExpressMiddleware = createExpressMiddleware2;
|
|
1117
1151
|
exports.createFastifyPlugin = createFastifyPlugin2;
|
|
1118
|
-
exports.default =
|
|
1152
|
+
exports.default = AnomiraClient;
|
|
1119
1153
|
//# sourceMappingURL=index.cjs.map
|
|
1120
1154
|
//# sourceMappingURL=index.cjs.map
|