emilsoftware-utilities 2.0.0-dev.77 → 2.0.0-dev.78
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/dist/accessi/resource-server/token.js +2 -0
- package/dist/accessi/resource-server/token.js.map +1 -1
- package/dist/accessi/server/standalone.js +2 -1
- package/dist/accessi/server/standalone.js.map +1 -1
- package/dist/accessi-module/AccessiModule.d.ts +3 -0
- package/dist/accessi-module/AccessiModule.js.map +1 -1
- package/dist/accessi-module/Console/console.js +1 -0
- package/dist/accessi-module/Console/wiki.js +1 -0
- package/dist/accessi-module/Console/wiki.js.map +1 -1
- package/dist/accessi-module/Controllers/AccessiIntrospectionController.js +9 -0
- package/dist/accessi-module/Controllers/AccessiIntrospectionController.js.map +1 -1
- package/dist/accessi-module/Controllers/AuthController.js +67 -8
- package/dist/accessi-module/Controllers/AuthController.js.map +1 -1
- package/dist/accessi-module/Controllers/ConsoleEntryController.js +9 -0
- package/dist/accessi-module/Controllers/ConsoleEntryController.js.map +1 -1
- package/dist/accessi-module/Services/AuthService/AuthService.js +8 -2
- package/dist/accessi-module/Services/AuthService/AuthService.js.map +1 -1
- package/dist/accessi-module/index.d.ts +4 -0
- package/dist/accessi-module/index.js +15 -1
- package/dist/accessi-module/index.js.map +1 -1
- package/dist/accessi-module/observability/metrics.d.ts +79 -0
- package/dist/accessi-module/observability/metrics.js +216 -0
- package/dist/accessi-module/observability/metrics.js.map +1 -0
- package/dist/accessi-module/observability/redisMetrics.d.ts +12 -0
- package/dist/accessi-module/observability/redisMetrics.js +44 -0
- package/dist/accessi-module/observability/redisMetrics.js.map +1 -0
- package/dist/accessi-module/security/accessTokenRevocation.js +5 -3
- package/dist/accessi-module/security/accessTokenRevocation.js.map +1 -1
- package/dist/accessi-module/security/consoleEntry.js +5 -3
- package/dist/accessi-module/security/consoleEntry.js.map +1 -1
- package/dist/accessi-module/security/edgeRateLimit.js +3 -0
- package/dist/accessi-module/security/edgeRateLimit.js.map +1 -1
- package/dist/accessi-module/security/jwks.js +3 -0
- package/dist/accessi-module/security/jwks.js.map +1 -1
- package/dist/accessi-module/security/jwtTokenOptions.js +2 -0
- package/dist/accessi-module/security/jwtTokenOptions.js.map +1 -1
- package/dist/accessi-module/security/publicAuthRateLimit.js +3 -0
- package/dist/accessi-module/security/publicAuthRateLimit.js.map +1 -1
- package/dist/accessi-module/security/rateLimitStore.js +4 -2
- package/dist/accessi-module/security/rateLimitStore.js.map +1 -1
- package/dist/accessi-module/security/refreshTokenRotation.js +7 -5
- package/dist/accessi-module/security/refreshTokenRotation.js.map +1 -1
- package/dist/accessi-module/security/serviceTokenRateLimit.js +3 -0
- package/dist/accessi-module/security/serviceTokenRateLimit.js.map +1 -1
- package/dist/accessi-module/security/sessionRevocation.js +7 -3
- package/dist/accessi-module/security/sessionRevocation.js.map +1 -1
- package/dist/emilsoftware-utilities-2.0.0-dev.78.tgz +0 -0
- package/docs/metrics.md +153 -0
- package/docs/redis-load-testing.md +88 -0
- package/package.json +4 -3
- package/dist/emilsoftware-utilities-2.0.0-dev.77.tgz +0 -0
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createAccessiMetricsRegistry = createAccessiMetricsRegistry;
|
|
4
|
+
exports.getAccessiMetricsRegistry = getAccessiMetricsRegistry;
|
|
5
|
+
exports.getAccessiMetricsSink = getAccessiMetricsSink;
|
|
6
|
+
exports.recordAccessiMetric = recordAccessiMetric;
|
|
7
|
+
exports.recordAccessiDuration = recordAccessiDuration;
|
|
8
|
+
exports.renderAccessiMetrics = renderAccessiMetrics;
|
|
9
|
+
exports.resetAccessiMetrics = resetAccessiMetrics;
|
|
10
|
+
exports.accessiMetricName = accessiMetricName;
|
|
11
|
+
exports.countAccessiMetric = countAccessiMetric;
|
|
12
|
+
exports.observeAccessiMetric = observeAccessiMetric;
|
|
13
|
+
const HISTOGRAM_BUCKETS_MS = [5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000, 10000];
|
|
14
|
+
const METRIC_NAME = /^[a-zA-Z_:][a-zA-Z0-9_:]*$/;
|
|
15
|
+
const LABEL_NAME = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
16
|
+
function normalizeValue(value) {
|
|
17
|
+
if (value === undefined)
|
|
18
|
+
return undefined;
|
|
19
|
+
if (typeof value === 'boolean')
|
|
20
|
+
return value ? 'true' : 'false';
|
|
21
|
+
return String(value);
|
|
22
|
+
}
|
|
23
|
+
function normalizeLabels(labels) {
|
|
24
|
+
const normalized = {};
|
|
25
|
+
for (const [name, value] of Object.entries(labels !== null && labels !== void 0 ? labels : {})) {
|
|
26
|
+
const text = normalizeValue(value);
|
|
27
|
+
if (text !== undefined && LABEL_NAME.test(name))
|
|
28
|
+
normalized[name] = text;
|
|
29
|
+
}
|
|
30
|
+
return normalized;
|
|
31
|
+
}
|
|
32
|
+
function labelKey(labels) {
|
|
33
|
+
return Object.keys(labels)
|
|
34
|
+
.sort()
|
|
35
|
+
.map((name) => `${name}=${labels[name]}`)
|
|
36
|
+
.join(',');
|
|
37
|
+
}
|
|
38
|
+
function escapeLabel(value) {
|
|
39
|
+
return value.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n');
|
|
40
|
+
}
|
|
41
|
+
function renderLabels(labels, extra) {
|
|
42
|
+
const entries = Object.entries(labels).sort(([left], [right]) => left.localeCompare(right));
|
|
43
|
+
if (extra)
|
|
44
|
+
entries.push(extra);
|
|
45
|
+
if (entries.length === 0)
|
|
46
|
+
return '';
|
|
47
|
+
return `{${entries.map(([name, value]) => `${name}="${escapeLabel(value)}"`).join(',')}}`;
|
|
48
|
+
}
|
|
49
|
+
function metricName(name) {
|
|
50
|
+
return METRIC_NAME.test(name) ? name : name.replace(/[^a-zA-Z0-9_:]/g, '_');
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Registry in memoria con rendering Prometheus.
|
|
54
|
+
*
|
|
55
|
+
* Non usa dipendenze: la stessa struttura e usata dal servizio standalone e
|
|
56
|
+
* dai test. `reset()` serve per isolare i test.
|
|
57
|
+
*/
|
|
58
|
+
function createAccessiMetricsRegistry() {
|
|
59
|
+
const counters = new Map();
|
|
60
|
+
const gauges = new Map();
|
|
61
|
+
const histograms = new Map();
|
|
62
|
+
const entryKey = (name, labels) => `${metricName(name)}|${labelKey(labels)}`;
|
|
63
|
+
return {
|
|
64
|
+
increment(name, value = 1, labels) {
|
|
65
|
+
const normalizedName = metricName(name);
|
|
66
|
+
const normalizedLabels = normalizeLabels(labels);
|
|
67
|
+
const key = entryKey(normalizedName, normalizedLabels);
|
|
68
|
+
const current = counters.get(key);
|
|
69
|
+
if (current)
|
|
70
|
+
current.value += value;
|
|
71
|
+
else
|
|
72
|
+
counters.set(key, { name: normalizedName, labels: normalizedLabels, value });
|
|
73
|
+
},
|
|
74
|
+
gauge(name, value, labels) {
|
|
75
|
+
const normalizedName = metricName(name);
|
|
76
|
+
const normalizedLabels = normalizeLabels(labels);
|
|
77
|
+
gauges.set(entryKey(normalizedName, normalizedLabels), {
|
|
78
|
+
name: normalizedName,
|
|
79
|
+
labels: normalizedLabels,
|
|
80
|
+
value,
|
|
81
|
+
});
|
|
82
|
+
},
|
|
83
|
+
observe(name, valueMs, labels) {
|
|
84
|
+
if (!Number.isFinite(valueMs) || valueMs < 0)
|
|
85
|
+
return;
|
|
86
|
+
const normalizedName = metricName(name);
|
|
87
|
+
const normalizedLabels = normalizeLabels(labels);
|
|
88
|
+
const key = entryKey(normalizedName, normalizedLabels);
|
|
89
|
+
let entry = histograms.get(key);
|
|
90
|
+
if (!entry) {
|
|
91
|
+
entry = {
|
|
92
|
+
name: normalizedName,
|
|
93
|
+
labels: normalizedLabels,
|
|
94
|
+
counts: HISTOGRAM_BUCKETS_MS.map(() => 0),
|
|
95
|
+
sum: 0,
|
|
96
|
+
count: 0,
|
|
97
|
+
};
|
|
98
|
+
histograms.set(key, entry);
|
|
99
|
+
}
|
|
100
|
+
entry.sum += valueMs;
|
|
101
|
+
entry.count += 1;
|
|
102
|
+
const histogram = entry;
|
|
103
|
+
HISTOGRAM_BUCKETS_MS.forEach((bucket, index) => {
|
|
104
|
+
var _a;
|
|
105
|
+
if (valueMs <= bucket)
|
|
106
|
+
histogram.counts[index] = ((_a = histogram.counts[index]) !== null && _a !== void 0 ? _a : 0) + 1;
|
|
107
|
+
});
|
|
108
|
+
},
|
|
109
|
+
renderPrometheus() {
|
|
110
|
+
const lines = [];
|
|
111
|
+
const grouped = (entries) => {
|
|
112
|
+
const byName = new Map();
|
|
113
|
+
for (const entry of entries) {
|
|
114
|
+
const bucket = byName.get(entry.name);
|
|
115
|
+
if (bucket)
|
|
116
|
+
bucket.push(entry);
|
|
117
|
+
else
|
|
118
|
+
byName.set(entry.name, [entry]);
|
|
119
|
+
}
|
|
120
|
+
return [...byName.entries()].sort(([left], [right]) => left.localeCompare(right));
|
|
121
|
+
};
|
|
122
|
+
const sorted = (entries) => [...entries].sort((left, right) => labelKey(left.labels).localeCompare(labelKey(right.labels)));
|
|
123
|
+
for (const [name, entries] of grouped([...counters.values()])) {
|
|
124
|
+
lines.push(`# HELP ${name} Accessi metric.`);
|
|
125
|
+
lines.push(`# TYPE ${name} counter`);
|
|
126
|
+
for (const entry of sorted(entries))
|
|
127
|
+
lines.push(`${name}${renderLabels(entry.labels)} ${entry.value}`);
|
|
128
|
+
}
|
|
129
|
+
for (const [name, entries] of grouped([...gauges.values()])) {
|
|
130
|
+
lines.push(`# HELP ${name} Accessi metric.`);
|
|
131
|
+
lines.push(`# TYPE ${name} gauge`);
|
|
132
|
+
for (const entry of sorted(entries))
|
|
133
|
+
lines.push(`${name}${renderLabels(entry.labels)} ${entry.value}`);
|
|
134
|
+
}
|
|
135
|
+
for (const [name, entries] of grouped([...histograms.values()])) {
|
|
136
|
+
lines.push(`# HELP ${name} Accessi metric.`);
|
|
137
|
+
lines.push(`# TYPE ${name} histogram`);
|
|
138
|
+
for (const entry of sorted(entries)) {
|
|
139
|
+
HISTOGRAM_BUCKETS_MS.forEach((bucket, index) => {
|
|
140
|
+
lines.push(`${name}_bucket${renderLabels(entry.labels, ['le', String(bucket)])} ${entry.counts[index]}`);
|
|
141
|
+
});
|
|
142
|
+
lines.push(`${name}_bucket${renderLabels(entry.labels, ['le', '+Inf'])} ${entry.count}`);
|
|
143
|
+
lines.push(`${name}_sum${renderLabels(entry.labels)} ${entry.sum}`);
|
|
144
|
+
lines.push(`${name}_count${renderLabels(entry.labels)} ${entry.count}`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return lines.length > 0 ? `${lines.join('\n')}\n` : '';
|
|
148
|
+
},
|
|
149
|
+
snapshot() {
|
|
150
|
+
return {
|
|
151
|
+
counters: [...counters.values()].map((entry) => (Object.assign(Object.assign({}, entry), { labels: Object.assign({}, entry.labels) }))),
|
|
152
|
+
gauges: [...gauges.values()].map((entry) => (Object.assign(Object.assign({}, entry), { labels: Object.assign({}, entry.labels) }))),
|
|
153
|
+
histograms: [...histograms.values()].map((entry) => ({
|
|
154
|
+
name: entry.name,
|
|
155
|
+
labels: Object.assign({}, entry.labels),
|
|
156
|
+
count: entry.count,
|
|
157
|
+
sum: entry.sum,
|
|
158
|
+
buckets: HISTOGRAM_BUCKETS_MS.map((le, index) => { var _a; return ({ le, count: (_a = entry.counts[index]) !== null && _a !== void 0 ? _a : 0 }); }),
|
|
159
|
+
})),
|
|
160
|
+
};
|
|
161
|
+
},
|
|
162
|
+
reset() {
|
|
163
|
+
counters.clear();
|
|
164
|
+
gauges.clear();
|
|
165
|
+
histograms.clear();
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
const defaultRegistry = createAccessiMetricsRegistry();
|
|
170
|
+
/** Registry globale usato da Accessi e dal servizio standalone. */
|
|
171
|
+
function getAccessiMetricsRegistry() {
|
|
172
|
+
return defaultRegistry;
|
|
173
|
+
}
|
|
174
|
+
/** Sink configurato per l'istanza, oppure registry globale. */
|
|
175
|
+
function getAccessiMetricsSink(options) {
|
|
176
|
+
var _a, _b, _c;
|
|
177
|
+
if (((_a = options === null || options === void 0 ? void 0 : options.metrics) === null || _a === void 0 ? void 0 : _a.enabled) === false)
|
|
178
|
+
return undefined;
|
|
179
|
+
return (_c = (_b = options === null || options === void 0 ? void 0 : options.metrics) === null || _b === void 0 ? void 0 : _b.sink) !== null && _c !== void 0 ? _c : defaultRegistry;
|
|
180
|
+
}
|
|
181
|
+
/** Registra una metrica sul registry globale (usato da store e moduli senza options). */
|
|
182
|
+
function recordAccessiMetric(name, value = 1, labels) {
|
|
183
|
+
defaultRegistry.increment(name, value, labels);
|
|
184
|
+
}
|
|
185
|
+
/** Registra una durata in millisecondi sul registry globale. */
|
|
186
|
+
function recordAccessiDuration(name, valueMs, labels) {
|
|
187
|
+
defaultRegistry.observe(name, valueMs, labels);
|
|
188
|
+
}
|
|
189
|
+
/** Render Prometheus del registry globale. */
|
|
190
|
+
function renderAccessiMetrics() {
|
|
191
|
+
return defaultRegistry.renderPrometheus();
|
|
192
|
+
}
|
|
193
|
+
/** Azzera il registry globale (test e reset espliciti). */
|
|
194
|
+
function resetAccessiMetrics() {
|
|
195
|
+
defaultRegistry.reset();
|
|
196
|
+
}
|
|
197
|
+
/** Prefisso configurato, con normalizzazione per evitare nomi non validi. */
|
|
198
|
+
function accessiMetricName(options, name) {
|
|
199
|
+
var _a, _b;
|
|
200
|
+
const prefix = (_b = (_a = options === null || options === void 0 ? void 0 : options.metrics) === null || _a === void 0 ? void 0 : _a.prefix) !== null && _b !== void 0 ? _b : 'accessi';
|
|
201
|
+
if (!prefix.trim())
|
|
202
|
+
return name;
|
|
203
|
+
return `${prefix.trim().replace(/[^a-zA-Z0-9_:]/g, '_')}_${name}`;
|
|
204
|
+
}
|
|
205
|
+
/** Incrementa un contatore di dominio sull'istanza (o sul registry globale). */
|
|
206
|
+
function countAccessiMetric(options, name, labels, value = 1) {
|
|
207
|
+
var _a;
|
|
208
|
+
(_a = getAccessiMetricsSink(options)) === null || _a === void 0 ? void 0 : _a.increment(accessiMetricName(options, name), value, labels);
|
|
209
|
+
}
|
|
210
|
+
/** Registra una durata di dominio in millisecondi. */
|
|
211
|
+
function observeAccessiMetric(options, name, valueMs, labels) {
|
|
212
|
+
var _a;
|
|
213
|
+
const sink = getAccessiMetricsSink(options);
|
|
214
|
+
(_a = sink === null || sink === void 0 ? void 0 : sink.observe) === null || _a === void 0 ? void 0 : _a.call(sink, accessiMetricName(options, name), valueMs, labels);
|
|
215
|
+
}
|
|
216
|
+
//# sourceMappingURL=metrics.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"metrics.js","sourceRoot":"","sources":["../../../src/accessi-module/observability/metrics.ts"],"names":[],"mappings":"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","sourcesContent":["import type { AccessiOptions } from '../AccessiModule';\n\n/** Valore ammesso per un label Prometheus. */\nexport type AccessiMetricLabelValue = string | number | boolean | undefined;\nexport type AccessiMetricLabels = Record<string, AccessiMetricLabelValue>;\n\n/**\n * Destinazione delle metriche Accessi.\n *\n * L'implementazione predefinita e il registry in memoria renderizzato in\n * formato Prometheus. Un host puo passare un sink custom (OpenTelemetry,\n * StatsD, ...) tramite `metrics.sink`.\n */\nexport interface AccessiMetricsSink {\n increment(name: string, value?: number, labels?: AccessiMetricLabels): void;\n gauge?(name: string, value: number, labels?: AccessiMetricLabels): void;\n observe?(name: string, valueMs: number, labels?: AccessiMetricLabels): void;\n}\n\n/** Opzioni di osservabilita di Accessi. */\nexport interface AccessiMetricsOptions {\n /** `false` disabilita le metriche di dominio. Default: abilitate. */\n enabled?: boolean;\n /** Sink custom. Default: registry in memoria. */\n sink?: AccessiMetricsSink;\n /** Prefisso dei nomi. Default `accessi`. */\n prefix?: string;\n}\n\nexport interface AccessiMetricsRegistry extends AccessiMetricsSink {\n observe(name: string, valueMs: number, labels?: AccessiMetricLabels): void;\n gauge(name: string, value: number, labels?: AccessiMetricLabels): void;\n renderPrometheus(): string;\n snapshot(): AccessiMetricsSnapshot;\n reset(): void;\n}\n\nexport interface AccessiMetricsSnapshot {\n counters: Array<{ name: string; labels: Record<string, string>; value: number }>;\n gauges: Array<{ name: string; labels: Record<string, string>; value: number }>;\n histograms: Array<{\n name: string;\n labels: Record<string, string>;\n count: number;\n sum: number;\n buckets: Array<{ le: number; count: number }>;\n }>;\n}\n\nconst HISTOGRAM_BUCKETS_MS = [5, 10, 25, 50, 100, 250, 500, 1_000, 2_500, 5_000, 10_000];\nconst METRIC_NAME = /^[a-zA-Z_:][a-zA-Z0-9_:]*$/;\nconst LABEL_NAME = /^[a-zA-Z_][a-zA-Z0-9_]*$/;\n\nfunction normalizeValue(value: AccessiMetricLabelValue): string | undefined {\n if (value === undefined) return undefined;\n if (typeof value === 'boolean') return value ? 'true' : 'false';\n return String(value);\n}\n\nfunction normalizeLabels(labels: AccessiMetricLabels | undefined): Record<string, string> {\n const normalized: Record<string, string> = {};\n for (const [name, value] of Object.entries(labels ?? {})) {\n const text = normalizeValue(value);\n if (text !== undefined && LABEL_NAME.test(name)) normalized[name] = text;\n }\n return normalized;\n}\n\nfunction labelKey(labels: Record<string, string>): string {\n return Object.keys(labels)\n .sort()\n .map((name) => `${name}=${labels[name]}`)\n .join(',');\n}\n\nfunction escapeLabel(value: string): string {\n return value.replace(/\\\\/g, '\\\\\\\\').replace(/\"/g, '\\\\\"').replace(/\\n/g, '\\\\n');\n}\n\nfunction renderLabels(labels: Record<string, string>, extra?: [string, string]): string {\n const entries = Object.entries(labels).sort(([left], [right]) => left.localeCompare(right));\n if (extra) entries.push(extra);\n if (entries.length === 0) return '';\n return `{${entries.map(([name, value]) => `${name}=\"${escapeLabel(value)}\"`).join(',')}}`;\n}\n\nfunction metricName(name: string): string {\n return METRIC_NAME.test(name) ? name : name.replace(/[^a-zA-Z0-9_:]/g, '_');\n}\n\n/**\n * Registry in memoria con rendering Prometheus.\n *\n * Non usa dipendenze: la stessa struttura e usata dal servizio standalone e\n * dai test. `reset()` serve per isolare i test.\n */\nexport function createAccessiMetricsRegistry(): AccessiMetricsRegistry {\n const counters = new Map<string, { name: string; labels: Record<string, string>; value: number }>();\n const gauges = new Map<string, { name: string; labels: Record<string, string>; value: number }>();\n const histograms = new Map<string, {\n name: string;\n labels: Record<string, string>;\n counts: number[];\n sum: number;\n count: number;\n }>();\n\n const entryKey = (name: string, labels: Record<string, string>) => `${metricName(name)}|${labelKey(labels)}`;\n\n return {\n increment(name, value = 1, labels) {\n const normalizedName = metricName(name);\n const normalizedLabels = normalizeLabels(labels);\n const key = entryKey(normalizedName, normalizedLabels);\n const current = counters.get(key);\n if (current) current.value += value;\n else counters.set(key, { name: normalizedName, labels: normalizedLabels, value });\n },\n gauge(name, value, labels) {\n const normalizedName = metricName(name);\n const normalizedLabels = normalizeLabels(labels);\n gauges.set(entryKey(normalizedName, normalizedLabels), {\n name: normalizedName,\n labels: normalizedLabels,\n value,\n });\n },\n observe(name, valueMs, labels) {\n if (!Number.isFinite(valueMs) || valueMs < 0) return;\n const normalizedName = metricName(name);\n const normalizedLabels = normalizeLabels(labels);\n const key = entryKey(normalizedName, normalizedLabels);\n let entry = histograms.get(key);\n if (!entry) {\n entry = {\n name: normalizedName,\n labels: normalizedLabels,\n counts: HISTOGRAM_BUCKETS_MS.map(() => 0),\n sum: 0,\n count: 0,\n };\n histograms.set(key, entry);\n }\n entry.sum += valueMs;\n entry.count += 1;\n const histogram = entry;\n HISTOGRAM_BUCKETS_MS.forEach((bucket, index) => {\n if (valueMs <= bucket) histogram.counts[index] = (histogram.counts[index] ?? 0) + 1;\n });\n },\n renderPrometheus() {\n const lines: string[] = [];\n const grouped = <T extends { name: string }>(entries: T[]): Array<[string, T[]]> => {\n const byName = new Map<string, T[]>();\n for (const entry of entries) {\n const bucket = byName.get(entry.name);\n if (bucket) bucket.push(entry);\n else byName.set(entry.name, [entry]);\n }\n return [...byName.entries()].sort(([left], [right]) => left.localeCompare(right));\n };\n const sorted = <T extends { name: string; labels: Record<string, string> }>(entries: T[]) =>\n [...entries].sort((left, right) => labelKey(left.labels).localeCompare(labelKey(right.labels)));\n\n for (const [name, entries] of grouped([...counters.values()])) {\n lines.push(`# HELP ${name} Accessi metric.`);\n lines.push(`# TYPE ${name} counter`);\n for (const entry of sorted(entries)) lines.push(`${name}${renderLabels(entry.labels)} ${entry.value}`);\n }\n\n for (const [name, entries] of grouped([...gauges.values()])) {\n lines.push(`# HELP ${name} Accessi metric.`);\n lines.push(`# TYPE ${name} gauge`);\n for (const entry of sorted(entries)) lines.push(`${name}${renderLabels(entry.labels)} ${entry.value}`);\n }\n\n for (const [name, entries] of grouped([...histograms.values()])) {\n lines.push(`# HELP ${name} Accessi metric.`);\n lines.push(`# TYPE ${name} histogram`);\n for (const entry of sorted(entries)) {\n HISTOGRAM_BUCKETS_MS.forEach((bucket, index) => {\n lines.push(`${name}_bucket${renderLabels(entry.labels, ['le', String(bucket)])} ${entry.counts[index]}`);\n });\n lines.push(`${name}_bucket${renderLabels(entry.labels, ['le', '+Inf'])} ${entry.count}`);\n lines.push(`${name}_sum${renderLabels(entry.labels)} ${entry.sum}`);\n lines.push(`${name}_count${renderLabels(entry.labels)} ${entry.count}`);\n }\n }\n\n return lines.length > 0 ? `${lines.join('\\n')}\\n` : '';\n },\n snapshot() {\n return {\n counters: [...counters.values()].map((entry) => ({ ...entry, labels: { ...entry.labels } })),\n gauges: [...gauges.values()].map((entry) => ({ ...entry, labels: { ...entry.labels } })),\n histograms: [...histograms.values()].map((entry) => ({\n name: entry.name,\n labels: { ...entry.labels },\n count: entry.count,\n sum: entry.sum,\n buckets: HISTOGRAM_BUCKETS_MS.map((le, index) => ({ le, count: entry.counts[index] ?? 0 })),\n })),\n };\n },\n reset() {\n counters.clear();\n gauges.clear();\n histograms.clear();\n },\n };\n}\n\nconst defaultRegistry = createAccessiMetricsRegistry();\n\n/** Registry globale usato da Accessi e dal servizio standalone. */\nexport function getAccessiMetricsRegistry(): AccessiMetricsRegistry {\n return defaultRegistry;\n}\n\n/** Sink configurato per l'istanza, oppure registry globale. */\nexport function getAccessiMetricsSink(options: AccessiOptions | undefined): AccessiMetricsSink | undefined {\n if (options?.metrics?.enabled === false) return undefined;\n return options?.metrics?.sink ?? defaultRegistry;\n}\n\n/** Registra una metrica sul registry globale (usato da store e moduli senza options). */\nexport function recordAccessiMetric(\n name: string,\n value = 1,\n labels?: AccessiMetricLabels,\n): void {\n defaultRegistry.increment(name, value, labels);\n}\n\n/** Registra una durata in millisecondi sul registry globale. */\nexport function recordAccessiDuration(\n name: string,\n valueMs: number,\n labels?: AccessiMetricLabels,\n): void {\n defaultRegistry.observe(name, valueMs, labels);\n}\n\n/** Render Prometheus del registry globale. */\nexport function renderAccessiMetrics(): string {\n return defaultRegistry.renderPrometheus();\n}\n\n/** Azzera il registry globale (test e reset espliciti). */\nexport function resetAccessiMetrics(): void {\n defaultRegistry.reset();\n}\n\n/** Prefisso configurato, con normalizzazione per evitare nomi non validi. */\nexport function accessiMetricName(options: AccessiOptions | undefined, name: string): string {\n const prefix = options?.metrics?.prefix ?? 'accessi';\n if (!prefix.trim()) return name;\n return `${prefix.trim().replace(/[^a-zA-Z0-9_:]/g, '_')}_${name}`;\n}\n\n/** Incrementa un contatore di dominio sull'istanza (o sul registry globale). */\nexport function countAccessiMetric(\n options: AccessiOptions | undefined,\n name: string,\n labels?: AccessiMetricLabels,\n value = 1,\n): void {\n getAccessiMetricsSink(options)?.increment(accessiMetricName(options, name), value, labels);\n}\n\n/** Registra una durata di dominio in millisecondi. */\nexport function observeAccessiMetric(\n options: AccessiOptions | undefined,\n name: string,\n valueMs: number,\n labels?: AccessiMetricLabels,\n): void {\n const sink = getAccessiMetricsSink(options);\n sink?.observe?.(accessiMetricName(options, name), valueMs, labels);\n}\n"]}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Client Redis minimo usato dagli store Accessi. */
|
|
2
|
+
export interface AccessiRedisEvalClient {
|
|
3
|
+
eval(script: string, numKeys: number, ...args: Array<string | number>): Promise<unknown>;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Avvolge un client Redis per misurare errori e latenza degli script Lua.
|
|
7
|
+
*
|
|
8
|
+
* Gli store accettano qualsiasi client con `eval` (ioredis, node-redis, client
|
|
9
|
+
* di test). Il wrapper e trasparente: altri metodi (`quit`, `command`, ...)
|
|
10
|
+
* restano disponibili e legati all'istanza originale.
|
|
11
|
+
*/
|
|
12
|
+
export declare function instrumentAccessiRedisClient<T extends AccessiRedisEvalClient>(store: string, client: T): T;
|
|
@@ -0,0 +1,44 @@
|
|
|
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
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.instrumentAccessiRedisClient = instrumentAccessiRedisClient;
|
|
13
|
+
const metrics_1 = require("./metrics");
|
|
14
|
+
/**
|
|
15
|
+
* Avvolge un client Redis per misurare errori e latenza degli script Lua.
|
|
16
|
+
*
|
|
17
|
+
* Gli store accettano qualsiasi client con `eval` (ioredis, node-redis, client
|
|
18
|
+
* di test). Il wrapper e trasparente: altri metodi (`quit`, `command`, ...)
|
|
19
|
+
* restano disponibili e legati all'istanza originale.
|
|
20
|
+
*/
|
|
21
|
+
function instrumentAccessiRedisClient(store, client) {
|
|
22
|
+
return new Proxy(client, {
|
|
23
|
+
get(target, property) {
|
|
24
|
+
if (property === 'eval') {
|
|
25
|
+
return (script, numKeys, ...args) => __awaiter(this, void 0, void 0, function* () {
|
|
26
|
+
const startedAt = Date.now();
|
|
27
|
+
try {
|
|
28
|
+
return yield target.eval(script, numKeys, ...args);
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
(0, metrics_1.recordAccessiMetric)('accessi_store_errors_total', 1, { store });
|
|
32
|
+
throw error;
|
|
33
|
+
}
|
|
34
|
+
finally {
|
|
35
|
+
(0, metrics_1.recordAccessiDuration)('accessi_store_operation_duration_ms', Date.now() - startedAt, { store });
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
const value = Reflect.get(target, property, target);
|
|
40
|
+
return typeof value === 'function' ? value.bind(target) : value;
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=redisMetrics.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redisMetrics.js","sourceRoot":"","sources":["../../../src/accessi-module/observability/redisMetrics.ts"],"names":[],"mappings":";;;;;;;;;;;AAcA,oEAuBC;AArCD,uCAAuE;AAOvE;;;;;;GAMG;AACH,SAAgB,4BAA4B,CAC1C,KAAa,EACb,MAAS;IAET,OAAO,IAAI,KAAK,CAAC,MAAM,EAAE;QACvB,GAAG,CAAC,MAAM,EAAE,QAAQ;YAClB,IAAI,QAAQ,KAAK,MAAM,EAAE,CAAC;gBACxB,OAAO,CAAO,MAAc,EAAE,OAAe,EAAE,GAAG,IAA4B,EAAoB,EAAE;oBAClG,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;oBAC7B,IAAI,CAAC;wBACH,OAAO,MAAM,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC,CAAC;oBACrD,CAAC;oBAAC,OAAO,KAAK,EAAE,CAAC;wBACf,IAAA,6BAAmB,EAAC,4BAA4B,EAAE,CAAC,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC;wBAChE,MAAM,KAAK,CAAC;oBACd,CAAC;4BAAS,CAAC;wBACT,IAAA,+BAAqB,EAAC,qCAAqC,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,SAAS,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC;oBAClG,CAAC;gBACH,CAAC,CAAA,CAAC;YACJ,CAAC;YACD,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,QAAQ,EAAE,MAAM,CAAC,CAAC;YACpD,OAAO,OAAO,KAAK,KAAK,UAAU,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC;QAClE,CAAC;KACF,CAAM,CAAC;AACV,CAAC","sourcesContent":["import { recordAccessiDuration, recordAccessiMetric } from './metrics';\n\n/** Client Redis minimo usato dagli store Accessi. */\nexport interface AccessiRedisEvalClient {\n eval(script: string, numKeys: number, ...args: Array<string | number>): Promise<unknown>;\n}\n\n/**\n * Avvolge un client Redis per misurare errori e latenza degli script Lua.\n *\n * Gli store accettano qualsiasi client con `eval` (ioredis, node-redis, client\n * di test). Il wrapper e trasparente: altri metodi (`quit`, `command`, ...)\n * restano disponibili e legati all'istanza originale.\n */\nexport function instrumentAccessiRedisClient<T extends AccessiRedisEvalClient>(\n store: string,\n client: T,\n): T {\n return new Proxy(client, {\n get(target, property) {\n if (property === 'eval') {\n return async (script: string, numKeys: number, ...args: Array<string | number>): Promise<unknown> => {\n const startedAt = Date.now();\n try {\n return await target.eval(script, numKeys, ...args);\n } catch (error) {\n recordAccessiMetric('accessi_store_errors_total', 1, { store });\n throw error;\n } finally {\n recordAccessiDuration('accessi_store_operation_duration_ms', Date.now() - startedAt, { store });\n }\n };\n }\n const value = Reflect.get(target, property, target);\n return typeof value === 'function' ? value.bind(target) : value;\n },\n }) as T;\n}\n"]}
|
|
@@ -16,6 +16,7 @@ exports.isAccessiAccessTokenRevoked = isAccessiAccessTokenRevoked;
|
|
|
16
16
|
exports.revokeAccessiAccessToken = revokeAccessiAccessToken;
|
|
17
17
|
exports.revokeAccessiAccessTokenIfConfigured = revokeAccessiAccessTokenIfConfigured;
|
|
18
18
|
exports.clearDefaultAccessTokenRevocations = clearDefaultAccessTokenRevocations;
|
|
19
|
+
const redisMetrics_1 = require("../observability/redisMetrics");
|
|
19
20
|
const REVOKE_SCRIPT = "if tonumber(ARGV[1]) > 0 then return redis.call('SET', KEYS[1], '1', 'PX', ARGV[1]) else return redis.call('SET', KEYS[1], '1') end";
|
|
20
21
|
const IS_REVOKED_SCRIPT = "return redis.call('EXISTS', KEYS[1])";
|
|
21
22
|
function createMemoryAccessTokenRevocationStore() {
|
|
@@ -64,6 +65,7 @@ function createRedisAccessTokenRevocationStore(options) {
|
|
|
64
65
|
}
|
|
65
66
|
const keyPrefix = ((_a = options.keyPrefix) !== null && _a !== void 0 ? _a : 'accessi:revoked-token:').replace(/:+$/, ':');
|
|
66
67
|
const ttlMs = Math.max(0, Math.trunc((_b = options.ttlMs) !== null && _b !== void 0 ? _b : 24 * 60 * 60 * 1000));
|
|
68
|
+
const client = (0, redisMetrics_1.instrumentAccessiRedisClient)('access_token_revocation', options.client);
|
|
67
69
|
return {
|
|
68
70
|
revoke(jti, expiresAtMs) {
|
|
69
71
|
return __awaiter(this, void 0, void 0, function* () {
|
|
@@ -73,19 +75,19 @@ function createRedisAccessTokenRevocationStore(options) {
|
|
|
73
75
|
const ttl = Number.isFinite(expiresAtMs)
|
|
74
76
|
? Math.max(1, Math.trunc(expiresAtMs - Date.now()))
|
|
75
77
|
: ttlMs;
|
|
76
|
-
yield
|
|
78
|
+
yield client.eval(REVOKE_SCRIPT, 1, `${keyPrefix}${jti}`, ttl);
|
|
77
79
|
});
|
|
78
80
|
},
|
|
79
81
|
isRevoked(jti) {
|
|
80
82
|
return __awaiter(this, void 0, void 0, function* () {
|
|
81
|
-
const result = yield
|
|
83
|
+
const result = yield client.eval(IS_REVOKED_SCRIPT, 1, `${keyPrefix}${jti}`);
|
|
82
84
|
return Number(result) > 0;
|
|
83
85
|
});
|
|
84
86
|
},
|
|
85
87
|
healthCheck() {
|
|
86
88
|
return __awaiter(this, void 0, void 0, function* () {
|
|
87
89
|
try {
|
|
88
|
-
const result = yield
|
|
90
|
+
const result = yield client.eval("return redis.call('PING')", 0);
|
|
89
91
|
return String(result).toUpperCase() === 'PONG';
|
|
90
92
|
}
|
|
91
93
|
catch (_a) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"accessTokenRevocation.js","sourceRoot":"","sources":["../../../src/accessi-module/security/accessTokenRevocation.ts"],"names":[],"mappings":";;;;;;;;;;;AAiCA,wFA6BC;AAeD,sFAmCC;AAID,sEAMC;AAGD,kEASC;AAGD,4DAYC;AAGD,oFAQC;AAGD,gFAEC;AAxID,MAAM,aAAa,GACjB,qIAAqI,CAAC;AACxI,MAAM,iBAAiB,GAAG,sCAAsC,CAAC;AAEjE,SAAgB,sCAAsC;IACpD,MAAM,OAAO,GAAG,IAAI,GAAG,EAAkB,CAAC;IAC1C,MAAM,KAAK,GAAG,CAAC,GAAW,EAAE,EAAE;QAC5B,KAAK,MAAM,CAAC,GAAG,EAAE,SAAS,CAAC,IAAI,OAAO,EAAE,CAAC;YACvC,IAAI,SAAS,IAAI,GAAG;gBAAE,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QAC5C,CAAC;IACH,CAAC,CAAC;IACF,OAAO;QACC,MAAM,CAAC,GAAG,EAAE,WAAW;;gBAC3B,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;gBACvB,KAAK,CAAC,GAAG,CAAC,CAAC;gBACX,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,GAAG,CAAC,WAAW,EAAE,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC;YACnD,CAAC;SAAA;QACK,SAAS,CAAC,GAAG;;gBACjB,MAAM,SAAS,GAAG,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;gBACnC,IAAI,SAAS,KAAK,SAAS;oBAAE,OAAO,KAAK,CAAC;gBAC1C,IAAI,SAAS,IAAI,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC;oBAC5B,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;oBACpB,OAAO,KAAK,CAAC;gBACf,CAAC;gBACD,OAAO,IAAI,CAAC;YACd,CAAC;SAAA;QACK,WAAW;;gBACf,OAAO,IAAI,CAAC;YACd,CAAC;SAAA;QACD,KAAK;YACH,OAAO,CAAC,KAAK,EAAE,CAAC;QAClB,CAAC;KACF,CAAC;AACJ,CAAC;AAcD,0DAA0D;AAC1D,SAAgB,qCAAqC,CACnD,OAA+C;;IAE/C,IAAI,CAAC,CAAA,OAAO,aAAP,OAAO,uBAAP,OAAO,CAAE,MAAM,CAAA,IAAI,OAAO,OAAO,CAAC,MAAM,CAAC,IAAI,KAAK,UAAU,EAAE,CAAC;QAClE,MAAM,IAAI,KAAK,CAAC,mEAAmE,CAAC,CAAC;IACvF,CAAC;IAED,MAAM,SAAS,GAAG,CAAC,MAAA,OAAO,CAAC,SAAS,mCAAI,wBAAwB,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;IACtF,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,MAAA,OAAO,CAAC,KAAK,mCAAI,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC,CAAC;IAE5E,OAAO;QACC,MAAM,CAAC,GAAG,EAAE,WAAW;;gBAC3B,wEAAwE;gBACxE,0EAA0E;gBAC1E,uCAAuC;gBACvC,MAAM,GAAG,GAAG,MAAM,CAAC,QAAQ,CAAC,WAAW,CAAC;oBACtC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,WAAW,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC;oBACnD,CAAC,CAAC,KAAK,CAAC;gBACV,MAAM,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,aAAa,EAAE,CAAC,EAAE,GAAG,SAAS,GAAG,GAAG,EAAE,EAAE,GAAG,CAAC,CAAC;YACzE,CAAC;SAAA;QAEK,SAAS,CAAC,GAAG;;gBACjB,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,iBAAiB,EAAE,CAAC,EAAE,GAAG,SAAS,GAAG,GAAG,EAAE,CAAC,CAAC;gBACrF,OAAO,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YAC5B,CAAC;SAAA;QAEK,WAAW;;gBACf,IAAI,CAAC;oBACH,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,2BAA2B,EAAE,CAAC,CAAC,CAAC;oBACzE,OAAO,MAAM,CAAC,MAAM,CAAC,CAAC,WAAW,EAAE,KAAK,MAAM,CAAC;gBACjD,CAAC;gBAAC,WAAM,CAAC;oBACP,OAAO,KAAK,CAAC;gBACf,CAAC;YACH,CAAC;SAAA;KACF,CAAC;AACJ,CAAC;AAED,MAAM,kBAAkB,GAAG,sCAAsC,EAAE,CAAC;AAEpE,SAAgB,6BAA6B,CAC3C,OAAuB;;IAEvB,IAAI,CAAA,MAAA,OAAO,CAAC,qBAAqB,0CAAE,OAAO,MAAK,KAAK;QAAE,OAAO,SAAS,CAAC;IACvE,IAAI,MAAA,OAAO,CAAC,qBAAqB,0CAAE,KAAK;QAAE,OAAO,OAAO,CAAC,qBAAqB,CAAC,KAAK,CAAC;IACrF,OAAO,CAAA,MAAA,OAAO,CAAC,qBAAqB,0CAAE,OAAO,MAAK,IAAI,CAAC,CAAC,CAAC,kBAAkB,CAAC,CAAC,CAAC,SAAS,CAAC;AAC1F,CAAC;AAED,mDAAmD;AACnD,SAAsB,2BAA2B,CAC/C,OAAuB,EACvB,OAAgC;;QAEhC,MAAM,KAAK,GAAG,6BAA6B,CAAC,OAAO,CAAC,CAAC;QACrD,IAAI,CAAC,KAAK;YAAE,OAAO,KAAK,CAAC;QACzB,MAAM,GAAG,GAAG,OAAO,OAAO,CAAC,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS,CAAC;QACtE,IAAI,CAAC,GAAG;YAAE,OAAO,KAAK,CAAC;QACvB,OAAO,KAAK,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;IAC9B,CAAC;CAAA;AAED,sEAAsE;AACtE,SAAsB,wBAAwB,CAC5C,OAAuB,EACvB,GAAW,EACX,WAAoB;;;QAEpB,MAAM,KAAK,GAAG,6BAA6B,CAAC,OAAO,CAAC,CAAC;QACrD,IAAI,CAAC,KAAK;YAAE,MAAM,IAAI,KAAK,CAAC,gDAAgD,CAAC,CAAC;QAC9E,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;YACjD,MAAM,IAAI,KAAK,CAAC,iBAAiB,CAAC,CAAC;QACrC,CAAC;QACD,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,MAAA,MAAA,OAAO,CAAC,qBAAqB,0CAAE,KAAK,mCAAI,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC,CAAC;QACnG,MAAM,KAAK,CAAC,MAAM,CAAC,GAAG,EAAE,WAAW,aAAX,WAAW,cAAX,WAAW,GAAI,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK,CAAC,CAAC;IAC7D,CAAC;CAAA;AAED,+EAA+E;AAC/E,SAAsB,oCAAoC,CACxD,OAAuB,EACvB,GAAW,EACX,WAAoB;;QAEpB,IAAI,CAAC,6BAA6B,CAAC,OAAO,CAAC;YAAE,OAAO,KAAK,CAAC;QAC1D,MAAM,wBAAwB,CAAC,OAAO,EAAE,GAAG,EAAE,WAAW,CAAC,CAAC;QAC1D,OAAO,IAAI,CAAC;IACd,CAAC;CAAA;AAED,4DAA4D;AAC5D,SAAgB,kCAAkC;IAChD,kBAAkB,CAAC,KAAK,EAAE,CAAC;AAC7B,CAAC","sourcesContent":["import type { AccessiOptions } from '../AccessiModule';\n\n/**\n * Revoca dei singoli access token tramite `jti`.\n *\n * A differenza della revoca per utente, questa annulla un solo token senza\n * toccare le altre sessioni. La chiave scade al massimo a `exp` del token, cosi\n * la memoria usata resta limitata alla durata dei token emessi.\n */\nexport interface AccessTokenRevocationStore {\n revoke(jti: string, expiresAtMs: number): Promise<void>;\n isRevoked(jti: string): Promise<boolean>;\n /** Health check opzionale per readiness del servizio. */\n healthCheck?(): Promise<boolean>;\n}\n\nexport interface AccessTokenRevocationOptions {\n /** Default: disabilitata se non viene fornito uno store. */\n enabled?: boolean;\n /** Store condiviso (es. Redis) per revocare i token tra istanze. */\n store?: AccessTokenRevocationStore;\n /** TTL di default quando `exp` non e noto. Default: 24 ore. */\n ttlMs?: number;\n}\n\nexport interface MemoryAccessTokenRevocationStore extends AccessTokenRevocationStore {\n clear(): void;\n}\n\nconst REVOKE_SCRIPT =\n \"if tonumber(ARGV[1]) > 0 then return redis.call('SET', KEYS[1], '1', 'PX', ARGV[1]) else return redis.call('SET', KEYS[1], '1') end\";\nconst IS_REVOKED_SCRIPT = \"return redis.call('EXISTS', KEYS[1])\";\n\nexport function createMemoryAccessTokenRevocationStore(): MemoryAccessTokenRevocationStore {\n const revoked = new Map<string, number>();\n const prune = (now: number) => {\n for (const [jti, expiresAt] of revoked) {\n if (expiresAt <= now) revoked.delete(jti);\n }\n };\n return {\n async revoke(jti, expiresAtMs) {\n const now = Date.now();\n prune(now);\n revoked.set(jti, Math.max(expiresAtMs, now + 1));\n },\n async isRevoked(jti) {\n const expiresAt = revoked.get(jti);\n if (expiresAt === undefined) return false;\n if (expiresAt <= Date.now()) {\n revoked.delete(jti);\n return false;\n }\n return true;\n },\n async healthCheck() {\n return true;\n },\n clear() {\n revoked.clear();\n },\n };\n}\n\nexport interface AccessTokenRevocationRedisClient {\n eval(script: string, numKeys: number, ...args: Array<string | number>): Promise<unknown>;\n}\n\nexport interface RedisAccessTokenRevocationStoreOptions {\n client: AccessTokenRevocationRedisClient;\n /** Default: `accessi:revoked-token:`. */\n keyPrefix?: string;\n /** Default: 24 ore. */\n ttlMs?: number;\n}\n\n/** Store Redis per la revoca dei singoli access token. */\nexport function createRedisAccessTokenRevocationStore(\n options: RedisAccessTokenRevocationStoreOptions,\n): AccessTokenRevocationStore {\n if (!options?.client || typeof options.client.eval !== 'function') {\n throw new Error('Redis access token revocation store: client.eval non configurato.');\n }\n\n const keyPrefix = (options.keyPrefix ?? 'accessi:revoked-token:').replace(/:+$/, ':');\n const ttlMs = Math.max(0, Math.trunc(options.ttlMs ?? 24 * 60 * 60 * 1000));\n\n return {\n async revoke(jti, expiresAtMs) {\n // Se il token e gia scaduto non serve conservare la revoca a lungo: una\n // chiave senza TTL resterebbe per sempre. Il minimo di 1 ms la fa sparire\n // subito, come nello store in memoria.\n const ttl = Number.isFinite(expiresAtMs)\n ? Math.max(1, Math.trunc(expiresAtMs - Date.now()))\n : ttlMs;\n await options.client.eval(REVOKE_SCRIPT, 1, `${keyPrefix}${jti}`, ttl);\n },\n\n async isRevoked(jti) {\n const result = await options.client.eval(IS_REVOKED_SCRIPT, 1, `${keyPrefix}${jti}`);\n return Number(result) > 0;\n },\n\n async healthCheck() {\n try {\n const result = await options.client.eval(\"return redis.call('PING')\", 0);\n return String(result).toUpperCase() === 'PONG';\n } catch {\n return false;\n }\n },\n };\n}\n\nconst defaultMemoryStore = createMemoryAccessTokenRevocationStore();\n\nexport function getAccessTokenRevocationStore(\n options: AccessiOptions,\n): AccessTokenRevocationStore | undefined {\n if (options.accessTokenRevocation?.enabled === false) return undefined;\n if (options.accessTokenRevocation?.store) return options.accessTokenRevocation.store;\n return options.accessTokenRevocation?.enabled === true ? defaultMemoryStore : undefined;\n}\n\n/** True se il `jti` del token risulta revocato. */\nexport async function isAccessiAccessTokenRevoked(\n options: AccessiOptions,\n payload: Record<string, unknown>,\n): Promise<boolean> {\n const store = getAccessTokenRevocationStore(options);\n if (!store) return false;\n const jti = typeof payload.jti === 'string' ? payload.jti : undefined;\n if (!jti) return false;\n return store.isRevoked(jti);\n}\n\n/** Revoca un singolo access token. Richiede uno store configurato. */\nexport async function revokeAccessiAccessToken(\n options: AccessiOptions,\n jti: string,\n expiresAtMs?: number,\n): Promise<void> {\n const store = getAccessTokenRevocationStore(options);\n if (!store) throw new Error('Access token revocation store non configurato.');\n if (typeof jti !== 'string' || jti.trim() === '') {\n throw new Error('jti non valido.');\n }\n const ttlMs = Math.max(0, Math.trunc(options.accessTokenRevocation?.ttlMs ?? 24 * 60 * 60 * 1000));\n await store.revoke(jti, expiresAtMs ?? Date.now() + ttlMs);\n}\n\n/** Variante per i flussi applicativi: no-op se la revoca non e configurata. */\nexport async function revokeAccessiAccessTokenIfConfigured(\n options: AccessiOptions,\n jti: string,\n expiresAtMs?: number,\n): Promise<boolean> {\n if (!getAccessTokenRevocationStore(options)) return false;\n await revokeAccessiAccessToken(options, jti, expiresAtMs);\n return true;\n}\n\n/** Solo per test: azzera lo store in memoria di default. */\nexport function clearDefaultAccessTokenRevocations(): void {\n defaultMemoryStore.clear();\n}\n"]}
|
|
1
|
+
{"version":3,"file":"accessTokenRevocation.js","sourceRoot":"","sources":["../../../src/accessi-module/security/accessTokenRevocation.ts"],"names":[],"mappings":"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","sourcesContent":["import type { AccessiOptions } from '../AccessiModule';\nimport { instrumentAccessiRedisClient } from '../observability/redisMetrics';\n\n/**\n * Revoca dei singoli access token tramite `jti`.\n *\n * A differenza della revoca per utente, questa annulla un solo token senza\n * toccare le altre sessioni. La chiave scade al massimo a `exp` del token, cosi\n * la memoria usata resta limitata alla durata dei token emessi.\n */\nexport interface AccessTokenRevocationStore {\n revoke(jti: string, expiresAtMs: number): Promise<void>;\n isRevoked(jti: string): Promise<boolean>;\n /** Health check opzionale per readiness del servizio. */\n healthCheck?(): Promise<boolean>;\n}\n\nexport interface AccessTokenRevocationOptions {\n /** Default: disabilitata se non viene fornito uno store. */\n enabled?: boolean;\n /** Store condiviso (es. Redis) per revocare i token tra istanze. */\n store?: AccessTokenRevocationStore;\n /** TTL di default quando `exp` non e noto. Default: 24 ore. */\n ttlMs?: number;\n}\n\nexport interface MemoryAccessTokenRevocationStore extends AccessTokenRevocationStore {\n clear(): void;\n}\n\nconst REVOKE_SCRIPT =\n \"if tonumber(ARGV[1]) > 0 then return redis.call('SET', KEYS[1], '1', 'PX', ARGV[1]) else return redis.call('SET', KEYS[1], '1') end\";\nconst IS_REVOKED_SCRIPT = \"return redis.call('EXISTS', KEYS[1])\";\n\nexport function createMemoryAccessTokenRevocationStore(): MemoryAccessTokenRevocationStore {\n const revoked = new Map<string, number>();\n const prune = (now: number) => {\n for (const [jti, expiresAt] of revoked) {\n if (expiresAt <= now) revoked.delete(jti);\n }\n };\n return {\n async revoke(jti, expiresAtMs) {\n const now = Date.now();\n prune(now);\n revoked.set(jti, Math.max(expiresAtMs, now + 1));\n },\n async isRevoked(jti) {\n const expiresAt = revoked.get(jti);\n if (expiresAt === undefined) return false;\n if (expiresAt <= Date.now()) {\n revoked.delete(jti);\n return false;\n }\n return true;\n },\n async healthCheck() {\n return true;\n },\n clear() {\n revoked.clear();\n },\n };\n}\n\nexport interface AccessTokenRevocationRedisClient {\n eval(script: string, numKeys: number, ...args: Array<string | number>): Promise<unknown>;\n}\n\nexport interface RedisAccessTokenRevocationStoreOptions {\n client: AccessTokenRevocationRedisClient;\n /** Default: `accessi:revoked-token:`. */\n keyPrefix?: string;\n /** Default: 24 ore. */\n ttlMs?: number;\n}\n\n/** Store Redis per la revoca dei singoli access token. */\nexport function createRedisAccessTokenRevocationStore(\n options: RedisAccessTokenRevocationStoreOptions,\n): AccessTokenRevocationStore {\n if (!options?.client || typeof options.client.eval !== 'function') {\n throw new Error('Redis access token revocation store: client.eval non configurato.');\n }\n\n const keyPrefix = (options.keyPrefix ?? 'accessi:revoked-token:').replace(/:+$/, ':');\n const ttlMs = Math.max(0, Math.trunc(options.ttlMs ?? 24 * 60 * 60 * 1000));\n const client = instrumentAccessiRedisClient('access_token_revocation', options.client);\n\n return {\n async revoke(jti, expiresAtMs) {\n // Se il token e gia scaduto non serve conservare la revoca a lungo: una\n // chiave senza TTL resterebbe per sempre. Il minimo di 1 ms la fa sparire\n // subito, come nello store in memoria.\n const ttl = Number.isFinite(expiresAtMs)\n ? Math.max(1, Math.trunc(expiresAtMs - Date.now()))\n : ttlMs;\n await client.eval(REVOKE_SCRIPT, 1, `${keyPrefix}${jti}`, ttl);\n },\n\n async isRevoked(jti) {\n const result = await client.eval(IS_REVOKED_SCRIPT, 1, `${keyPrefix}${jti}`);\n return Number(result) > 0;\n },\n\n async healthCheck() {\n try {\n const result = await client.eval(\"return redis.call('PING')\", 0);\n return String(result).toUpperCase() === 'PONG';\n } catch {\n return false;\n }\n },\n };\n}\n\nconst defaultMemoryStore = createMemoryAccessTokenRevocationStore();\n\nexport function getAccessTokenRevocationStore(\n options: AccessiOptions,\n): AccessTokenRevocationStore | undefined {\n if (options.accessTokenRevocation?.enabled === false) return undefined;\n if (options.accessTokenRevocation?.store) return options.accessTokenRevocation.store;\n return options.accessTokenRevocation?.enabled === true ? defaultMemoryStore : undefined;\n}\n\n/** True se il `jti` del token risulta revocato. */\nexport async function isAccessiAccessTokenRevoked(\n options: AccessiOptions,\n payload: Record<string, unknown>,\n): Promise<boolean> {\n const store = getAccessTokenRevocationStore(options);\n if (!store) return false;\n const jti = typeof payload.jti === 'string' ? payload.jti : undefined;\n if (!jti) return false;\n return store.isRevoked(jti);\n}\n\n/** Revoca un singolo access token. Richiede uno store configurato. */\nexport async function revokeAccessiAccessToken(\n options: AccessiOptions,\n jti: string,\n expiresAtMs?: number,\n): Promise<void> {\n const store = getAccessTokenRevocationStore(options);\n if (!store) throw new Error('Access token revocation store non configurato.');\n if (typeof jti !== 'string' || jti.trim() === '') {\n throw new Error('jti non valido.');\n }\n const ttlMs = Math.max(0, Math.trunc(options.accessTokenRevocation?.ttlMs ?? 24 * 60 * 60 * 1000));\n await store.revoke(jti, expiresAtMs ?? Date.now() + ttlMs);\n}\n\n/** Variante per i flussi applicativi: no-op se la revoca non e configurata. */\nexport async function revokeAccessiAccessTokenIfConfigured(\n options: AccessiOptions,\n jti: string,\n expiresAtMs?: number,\n): Promise<boolean> {\n if (!getAccessTokenRevocationStore(options)) return false;\n await revokeAccessiAccessToken(options, jti, expiresAtMs);\n return true;\n}\n\n/** Solo per test: azzera lo store in memoria di default. */\nexport function clearDefaultAccessTokenRevocations(): void {\n defaultMemoryStore.clear();\n}\n"]}
|
|
@@ -16,6 +16,7 @@ exports.consumeConsoleEntryTicket = consumeConsoleEntryTicket;
|
|
|
16
16
|
exports.clearConsoleEntryTickets = clearConsoleEntryTickets;
|
|
17
17
|
exports.createRedisConsoleEntryStore = createRedisConsoleEntryStore;
|
|
18
18
|
const crypto_1 = require("crypto");
|
|
19
|
+
const redisMetrics_1 = require("../observability/redisMetrics");
|
|
19
20
|
const DEFAULT_TTL_MS = 60000;
|
|
20
21
|
const DEFAULT_MAX_ENTRIES = 5000;
|
|
21
22
|
const TICKET_PATTERN = /^[a-f0-9]{64}$/i;
|
|
@@ -158,6 +159,7 @@ function createRedisConsoleEntryStore(options) {
|
|
|
158
159
|
const key = deriveEncryptionKey(options.encryptionKey);
|
|
159
160
|
const keyPrefix = ((_a = options.keyPrefix) !== null && _a !== void 0 ? _a : 'accessi:console-entry:').replace(/:+$/, ':');
|
|
160
161
|
const maxAttempts = Math.max(1, (_b = options.maxIssueAttempts) !== null && _b !== void 0 ? _b : 5);
|
|
162
|
+
const client = (0, redisMetrics_1.instrumentAccessiRedisClient)('console_entry', options.client);
|
|
161
163
|
return {
|
|
162
164
|
issue(record, ttlMs) {
|
|
163
165
|
return __awaiter(this, void 0, void 0, function* () {
|
|
@@ -165,7 +167,7 @@ function createRedisConsoleEntryStore(options) {
|
|
|
165
167
|
const value = encryptRecord(record, key);
|
|
166
168
|
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
|
167
169
|
const ticket = createTicket();
|
|
168
|
-
const result = yield
|
|
170
|
+
const result = yield client.eval(SET_SCRIPT, 1, `${keyPrefix}${ticket}`, value, safeTtl);
|
|
169
171
|
if (result) {
|
|
170
172
|
return { ticket, expiresInSeconds: Math.ceil(safeTtl / 1000) };
|
|
171
173
|
}
|
|
@@ -178,7 +180,7 @@ function createRedisConsoleEntryStore(options) {
|
|
|
178
180
|
if (typeof ticket !== 'string' || !TICKET_PATTERN.test(ticket)) {
|
|
179
181
|
return null;
|
|
180
182
|
}
|
|
181
|
-
const value = yield
|
|
183
|
+
const value = yield client.eval(CONSUME_SCRIPT, 1, `${keyPrefix}${ticket}`);
|
|
182
184
|
if (typeof value !== 'string')
|
|
183
185
|
return null;
|
|
184
186
|
return decryptRecord(value, key);
|
|
@@ -187,7 +189,7 @@ function createRedisConsoleEntryStore(options) {
|
|
|
187
189
|
healthCheck() {
|
|
188
190
|
return __awaiter(this, void 0, void 0, function* () {
|
|
189
191
|
try {
|
|
190
|
-
const result = yield
|
|
192
|
+
const result = yield client.eval("return redis.call('PING')", 0);
|
|
191
193
|
return String(result).toUpperCase() === 'PONG';
|
|
192
194
|
}
|
|
193
195
|
catch (_a) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"consoleEntry.js","sourceRoot":"","sources":["../../../src/accessi-module/security/consoleEntry.ts"],"names":[],"mappings":"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","sourcesContent":["import { createCipheriv, createDecipheriv, createHash, randomBytes } from 'crypto';\n\n/**\n * Ticket di ingresso alla console, monouso e a breve scadenza.\n *\n * Serve a passare una sessione gia autenticata (frontend che possiede un JWT Accessi)\n * alla console amministrativa senza reinserire le credenziali e senza mettere il JWT\n * nell'URL: il frontend scambia il proprio JWT con un ticket opaco, apre la console\n * con `#entry=<ticket>` e la console lo scambia con il token originale.\n *\n * Il ticket e valido una sola volta, per pochi secondi, ed e legato all'utente che\n * lo ha richiesto (che deve avere accesso alla console). Lo store di default e in\n * memoria; in multi-istanza usare `createRedisConsoleEntryStore`.\n */\n\nexport interface ConsoleEntryRecord {\n token: string;\n codiceUtente: number;\n}\n\nexport interface ConsoleEntryTicket {\n ticket: string;\n expiresInSeconds: number;\n}\n\n/** Store pluggable: memoria per single instance, Redis per multi-istanza. */\nexport interface ConsoleEntryStore {\n issue(record: ConsoleEntryRecord, ttlMs: number): Promise<ConsoleEntryTicket>;\n consume(ticket: string): Promise<ConsoleEntryRecord | null>;\n /** Health check opzionale per readiness del servizio. */\n healthCheck?(): Promise<boolean>;\n}\n\nconst DEFAULT_TTL_MS = 60_000;\nconst DEFAULT_MAX_ENTRIES = 5000;\nconst TICKET_PATTERN = /^[a-f0-9]{64}$/i;\n\ninterface StoredEntry extends ConsoleEntryRecord {\n expiresAt: number;\n}\n\n/** Store in memoria con API sincrona per retrocompatibilita e API async per l'interfaccia. */\nexport interface MemoryConsoleEntryStore extends ConsoleEntryStore {\n issueSync(record: ConsoleEntryRecord, ttlMs?: number): ConsoleEntryTicket;\n consumeSync(ticket: string): ConsoleEntryRecord | null;\n clear(): void;\n}\n\nfunction createTicket(): string {\n return randomBytes(32).toString('hex');\n}\n\nfunction normalizeTtl(ttlMs: number): number {\n return Math.max(1, Math.trunc(ttlMs));\n}\n\n/** Store in memoria: adatto a sviluppo e a una singola istanza. */\nexport function createMemoryConsoleEntryStore(\n maxEntries: number = DEFAULT_MAX_ENTRIES,\n): MemoryConsoleEntryStore {\n const entries = new Map<string, StoredEntry>();\n\n const sweep = (now: number): void => {\n for (const [ticket, record] of entries) {\n if (record.expiresAt <= now) entries.delete(ticket);\n }\n };\n\n const issueSync = (record: ConsoleEntryRecord, ttlMs: number = DEFAULT_TTL_MS): ConsoleEntryTicket => {\n // Il TTL negativo resta supportato per test e per il comportamento storico:\n // il ticket e immediatamente scaduto.\n const ttl = Math.trunc(ttlMs);\n const now = Date.now();\n sweep(now);\n\n while (entries.size >= maxEntries) {\n const oldest = entries.keys().next().value;\n if (oldest === undefined) break;\n entries.delete(oldest);\n }\n\n const ticket = createTicket();\n entries.set(ticket, { ...record, expiresAt: now + ttl });\n return { ticket, expiresInSeconds: Math.max(0, Math.ceil(ttl / 1000)) };\n };\n\n const consumeSync = (ticket: string): ConsoleEntryRecord | null => {\n if (typeof ticket !== 'string' || !TICKET_PATTERN.test(ticket)) {\n return null;\n }\n const record = entries.get(ticket);\n if (!record) return null;\n entries.delete(ticket);\n if (record.expiresAt <= Date.now()) return null;\n return { token: record.token, codiceUtente: record.codiceUtente };\n };\n\n return {\n issueSync,\n consumeSync,\n clear: () => entries.clear(),\n async issue(record, ttlMs) {\n return issueSync(record, ttlMs);\n },\n async consume(ticket) {\n return consumeSync(ticket);\n },\n async healthCheck() {\n return true;\n },\n };\n}\n\nconst defaultStore = createMemoryConsoleEntryStore();\n\n/** Store di default in memoria, usato quando `AccessiOptions.consoleEntry.store` non e configurato. */\nexport function getDefaultConsoleEntryStore(): ConsoleEntryStore {\n return defaultStore;\n}\n\n// Funzioni storiche sincrone: mantengono il comportamento per test e consumer esistenti.\nexport function issueConsoleEntryTicket(\n record: ConsoleEntryRecord,\n ttlMs: number = DEFAULT_TTL_MS,\n): ConsoleEntryTicket {\n return defaultStore.issueSync(record, ttlMs);\n}\n\nexport function consumeConsoleEntryTicket(ticket: unknown): ConsoleEntryRecord | null {\n return typeof ticket === 'string' ? defaultStore.consumeSync(ticket) : null;\n}\n\n/** Solo per i test: azzera lo store in memoria di default. */\nexport function clearConsoleEntryTickets(): void {\n defaultStore.clear();\n}\n\n/** Client Redis minimale: compatibile con ioredis (`redis.eval(script, numKeys, ...args)`). */\nexport interface ConsoleEntryRedisClient {\n eval(script: string, numKeys: number, ...args: Array<string | number>): Promise<unknown>;\n}\n\nexport interface RedisConsoleEntryStoreOptions {\n client: ConsoleEntryRedisClient;\n /** Segreto per cifrare il JWT nel Redis. Non usare il JWT in chiaro. */\n encryptionKey: string | Buffer;\n /** Default: `accessi:console-entry:`. */\n keyPrefix?: string;\n /** Tentativi in caso di collisione del ticket. Default: 5. */\n maxIssueAttempts?: number;\n}\n\nconst SET_SCRIPT = \"return redis.call('SET', KEYS[1], ARGV[1], 'PX', ARGV[2], 'NX')\";\nconst CONSUME_SCRIPT =\n \"local value = redis.call('GET', KEYS[1]) if value then redis.call('DEL', KEYS[1]) end return value\";\n\nfunction deriveEncryptionKey(secret: string | Buffer): Buffer {\n const source = Buffer.isBuffer(secret) ? secret : Buffer.from(secret, 'utf8');\n if (source.length === 0) {\n throw new Error('Redis console entry encryptionKey non configurata.');\n }\n return source.length === 32 ? source : createHash('sha256').update(source).digest();\n}\n\nfunction encryptRecord(record: ConsoleEntryRecord, key: Buffer): string {\n const iv = randomBytes(12);\n const cipher = createCipheriv('aes-256-gcm', key, iv);\n const ciphertext = Buffer.concat([\n cipher.update(JSON.stringify(record), 'utf8'),\n cipher.final(),\n ]);\n const tag = cipher.getAuthTag();\n return [iv, tag, ciphertext].map((part) => part.toString('base64url')).join('.');\n}\n\nfunction decryptRecord(value: string, key: Buffer): ConsoleEntryRecord | null {\n const [ivPart, tagPart, ciphertextPart] = value.split('.');\n if (!ivPart || !tagPart || !ciphertextPart) return null;\n try {\n const decipher = createDecipheriv('aes-256-gcm', key, Buffer.from(ivPart, 'base64url'));\n decipher.setAuthTag(Buffer.from(tagPart, 'base64url'));\n const plaintext = Buffer.concat([\n decipher.update(Buffer.from(ciphertextPart, 'base64url')),\n decipher.final(),\n ]).toString('utf8');\n const parsed: unknown = JSON.parse(plaintext);\n if (\n !parsed ||\n typeof parsed !== 'object' ||\n typeof (parsed as ConsoleEntryRecord).token !== 'string' ||\n typeof (parsed as ConsoleEntryRecord).codiceUtente !== 'number'\n ) {\n return null;\n }\n return parsed as ConsoleEntryRecord;\n } catch {\n return null;\n }\n}\n\n/**\n * Store Redis per ticket console.\n *\n * - il JWT viene cifrato con AES-256-GCM prima di essere scritto;\n * - `SET ... PX ... NX` evita collisioni;\n * - il consume usa `GET` + `DEL` atomici in Lua, quindi il ticket e monouso\n * anche con piu istanze.\n */\nexport function createRedisConsoleEntryStore(\n options: RedisConsoleEntryStoreOptions,\n): ConsoleEntryStore {\n if (!options?.client || typeof options.client.eval !== 'function') {\n throw new Error('Redis console entry store: client.eval non configurato.');\n }\n\n const key = deriveEncryptionKey(options.encryptionKey);\n const keyPrefix = (options.keyPrefix ?? 'accessi:console-entry:').replace(/:+$/, ':');\n const maxAttempts = Math.max(1, options.maxIssueAttempts ?? 5);\n\n return {\n async issue(record, ttlMs) {\n const safeTtl = normalizeTtl(ttlMs);\n const value = encryptRecord(record, key);\n\n for (let attempt = 0; attempt < maxAttempts; attempt += 1) {\n const ticket = createTicket();\n const result = await options.client.eval(\n SET_SCRIPT,\n 1,\n `${keyPrefix}${ticket}`,\n value,\n safeTtl,\n );\n if (result) {\n return { ticket, expiresInSeconds: Math.ceil(safeTtl / 1000) };\n }\n }\n\n throw new Error('Impossibile emettere un ticket console univoco.');\n },\n\n async consume(ticket) {\n if (typeof ticket !== 'string' || !TICKET_PATTERN.test(ticket)) {\n return null;\n }\n const value = await options.client.eval(\n CONSUME_SCRIPT,\n 1,\n `${keyPrefix}${ticket}`,\n );\n if (typeof value !== 'string') return null;\n return decryptRecord(value, key);\n },\n\n async healthCheck() {\n try {\n const result = await options.client.eval(\"return redis.call('PING')\", 0);\n return String(result).toUpperCase() === 'PONG';\n } catch {\n return false;\n }\n },\n };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"consoleEntry.js","sourceRoot":"","sources":["../../../src/accessi-module/security/consoleEntry.ts"],"names":[],"mappings":"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","sourcesContent":["import { createCipheriv, createDecipheriv, createHash, randomBytes } from 'crypto';\nimport { instrumentAccessiRedisClient } from '../observability/redisMetrics';\n\n/**\n * Ticket di ingresso alla console, monouso e a breve scadenza.\n *\n * Serve a passare una sessione gia autenticata (frontend che possiede un JWT Accessi)\n * alla console amministrativa senza reinserire le credenziali e senza mettere il JWT\n * nell'URL: il frontend scambia il proprio JWT con un ticket opaco, apre la console\n * con `#entry=<ticket>` e la console lo scambia con il token originale.\n *\n * Il ticket e valido una sola volta, per pochi secondi, ed e legato all'utente che\n * lo ha richiesto (che deve avere accesso alla console). Lo store di default e in\n * memoria; in multi-istanza usare `createRedisConsoleEntryStore`.\n */\n\nexport interface ConsoleEntryRecord {\n token: string;\n codiceUtente: number;\n}\n\nexport interface ConsoleEntryTicket {\n ticket: string;\n expiresInSeconds: number;\n}\n\n/** Store pluggable: memoria per single instance, Redis per multi-istanza. */\nexport interface ConsoleEntryStore {\n issue(record: ConsoleEntryRecord, ttlMs: number): Promise<ConsoleEntryTicket>;\n consume(ticket: string): Promise<ConsoleEntryRecord | null>;\n /** Health check opzionale per readiness del servizio. */\n healthCheck?(): Promise<boolean>;\n}\n\nconst DEFAULT_TTL_MS = 60_000;\nconst DEFAULT_MAX_ENTRIES = 5000;\nconst TICKET_PATTERN = /^[a-f0-9]{64}$/i;\n\ninterface StoredEntry extends ConsoleEntryRecord {\n expiresAt: number;\n}\n\n/** Store in memoria con API sincrona per retrocompatibilita e API async per l'interfaccia. */\nexport interface MemoryConsoleEntryStore extends ConsoleEntryStore {\n issueSync(record: ConsoleEntryRecord, ttlMs?: number): ConsoleEntryTicket;\n consumeSync(ticket: string): ConsoleEntryRecord | null;\n clear(): void;\n}\n\nfunction createTicket(): string {\n return randomBytes(32).toString('hex');\n}\n\nfunction normalizeTtl(ttlMs: number): number {\n return Math.max(1, Math.trunc(ttlMs));\n}\n\n/** Store in memoria: adatto a sviluppo e a una singola istanza. */\nexport function createMemoryConsoleEntryStore(\n maxEntries: number = DEFAULT_MAX_ENTRIES,\n): MemoryConsoleEntryStore {\n const entries = new Map<string, StoredEntry>();\n\n const sweep = (now: number): void => {\n for (const [ticket, record] of entries) {\n if (record.expiresAt <= now) entries.delete(ticket);\n }\n };\n\n const issueSync = (record: ConsoleEntryRecord, ttlMs: number = DEFAULT_TTL_MS): ConsoleEntryTicket => {\n // Il TTL negativo resta supportato per test e per il comportamento storico:\n // il ticket e immediatamente scaduto.\n const ttl = Math.trunc(ttlMs);\n const now = Date.now();\n sweep(now);\n\n while (entries.size >= maxEntries) {\n const oldest = entries.keys().next().value;\n if (oldest === undefined) break;\n entries.delete(oldest);\n }\n\n const ticket = createTicket();\n entries.set(ticket, { ...record, expiresAt: now + ttl });\n return { ticket, expiresInSeconds: Math.max(0, Math.ceil(ttl / 1000)) };\n };\n\n const consumeSync = (ticket: string): ConsoleEntryRecord | null => {\n if (typeof ticket !== 'string' || !TICKET_PATTERN.test(ticket)) {\n return null;\n }\n const record = entries.get(ticket);\n if (!record) return null;\n entries.delete(ticket);\n if (record.expiresAt <= Date.now()) return null;\n return { token: record.token, codiceUtente: record.codiceUtente };\n };\n\n return {\n issueSync,\n consumeSync,\n clear: () => entries.clear(),\n async issue(record, ttlMs) {\n return issueSync(record, ttlMs);\n },\n async consume(ticket) {\n return consumeSync(ticket);\n },\n async healthCheck() {\n return true;\n },\n };\n}\n\nconst defaultStore = createMemoryConsoleEntryStore();\n\n/** Store di default in memoria, usato quando `AccessiOptions.consoleEntry.store` non e configurato. */\nexport function getDefaultConsoleEntryStore(): ConsoleEntryStore {\n return defaultStore;\n}\n\n// Funzioni storiche sincrone: mantengono il comportamento per test e consumer esistenti.\nexport function issueConsoleEntryTicket(\n record: ConsoleEntryRecord,\n ttlMs: number = DEFAULT_TTL_MS,\n): ConsoleEntryTicket {\n return defaultStore.issueSync(record, ttlMs);\n}\n\nexport function consumeConsoleEntryTicket(ticket: unknown): ConsoleEntryRecord | null {\n return typeof ticket === 'string' ? defaultStore.consumeSync(ticket) : null;\n}\n\n/** Solo per i test: azzera lo store in memoria di default. */\nexport function clearConsoleEntryTickets(): void {\n defaultStore.clear();\n}\n\n/** Client Redis minimale: compatibile con ioredis (`redis.eval(script, numKeys, ...args)`). */\nexport interface ConsoleEntryRedisClient {\n eval(script: string, numKeys: number, ...args: Array<string | number>): Promise<unknown>;\n}\n\nexport interface RedisConsoleEntryStoreOptions {\n client: ConsoleEntryRedisClient;\n /** Segreto per cifrare il JWT nel Redis. Non usare il JWT in chiaro. */\n encryptionKey: string | Buffer;\n /** Default: `accessi:console-entry:`. */\n keyPrefix?: string;\n /** Tentativi in caso di collisione del ticket. Default: 5. */\n maxIssueAttempts?: number;\n}\n\nconst SET_SCRIPT = \"return redis.call('SET', KEYS[1], ARGV[1], 'PX', ARGV[2], 'NX')\";\nconst CONSUME_SCRIPT =\n \"local value = redis.call('GET', KEYS[1]) if value then redis.call('DEL', KEYS[1]) end return value\";\n\nfunction deriveEncryptionKey(secret: string | Buffer): Buffer {\n const source = Buffer.isBuffer(secret) ? secret : Buffer.from(secret, 'utf8');\n if (source.length === 0) {\n throw new Error('Redis console entry encryptionKey non configurata.');\n }\n return source.length === 32 ? source : createHash('sha256').update(source).digest();\n}\n\nfunction encryptRecord(record: ConsoleEntryRecord, key: Buffer): string {\n const iv = randomBytes(12);\n const cipher = createCipheriv('aes-256-gcm', key, iv);\n const ciphertext = Buffer.concat([\n cipher.update(JSON.stringify(record), 'utf8'),\n cipher.final(),\n ]);\n const tag = cipher.getAuthTag();\n return [iv, tag, ciphertext].map((part) => part.toString('base64url')).join('.');\n}\n\nfunction decryptRecord(value: string, key: Buffer): ConsoleEntryRecord | null {\n const [ivPart, tagPart, ciphertextPart] = value.split('.');\n if (!ivPart || !tagPart || !ciphertextPart) return null;\n try {\n const decipher = createDecipheriv('aes-256-gcm', key, Buffer.from(ivPart, 'base64url'));\n decipher.setAuthTag(Buffer.from(tagPart, 'base64url'));\n const plaintext = Buffer.concat([\n decipher.update(Buffer.from(ciphertextPart, 'base64url')),\n decipher.final(),\n ]).toString('utf8');\n const parsed: unknown = JSON.parse(plaintext);\n if (\n !parsed ||\n typeof parsed !== 'object' ||\n typeof (parsed as ConsoleEntryRecord).token !== 'string' ||\n typeof (parsed as ConsoleEntryRecord).codiceUtente !== 'number'\n ) {\n return null;\n }\n return parsed as ConsoleEntryRecord;\n } catch {\n return null;\n }\n}\n\n/**\n * Store Redis per ticket console.\n *\n * - il JWT viene cifrato con AES-256-GCM prima di essere scritto;\n * - `SET ... PX ... NX` evita collisioni;\n * - il consume usa `GET` + `DEL` atomici in Lua, quindi il ticket e monouso\n * anche con piu istanze.\n */\nexport function createRedisConsoleEntryStore(\n options: RedisConsoleEntryStoreOptions,\n): ConsoleEntryStore {\n if (!options?.client || typeof options.client.eval !== 'function') {\n throw new Error('Redis console entry store: client.eval non configurato.');\n }\n\n const key = deriveEncryptionKey(options.encryptionKey);\n const keyPrefix = (options.keyPrefix ?? 'accessi:console-entry:').replace(/:+$/, ':');\n const maxAttempts = Math.max(1, options.maxIssueAttempts ?? 5);\n const client = instrumentAccessiRedisClient('console_entry', options.client);\n\n return {\n async issue(record, ttlMs) {\n const safeTtl = normalizeTtl(ttlMs);\n const value = encryptRecord(record, key);\n\n for (let attempt = 0; attempt < maxAttempts; attempt += 1) {\n const ticket = createTicket();\n const result = await client.eval(\n SET_SCRIPT,\n 1,\n `${keyPrefix}${ticket}`,\n value,\n safeTtl,\n );\n if (result) {\n return { ticket, expiresInSeconds: Math.ceil(safeTtl / 1000) };\n }\n }\n\n throw new Error('Impossibile emettere un ticket console univoco.');\n },\n\n async consume(ticket) {\n if (typeof ticket !== 'string' || !TICKET_PATTERN.test(ticket)) {\n return null;\n }\n const value = await client.eval(\n CONSUME_SCRIPT,\n 1,\n `${keyPrefix}${ticket}`,\n );\n if (typeof value !== 'string') return null;\n return decryptRecord(value, key);\n },\n\n async healthCheck() {\n try {\n const result = await client.eval(\"return redis.call('PING')\", 0);\n return String(result).toUpperCase() === 'PONG';\n } catch {\n return false;\n }\n },\n };\n}\n"]}
|
|
@@ -12,6 +12,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
12
12
|
exports.createAccessiEdgeRateLimitMiddleware = createAccessiEdgeRateLimitMiddleware;
|
|
13
13
|
const audit_1 = require("./audit");
|
|
14
14
|
const rateLimitStore_1 = require("./rateLimitStore");
|
|
15
|
+
const metrics_1 = require("../observability/metrics");
|
|
15
16
|
const DEFAULT_WINDOW_MS = 60000;
|
|
16
17
|
const DEFAULT_MAX_ATTEMPTS = 300;
|
|
17
18
|
function defaultMatch(path) {
|
|
@@ -43,6 +44,7 @@ function createAccessiEdgeRateLimitMiddleware(options = {}) {
|
|
|
43
44
|
try {
|
|
44
45
|
const decision = yield store.consume(`${keyPrefix}|ip|${ip}`, windowMs, maxAttempts);
|
|
45
46
|
if (!decision.allowed) {
|
|
47
|
+
(0, metrics_1.recordAccessiMetric)('accessi_rate_limit_blocked_total', 1, { layer: 'edge' });
|
|
46
48
|
if (options.audit !== false) {
|
|
47
49
|
(0, audit_1.auditAccessi)('edge_rate_limited', {
|
|
48
50
|
ip,
|
|
@@ -64,6 +66,7 @@ function createAccessiEdgeRateLimitMiddleware(options = {}) {
|
|
|
64
66
|
next();
|
|
65
67
|
}
|
|
66
68
|
catch (_d) {
|
|
69
|
+
(0, metrics_1.recordAccessiMetric)('accessi_store_errors_total', 1, { store: 'edge_rate_limit' });
|
|
67
70
|
if (options.audit !== false) {
|
|
68
71
|
(0, audit_1.auditAccessi)('rate_limit_unavailable', { ip, path: req.path });
|
|
69
72
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"edgeRateLimit.js","sourceRoot":"","sources":["../../../src/accessi-module/security/edgeRateLimit.ts"],"names":[],"mappings":";;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"edgeRateLimit.js","sourceRoot":"","sources":["../../../src/accessi-module/security/edgeRateLimit.ts"],"names":[],"mappings":"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","sourcesContent":["import type { NextFunction, Request, Response } from 'express';\nimport { auditAccessi } from './audit';\nimport { getDefaultRateLimitStore } from './rateLimitStore';\nimport type { RateLimitStore } from './rateLimitStore';\nimport { recordAccessiMetric } from '../observability/metrics';\n\n/** Opzioni del limite grossolano per IP applicato davanti alle rotte Accessi. */\nexport interface AccessiEdgeRateLimitOptions {\n /** Default: true quando l'oggetto viene configurato. */\n enabled?: boolean;\n /** Finestra del limite. Default: 60000 ms. */\n windowMs?: number;\n /** Richieste consentite per IP nella finestra. Default: 300. */\n maxAttempts?: number;\n /** Store condiviso. Default: memoria. */\n store?: RateLimitStore;\n /** Prefisso delle chiavi. Default: `edge`. */\n keyPrefix?: string;\n /** Audit strutturato degli eventi di blocco. Default: true. */\n audit?: boolean;\n /**\n * Decide se applicare il limite alla richiesta.\n *\n * Default: `/api` e `/api/*`. Il bootstrap Accessi passa un matcher che include\n * anche OpenAPI e JWKS.\n */\n match?: (path: string, req: Request) => boolean;\n}\n\nexport type AccessiEdgeRateLimitMiddleware = (\n req: Request,\n res: Response,\n next: NextFunction,\n) => Promise<void>;\n\nconst DEFAULT_WINDOW_MS = 60_000;\nconst DEFAULT_MAX_ATTEMPTS = 300;\n\nfunction defaultMatch(path: string): boolean {\n return path === '/api' || path.startsWith('/api/');\n}\n\n/**\n * Middleware edge per Accessi Service.\n *\n * Non sostituisce il rate limit applicativo per account: aggiunge una soglia\n * grossolana per IP, utile contro bot e floods prima che la richiesta raggiunga\n * controller, database o introspection.\n */\nexport function createAccessiEdgeRateLimitMiddleware(\n options: AccessiEdgeRateLimitOptions = {},\n): AccessiEdgeRateLimitMiddleware | undefined {\n if (options.enabled === false) return undefined;\n\n const windowMs = Math.max(1, Math.trunc(options.windowMs ?? DEFAULT_WINDOW_MS));\n const maxAttempts = Math.max(1, Math.trunc(options.maxAttempts ?? DEFAULT_MAX_ATTEMPTS));\n const store = options.store ?? getDefaultRateLimitStore();\n const keyPrefix = (options.keyPrefix ?? 'edge').replace(/\\|+$/, '');\n const match = options.match ?? defaultMatch;\n\n return async (req: Request, res: Response, next: NextFunction): Promise<void> => {\n if (!match(req.path, req)) {\n next();\n return;\n }\n\n const ip = req.ip ?? req.socket?.remoteAddress ?? 'unknown';\n try {\n const decision = await store.consume(\n `${keyPrefix}|ip|${ip}`,\n windowMs,\n maxAttempts,\n );\n if (!decision.allowed) {\n recordAccessiMetric('accessi_rate_limit_blocked_total', 1, { layer: 'edge' });\n if (options.audit !== false) {\n auditAccessi('edge_rate_limited', {\n ip,\n path: req.path,\n retryAfterSeconds: decision.retryAfterSeconds,\n });\n }\n res.setHeader('Retry-After', String(decision.retryAfterSeconds));\n res.status(429).json({\n severity: 'error',\n status: 429,\n statusCode: 2,\n code: 'ACCESSI_EDGE_RATE_LIMITED',\n error: 'ACCESSI_EDGE_RATE_LIMITED',\n message: 'Troppe richieste. Riprova piu tardi.',\n });\n return;\n }\n next();\n } catch {\n recordAccessiMetric('accessi_store_errors_total', 1, { store: 'edge_rate_limit' });\n if (options.audit !== false) {\n auditAccessi('rate_limit_unavailable', { ip, path: req.path });\n }\n res.status(503).json({\n severity: 'error',\n status: 503,\n statusCode: 2,\n code: 'ACCESSI_RATE_LIMIT_UNAVAILABLE',\n error: 'ACCESSI_RATE_LIMIT_UNAVAILABLE',\n message: 'Rate limit non disponibile. Riprova piu tardi.',\n });\n }\n };\n}\n"]}
|
|
@@ -5,6 +5,7 @@ exports.buildAccessiJwks = buildAccessiJwks;
|
|
|
5
5
|
exports.createAccessiJwksHandler = createAccessiJwksHandler;
|
|
6
6
|
exports.setupAccessiJwks = setupAccessiJwks;
|
|
7
7
|
const crypto_1 = require("crypto");
|
|
8
|
+
const metrics_1 = require("../observability/metrics");
|
|
8
9
|
const DEFAULT_JWKS_PATH = "accessi/.well-known/jwks.json";
|
|
9
10
|
function normalizePath(value) {
|
|
10
11
|
return `/${value.replace(/^\/+|\/+$/g, "")}`;
|
|
@@ -59,6 +60,7 @@ function createAccessiJwksHandler(options) {
|
|
|
59
60
|
return (_req, res) => {
|
|
60
61
|
const document = buildAccessiJwks(options);
|
|
61
62
|
if (!document) {
|
|
63
|
+
(0, metrics_1.recordAccessiMetric)('accessi_jwks_requests_total', 1, { result: 'unavailable' });
|
|
62
64
|
return res.status(404).json({
|
|
63
65
|
severity: "error",
|
|
64
66
|
status: 404,
|
|
@@ -70,6 +72,7 @@ function createAccessiJwksHandler(options) {
|
|
|
70
72
|
}
|
|
71
73
|
res.setHeader("Content-Type", "application/json");
|
|
72
74
|
res.setHeader("Cache-Control", "public, max-age=300");
|
|
75
|
+
(0, metrics_1.recordAccessiMetric)('accessi_jwks_requests_total', 1, { result: 'ok' });
|
|
73
76
|
return res.status(200).send(document);
|
|
74
77
|
};
|
|
75
78
|
}
|