@getstrata/core 1.1.9 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/core/database/transaction.d.ts +2 -1
  3. package/dist/core/database/transactionControl.d.ts +5 -0
  4. package/dist/core/http/loginThrottleMiddleware.d.ts +3 -0
  5. package/dist/core/http/metricsMiddleware.d.ts +1 -2
  6. package/dist/core/http/requestMetaContext.d.ts +2 -0
  7. package/dist/core/http/throttleMiddleware.d.ts +11 -2
  8. package/dist/core/metrics/prometheus.d.ts +11 -1
  9. package/dist/core/queue/jobRunner.d.ts +3 -1
  10. package/dist/core/queue/redisQueue.d.ts +20 -3
  11. package/dist/core/tenant/databaseTenantContext.d.ts +5 -1
  12. package/dist/entries/audit/exportAuditLogs.js +522 -62
  13. package/dist/entries/auth/basicAuthGuard.js +245 -219
  14. package/dist/entries/auth/oauth/samlProvider.js +185 -161
  15. package/dist/entries/auth/oneTimeToken.js +130 -127
  16. package/dist/entries/auth/passwordLogin.js +185 -180
  17. package/dist/entries/auth/saml/samlServiceProvider.js +185 -161
  18. package/dist/entries/auth/scimAuthMiddleware.js +525 -11
  19. package/dist/entries/database/migrations.js +132 -129
  20. package/dist/entries/database/schema.js +58 -39
  21. package/dist/entries/http/metricsMiddleware.js +124 -27
  22. package/dist/entries/http/response.js +107 -0
  23. package/dist/entries/http/routeMiddleware.js +40 -0
  24. package/dist/entries/http/throttleMiddleware.js +124 -11
  25. package/dist/entries/jobs/exportAuditLogsJob.js +522 -62
  26. package/dist/entries/metrics/prometheus.js +71 -11
  27. package/dist/entries/queue/createAppQueue.js +215 -21
  28. package/dist/entries/queue/jobRunner.js +3 -2
  29. package/dist/entries/queue/publicQueue.js +215 -21
  30. package/dist/entries/queue/queueMetrics.js +215 -21
  31. package/dist/entries/queue/redisQueue.js +223 -22
  32. package/dist/entries/tenant/databaseTenantContext.js +477 -16
  33. package/dist/entries/tenant/tenantDatabaseScope.js +523 -9
  34. package/dist/framework/public-api.d.ts +2 -2
  35. package/dist/index.js +954 -457
  36. package/package.json +1 -1
@@ -26,14 +26,6 @@ function createAsyncContextStore(key) {
26
26
  }
27
27
  var init_asyncContextStore = () => {};
28
28
 
29
- // ../../src/core/database/migrations/runner.ts
30
- import { readdir } from "fs/promises";
31
- import { join } from "path";
32
- import { pathToFileURL } from "url";
33
-
34
- // ../../src/core/database/dialect.ts
35
- init_asyncContextStore();
36
-
37
29
  // ../../src/core/database/schema/driver.ts
38
30
  function normalizeConnectionName(connection) {
39
31
  const normalized = connection.trim().toLowerCase();
@@ -73,6 +65,7 @@ function resolveDatabaseDriver(options = {}) {
73
65
  }
74
66
  return "pgsql";
75
67
  }
68
+ var init_driver = () => {};
76
69
 
77
70
  // ../../src/core/database/dialect.ts
78
71
  function assertSafeIdentifier(identifier) {
@@ -84,133 +77,143 @@ function assertSafeIdentifier(identifier) {
84
77
  function quoteIdentifierFor(dialect, column) {
85
78
  return dialect.quoteIdentifier(column);
86
79
  }
87
- var postgresDialect = {
88
- driver: "pgsql",
89
- placeholder(index) {
90
- return `$${index}`;
91
- },
92
- quoteIdentifier(identifier) {
93
- return `"${assertSafeIdentifier(identifier)}"`;
94
- },
95
- nowExpression() {
96
- return "NOW()";
97
- },
98
- timestampValue(value) {
99
- return value.toISOString();
100
- },
101
- returningClause(columns) {
102
- return ` RETURNING ${columns}`;
103
- },
104
- ilikeOperator() {
105
- return "ILIKE";
106
- },
107
- nullsLastSuffix() {
108
- return " NULLS LAST";
109
- },
110
- castToText(expression) {
111
- return `${expression}::text`;
112
- },
113
- upsertSuffix(conflictColumns, updateColumns) {
114
- const target = conflictColumns.map((column) => quoteIdentifierFor(this, column)).join(", ");
115
- if (updateColumns.length === 0) {
116
- return ` ON CONFLICT (${target}) DO NOTHING`;
117
- }
118
- const assignments = updateColumns.map((column) => {
119
- const quoted = quoteIdentifierFor(this, column);
120
- return `${quoted} = excluded.${quoted}`;
121
- }).join(", ");
122
- return ` ON CONFLICT (${target}) DO UPDATE SET ${assignments}`;
123
- }
124
- };
125
- var mysqlDialect = {
126
- driver: "mysql",
127
- placeholder() {
128
- return "?";
129
- },
130
- quoteIdentifier(identifier) {
131
- return `\`${assertSafeIdentifier(identifier)}\``;
132
- },
133
- nowExpression() {
134
- return "CURRENT_TIMESTAMP";
135
- },
136
- timestampValue(value) {
137
- return value.toISOString().slice(0, 19).replace("T", " ");
138
- },
139
- returningClause() {
140
- return "";
141
- },
142
- ilikeOperator() {
143
- return "LIKE";
144
- },
145
- nullsLastSuffix() {
146
- return "";
147
- },
148
- castToText(expression) {
149
- return `CAST(${expression} AS CHAR)`;
150
- },
151
- upsertSuffix(conflictColumns, updateColumns) {
152
- if (updateColumns.length === 0) {
153
- const anchor = quoteIdentifierFor(this, conflictColumns[0] ?? "");
154
- return ` ON DUPLICATE KEY UPDATE ${anchor} = ${anchor}`;
155
- }
156
- const assignments = updateColumns.map((column) => {
157
- const quoted = quoteIdentifierFor(this, column);
158
- return `${quoted} = VALUES(${quoted})`;
159
- }).join(", ");
160
- return ` ON DUPLICATE KEY UPDATE ${assignments}`;
161
- }
162
- };
163
- var sqliteDialect = {
164
- driver: "sqlite",
165
- placeholder() {
166
- return "?";
167
- },
168
- quoteIdentifier(identifier) {
169
- return `"${assertSafeIdentifier(identifier)}"`;
170
- },
171
- nowExpression() {
172
- return "strftime('%Y-%m-%dT%H:%M:%fZ', 'now')";
173
- },
174
- timestampValue(value) {
175
- return value.toISOString();
176
- },
177
- returningClause(columns) {
178
- return ` RETURNING ${columns}`;
179
- },
180
- ilikeOperator() {
181
- return "LIKE";
182
- },
183
- nullsLastSuffix() {
184
- return "";
185
- },
186
- castToText(expression) {
187
- return `CAST(${expression} AS TEXT)`;
188
- },
189
- upsertSuffix(conflictColumns, updateColumns) {
190
- const target = conflictColumns.map((column) => quoteIdentifierFor(this, column)).join(", ");
191
- if (updateColumns.length === 0) {
192
- return ` ON CONFLICT (${target}) DO NOTHING`;
193
- }
194
- const assignments = updateColumns.map((column) => {
195
- const quoted = quoteIdentifierFor(this, column);
196
- return `${quoted} = excluded.${quoted}`;
197
- }).join(", ");
198
- return ` ON CONFLICT (${target}) DO UPDATE SET ${assignments}`;
199
- }
200
- };
201
- var dialects = {
202
- pgsql: postgresDialect,
203
- mysql: mysqlDialect,
204
- sqlite: sqliteDialect
205
- };
206
- var dialectContext = createAsyncContextStore("@getstrata/sqlDialect");
207
- var dialectOverride = null;
208
80
  function dialectFor(driver) {
209
81
  return dialects[driver];
210
82
  }
211
83
  function currentSqlDialect() {
212
84
  return dialectContext.getStore() ?? dialectOverride ?? dialectFor(resolveDatabaseDriver());
213
85
  }
86
+ var postgresDialect, mysqlDialect, sqliteDialect, dialects, dialectContext, dialectOverride = null;
87
+ var init_dialect = __esm(() => {
88
+ init_asyncContextStore();
89
+ init_driver();
90
+ postgresDialect = {
91
+ driver: "pgsql",
92
+ placeholder(index) {
93
+ return `$${index}`;
94
+ },
95
+ quoteIdentifier(identifier) {
96
+ return `"${assertSafeIdentifier(identifier)}"`;
97
+ },
98
+ nowExpression() {
99
+ return "NOW()";
100
+ },
101
+ timestampValue(value) {
102
+ return value.toISOString();
103
+ },
104
+ returningClause(columns) {
105
+ return ` RETURNING ${columns}`;
106
+ },
107
+ ilikeOperator() {
108
+ return "ILIKE";
109
+ },
110
+ nullsLastSuffix() {
111
+ return " NULLS LAST";
112
+ },
113
+ castToText(expression) {
114
+ return `${expression}::text`;
115
+ },
116
+ upsertSuffix(conflictColumns, updateColumns) {
117
+ const target = conflictColumns.map((column) => quoteIdentifierFor(this, column)).join(", ");
118
+ if (updateColumns.length === 0) {
119
+ return ` ON CONFLICT (${target}) DO NOTHING`;
120
+ }
121
+ const assignments = updateColumns.map((column) => {
122
+ const quoted = quoteIdentifierFor(this, column);
123
+ return `${quoted} = excluded.${quoted}`;
124
+ }).join(", ");
125
+ return ` ON CONFLICT (${target}) DO UPDATE SET ${assignments}`;
126
+ }
127
+ };
128
+ mysqlDialect = {
129
+ driver: "mysql",
130
+ placeholder() {
131
+ return "?";
132
+ },
133
+ quoteIdentifier(identifier) {
134
+ return `\`${assertSafeIdentifier(identifier)}\``;
135
+ },
136
+ nowExpression() {
137
+ return "CURRENT_TIMESTAMP";
138
+ },
139
+ timestampValue(value) {
140
+ return value.toISOString().slice(0, 19).replace("T", " ");
141
+ },
142
+ returningClause() {
143
+ return "";
144
+ },
145
+ ilikeOperator() {
146
+ return "LIKE";
147
+ },
148
+ nullsLastSuffix() {
149
+ return "";
150
+ },
151
+ castToText(expression) {
152
+ return `CAST(${expression} AS CHAR)`;
153
+ },
154
+ upsertSuffix(conflictColumns, updateColumns) {
155
+ if (updateColumns.length === 0) {
156
+ const anchor = quoteIdentifierFor(this, conflictColumns[0] ?? "");
157
+ return ` ON DUPLICATE KEY UPDATE ${anchor} = ${anchor}`;
158
+ }
159
+ const assignments = updateColumns.map((column) => {
160
+ const quoted = quoteIdentifierFor(this, column);
161
+ return `${quoted} = VALUES(${quoted})`;
162
+ }).join(", ");
163
+ return ` ON DUPLICATE KEY UPDATE ${assignments}`;
164
+ }
165
+ };
166
+ sqliteDialect = {
167
+ driver: "sqlite",
168
+ placeholder() {
169
+ return "?";
170
+ },
171
+ quoteIdentifier(identifier) {
172
+ return `"${assertSafeIdentifier(identifier)}"`;
173
+ },
174
+ nowExpression() {
175
+ return "strftime('%Y-%m-%dT%H:%M:%fZ', 'now')";
176
+ },
177
+ timestampValue(value) {
178
+ return value.toISOString();
179
+ },
180
+ returningClause(columns) {
181
+ return ` RETURNING ${columns}`;
182
+ },
183
+ ilikeOperator() {
184
+ return "LIKE";
185
+ },
186
+ nullsLastSuffix() {
187
+ return "";
188
+ },
189
+ castToText(expression) {
190
+ return `CAST(${expression} AS TEXT)`;
191
+ },
192
+ upsertSuffix(conflictColumns, updateColumns) {
193
+ const target = conflictColumns.map((column) => quoteIdentifierFor(this, column)).join(", ");
194
+ if (updateColumns.length === 0) {
195
+ return ` ON CONFLICT (${target}) DO NOTHING`;
196
+ }
197
+ const assignments = updateColumns.map((column) => {
198
+ const quoted = quoteIdentifierFor(this, column);
199
+ return `${quoted} = excluded.${quoted}`;
200
+ }).join(", ");
201
+ return ` ON CONFLICT (${target}) DO UPDATE SET ${assignments}`;
202
+ }
203
+ };
204
+ dialects = {
205
+ pgsql: postgresDialect,
206
+ mysql: mysqlDialect,
207
+ sqlite: sqliteDialect
208
+ };
209
+ dialectContext = createAsyncContextStore("@getstrata/sqlDialect");
210
+ });
211
+
212
+ // ../../src/core/database/migrations/runner.ts
213
+ init_dialect();
214
+ import { readdir } from "fs/promises";
215
+ import { join } from "path";
216
+ import { pathToFileURL } from "url";
214
217
 
215
218
  // ../../src/core/database/migrations/advisoryLock.ts
216
219
  var MIGRATION_LOCK_KEY = 42424242;
@@ -1,4 +1,57 @@
1
1
  // @bun
2
+ var __esm = (fn, res, err) => () => {
3
+ if (fn)
4
+ try {
5
+ res = fn(fn = 0);
6
+ } catch (e) {
7
+ err = [e];
8
+ }
9
+ if (err)
10
+ throw err[0];
11
+ return res;
12
+ };
13
+
14
+ // ../../src/core/database/schema/driver.ts
15
+ function normalizeConnectionName(connection) {
16
+ const normalized = connection.trim().toLowerCase();
17
+ if (normalized === "pgsql" || normalized === "postgres" || normalized === "postgresql") {
18
+ return "pgsql";
19
+ }
20
+ if (normalized === "mysql" || normalized === "mariadb") {
21
+ return "mysql";
22
+ }
23
+ if (normalized === "sqlite") {
24
+ return "sqlite";
25
+ }
26
+ throw new Error(`Unsupported DB_CONNECTION: ${connection}`);
27
+ }
28
+ function resolveDriverFromUrl(url) {
29
+ const normalized = url.trim().toLowerCase();
30
+ if (normalized.startsWith("postgres://") || normalized.startsWith("postgresql://") || normalized.startsWith("postgres:")) {
31
+ return "pgsql";
32
+ }
33
+ if (normalized.startsWith("mysql://") || normalized.startsWith("mysql:")) {
34
+ return "mysql";
35
+ }
36
+ if (normalized.startsWith("sqlite:")) {
37
+ return "sqlite";
38
+ }
39
+ return null;
40
+ }
41
+ function resolveDatabaseDriver(options = {}) {
42
+ const connection = options.connection ?? process.env.DB_CONNECTION;
43
+ if (connection) {
44
+ return normalizeConnectionName(connection);
45
+ }
46
+ const url = options.url ?? process.env.DATABASE_URL ?? "";
47
+ const fromUrl = resolveDriverFromUrl(url);
48
+ if (fromUrl) {
49
+ return fromUrl;
50
+ }
51
+ return "pgsql";
52
+ }
53
+ var init_driver = () => {};
54
+
2
55
  // ../../src/core/database/schema/columnDefinition.ts
3
56
  class ColumnDefinition {
4
57
  name;
@@ -220,45 +273,10 @@ class Blueprint {
220
273
  });
221
274
  }
222
275
  }
223
- // ../../src/core/database/schema/driver.ts
224
- function normalizeConnectionName(connection) {
225
- const normalized = connection.trim().toLowerCase();
226
- if (normalized === "pgsql" || normalized === "postgres" || normalized === "postgresql") {
227
- return "pgsql";
228
- }
229
- if (normalized === "mysql" || normalized === "mariadb") {
230
- return "mysql";
231
- }
232
- if (normalized === "sqlite") {
233
- return "sqlite";
234
- }
235
- throw new Error(`Unsupported DB_CONNECTION: ${connection}`);
236
- }
237
- function resolveDriverFromUrl(url) {
238
- const normalized = url.trim().toLowerCase();
239
- if (normalized.startsWith("postgres://") || normalized.startsWith("postgresql://") || normalized.startsWith("postgres:")) {
240
- return "pgsql";
241
- }
242
- if (normalized.startsWith("mysql://") || normalized.startsWith("mysql:")) {
243
- return "mysql";
244
- }
245
- if (normalized.startsWith("sqlite:")) {
246
- return "sqlite";
247
- }
248
- return null;
249
- }
250
- function resolveDatabaseDriver(options = {}) {
251
- const connection = options.connection ?? process.env.DB_CONNECTION;
252
- if (connection) {
253
- return normalizeConnectionName(connection);
254
- }
255
- const url = options.url ?? process.env.DATABASE_URL ?? "";
256
- const fromUrl = resolveDriverFromUrl(url);
257
- if (fromUrl) {
258
- return fromUrl;
259
- }
260
- return "pgsql";
261
- }
276
+
277
+ // ../../src/core/database/schema/index.ts
278
+ init_driver();
279
+
262
280
  // ../../src/core/database/schema/errors.ts
263
281
  class UnsupportedSchemaFeatureError extends Error {
264
282
  constructor(feature, driver) {
@@ -1084,6 +1102,7 @@ function grammarForDriver(driver) {
1084
1102
  }
1085
1103
  }
1086
1104
  // ../../src/core/database/schema/schema.ts
1105
+ init_driver();
1087
1106
  class SchemaBuilder {
1088
1107
  #driver;
1089
1108
  #statements = [];
@@ -1,6 +1,50 @@
1
1
  // @bun
2
+ var __esm = (fn, res, err) => () => {
3
+ if (fn)
4
+ try {
5
+ res = fn(fn = 0);
6
+ } catch (e) {
7
+ err = [e];
8
+ }
9
+ if (err)
10
+ throw err[0];
11
+ return res;
12
+ };
13
+
14
+ // ../../src/core/runtime/asyncContextStore.ts
15
+ import { AsyncLocalStorage } from "async_hooks";
16
+ function createAsyncContextStore(key) {
17
+ const symbol = Symbol.for(key);
18
+ const globalRecord = globalThis;
19
+ const existing = globalRecord[symbol];
20
+ if (existing) {
21
+ return existing;
22
+ }
23
+ const store = new AsyncLocalStorage;
24
+ globalRecord[symbol] = store;
25
+ return store;
26
+ }
27
+ var init_asyncContextStore = () => {};
28
+
2
29
  // ../../src/core/metrics/prometheus.ts
30
+ var HTTP_DURATION_BUCKETS_MS = [5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000, 1e4];
31
+ function emptyHistogram() {
32
+ return {
33
+ cumulative: HTTP_DURATION_BUCKETS_MS.map(() => 0),
34
+ sum: 0,
35
+ count: 0
36
+ };
37
+ }
38
+
3
39
  class PrometheusRegistry {
40
+ maxSeries;
41
+ seriesLabels = new Map;
42
+ constructor(maxSeries = 4096) {
43
+ this.maxSeries = maxSeries;
44
+ if (!Number.isSafeInteger(maxSeries) || maxSeries < 1) {
45
+ throw new Error("Metrics maxSeries must be a positive safe integer.");
46
+ }
47
+ }
4
48
  httpRequestsTotal = new Map;
5
49
  httpRequestDurationMs = new Map;
6
50
  incrementHttpRequest(labels) {
@@ -8,10 +52,20 @@ class PrometheusRegistry {
8
52
  this.httpRequestsTotal.set(key, (this.httpRequestsTotal.get(key) ?? 0) + 1);
9
53
  }
10
54
  observeHttpDuration(labels, durationMs) {
55
+ if (!Number.isFinite(durationMs) || durationMs < 0)
56
+ return;
11
57
  const key = this.metricKey(labels);
12
- const samples = this.httpRequestDurationMs.get(key) ?? [];
13
- samples.push(durationMs);
14
- this.httpRequestDurationMs.set(key, samples);
58
+ const histogram = this.httpRequestDurationMs.get(key) ?? emptyHistogram();
59
+ histogram.count += 1;
60
+ histogram.sum += durationMs;
61
+ for (let index = 0;index < HTTP_DURATION_BUCKETS_MS.length; index += 1) {
62
+ const bound = HTTP_DURATION_BUCKETS_MS[index];
63
+ if (bound !== undefined && durationMs <= bound) {
64
+ const count = histogram.cumulative[index] ?? 0;
65
+ histogram.cumulative[index] = count + 1;
66
+ }
67
+ }
68
+ this.httpRequestDurationMs.set(key, histogram);
15
69
  }
16
70
  renderMetrics() {
17
71
  const lines = [
@@ -21,16 +75,22 @@ class PrometheusRegistry {
21
75
  for (const [key, value] of this.httpRequestsTotal) {
22
76
  lines.push(`http_requests_total{${key}} ${value}`);
23
77
  }
24
- lines.push("# HELP http_request_duration_ms_sum Sum of HTTP request durations in milliseconds.", "# TYPE http_request_duration_ms_sum counter");
25
- for (const [key, samples] of this.httpRequestDurationMs) {
26
- const sum = samples.reduce((total, sample) => total + sample, 0);
27
- lines.push(`http_request_duration_ms_sum{${key}} ${sum}`);
78
+ lines.push("# HELP http_request_duration_ms HTTP request duration in milliseconds.", "# TYPE http_request_duration_ms histogram");
79
+ for (const [key, histogram] of this.httpRequestDurationMs) {
80
+ for (let index = 0;index < HTTP_DURATION_BUCKETS_MS.length; index += 1) {
81
+ const bound = HTTP_DURATION_BUCKETS_MS[index];
82
+ lines.push(`http_request_duration_ms_bucket{${key},le="${bound}"} ${histogram.cumulative[index] ?? 0}`);
83
+ }
84
+ lines.push(`http_request_duration_ms_bucket{${key},le="+Inf"} ${histogram.count}`);
85
+ lines.push(`http_request_duration_ms_sum{${key}} ${histogram.sum}`);
86
+ lines.push(`http_request_duration_ms_count{${key}} ${histogram.count}`);
28
87
  }
29
88
  return `${lines.join(`
30
89
  `)}
31
90
  `;
32
91
  }
33
92
  resetForTests() {
93
+ this.seriesLabels.clear();
34
94
  this.httpRequestsTotal.clear();
35
95
  this.httpRequestDurationMs.clear();
36
96
  }
@@ -40,9 +100,10 @@ class PrometheusRegistry {
40
100
  let totalRequests = 0;
41
101
  for (const [key, count] of this.httpRequestsTotal) {
42
102
  totalRequests += count;
43
- const method = key.match(/method="([^"]+)"/)?.[1] ?? "GET";
44
- const path = key.match(/path="([^"]+)"/)?.[1] ?? "/";
45
- const status = key.match(/status="([^"]+)"/)?.[1] ?? "200";
103
+ const labels = this.seriesLabels.get(key);
104
+ if (!labels)
105
+ continue;
106
+ const { method, path, status } = labels;
46
107
  byStatus[status] = (byStatus[status] ?? 0) + count;
47
108
  const pathKey = `${method} ${path}`;
48
109
  const existing = pathCounts.get(pathKey);
@@ -59,33 +120,69 @@ class PrometheusRegistry {
59
120
  topPaths
60
121
  };
61
122
  }
123
+ getStorageStats() {
124
+ return {
125
+ series: this.seriesLabels.size,
126
+ histograms: this.httpRequestDurationMs.size,
127
+ bucketCounters: this.httpRequestDurationMs.size * HTTP_DURATION_BUCKETS_MS.length
128
+ };
129
+ }
62
130
  metricKey(labels) {
63
- return `method="${labels.method}",path="${labels.path}",status="${labels.status}"`;
131
+ const normalized = {
132
+ method: /^(GET|HEAD|POST|PUT|PATCH|DELETE|CONNECT|OPTIONS|TRACE)$/.test(labels.method) ? labels.method : "OTHER",
133
+ path: labels.path.length <= 512 ? labels.path : "__overflow__",
134
+ status: /^[1-5][0-9]{2}$/.test(labels.status) ? labels.status : "000"
135
+ };
136
+ const escapeLabel = (value) => value.replaceAll("\\", "\\\\").replaceAll(`
137
+ `, "\\n").replaceAll('"', "\\\"");
138
+ const encode = (value) => `method="${value.method}",path="${escapeLabel(value.path)}",status="${value.status}"`;
139
+ let key = encode(normalized);
140
+ if (!this.seriesLabels.has(key)) {
141
+ if (this.seriesLabels.size >= this.maxSeries) {
142
+ const overflow = { method: "OTHER", path: "__overflow__", status: "000" };
143
+ key = encode(overflow);
144
+ this.seriesLabels.set(key, overflow);
145
+ } else {
146
+ this.seriesLabels.set(key, normalized);
147
+ }
148
+ }
149
+ return key;
64
150
  }
65
151
  }
66
152
  var prometheusRegistry = new PrometheusRegistry;
67
153
 
68
- // ../../src/core/http/metricsMiddleware.ts
69
- function normalizeMetricPath(pathname) {
70
- return pathname.replace(/\/\d+/g, "/:id").replace(/\/[0-9a-f-]{36}/gi, "/:id");
154
+ // ../../src/core/http/requestMetaContext.ts
155
+ init_asyncContextStore();
156
+ var requestMetaContext = createAsyncContextStore("@getstrata/requestMetaContext");
157
+ function runWithRequestMeta(meta, callback) {
158
+ return requestMetaContext.run(meta, callback);
159
+ }
160
+ function currentRequestMeta() {
161
+ return requestMetaContext.getStore() ?? {
162
+ ipAddress: null,
163
+ userAgent: null
164
+ };
71
165
  }
166
+
167
+ // ../../src/core/http/metricsMiddleware.ts
72
168
  function createMetricsMiddleware() {
73
169
  return async (request, next) => {
74
170
  const startedAt = performance.now();
75
- const response = await next();
76
- const durationMs = performance.now() - startedAt;
77
- const path = normalizeMetricPath(new URL(request.url).pathname);
78
- const labels = {
79
- method: request.method,
80
- path,
81
- status: String(response.status)
82
- };
83
- prometheusRegistry.incrementHttpRequest(labels);
84
- prometheusRegistry.observeHttpDuration(labels, durationMs);
85
- return response;
171
+ let response;
172
+ try {
173
+ response = await next();
174
+ return response;
175
+ } finally {
176
+ const labels = {
177
+ method: request.method,
178
+ path: currentRequestMeta().routeTemplate ?? "__unmatched__",
179
+ status: String(response?.status ?? 500)
180
+ };
181
+ prometheusRegistry.incrementHttpRequest(labels);
182
+ prometheusRegistry.observeHttpDuration(labels, performance.now() - startedAt);
183
+ }
86
184
  };
87
185
  }
88
186
  export {
89
- createMetricsMiddleware,
90
- normalizeMetricPath
187
+ createMetricsMiddleware
91
188
  };