@forklaunch/core 0.19.5 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/{apiDefinition.types-DnUkFmfT.d.mts → apiDefinition.types-Br0fDuBQ.d.mts} +42 -8
- package/lib/{apiDefinition.types-DnUkFmfT.d.ts → apiDefinition.types-Br0fDuBQ.d.ts} +42 -8
- package/lib/cache/index.d.mts +3 -133
- package/lib/cache/index.d.ts +3 -133
- package/lib/http/index.d.mts +88 -3
- package/lib/http/index.d.ts +88 -3
- package/lib/http/index.js +168 -25
- package/lib/http/index.js.map +1 -1
- package/lib/http/index.mjs +166 -25
- package/lib/http/index.mjs.map +1 -1
- package/lib/mappers/index.js.map +1 -1
- package/lib/mappers/index.mjs.map +1 -1
- package/lib/persistence/index.d.mts +298 -1
- package/lib/persistence/index.d.ts +298 -1
- package/lib/persistence/index.js +465 -0
- package/lib/persistence/index.js.map +1 -1
- package/lib/persistence/index.mjs +426 -0
- package/lib/persistence/index.mjs.map +1 -1
- package/lib/ttlCache.interface-DClm-lSa.d.mts +133 -0
- package/lib/ttlCache.interface-DClm-lSa.d.ts +133 -0
- package/lib/ws/index.d.mts +1 -1
- package/lib/ws/index.d.ts +1 -1
- package/package.json +9 -9
package/lib/http/index.mjs
CHANGED
|
@@ -242,11 +242,36 @@ function discriminateResponseBodies(schemaValidator, responses) {
|
|
|
242
242
|
// src/http/router/routerSharedLogic.ts
|
|
243
243
|
import { isRecord as isRecord2 } from "@forklaunch/common";
|
|
244
244
|
|
|
245
|
+
// src/http/guards/hasPermissionChecks.ts
|
|
246
|
+
function hasPermissionChecks(maybePermissionedAuth) {
|
|
247
|
+
return typeof maybePermissionedAuth === "object" && maybePermissionedAuth !== null && ("allowedPermissions" in maybePermissionedAuth || "forbiddenPermissions" in maybePermissionedAuth);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// src/http/guards/hasRoleChecks.ts
|
|
251
|
+
function hasRoleChecks(maybeRoledAuth) {
|
|
252
|
+
if (typeof maybeRoledAuth !== "object" || maybeRoledAuth === null) {
|
|
253
|
+
return false;
|
|
254
|
+
}
|
|
255
|
+
const hasAllowedRoles = "allowedRoles" in maybeRoledAuth && maybeRoledAuth.allowedRoles instanceof Set && maybeRoledAuth.allowedRoles.size > 0;
|
|
256
|
+
const hasForbiddenRoles = "forbiddenRoles" in maybeRoledAuth && maybeRoledAuth.forbiddenRoles instanceof Set && maybeRoledAuth.forbiddenRoles.size > 0;
|
|
257
|
+
return hasAllowedRoles || hasForbiddenRoles;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// src/http/guards/hasScopeChecks.ts
|
|
261
|
+
function hasScopeChecks(maybePermissionedAuth) {
|
|
262
|
+
return typeof maybePermissionedAuth === "object" && maybePermissionedAuth !== null && "requiredScope" in maybePermissionedAuth && maybePermissionedAuth.requiredScope != null;
|
|
263
|
+
}
|
|
264
|
+
|
|
245
265
|
// src/http/guards/hasVersionedSchema.ts
|
|
246
266
|
function hasVersionedSchema(contractDetails) {
|
|
247
267
|
return typeof contractDetails === "object" && contractDetails !== null && "versions" in contractDetails && contractDetails.versions !== null;
|
|
248
268
|
}
|
|
249
269
|
|
|
270
|
+
// src/http/guards/isHmacMethod.ts
|
|
271
|
+
function isHmacMethod(maybeHmacMethod) {
|
|
272
|
+
return typeof maybeHmacMethod === "object" && maybeHmacMethod !== null && "hmac" in maybeHmacMethod && maybeHmacMethod.hmac != null;
|
|
273
|
+
}
|
|
274
|
+
|
|
250
275
|
// src/http/guards/isPathParamContractDetails.ts
|
|
251
276
|
function isPathParamHttpContractDetails(maybePathParamHttpContractDetails) {
|
|
252
277
|
return maybePathParamHttpContractDetails != null && typeof maybePathParamHttpContractDetails === "object" && "name" in maybePathParamHttpContractDetails && "summary" in maybePathParamHttpContractDetails && maybePathParamHttpContractDetails.name != null && maybePathParamHttpContractDetails.summary != null && ("responses" in maybePathParamHttpContractDetails && maybePathParamHttpContractDetails.responses != null || "versions" in maybePathParamHttpContractDetails && typeof maybePathParamHttpContractDetails.versions === "object" && maybePathParamHttpContractDetails.versions != null && Object.values(maybePathParamHttpContractDetails.versions).every(
|
|
@@ -299,11 +324,6 @@ function isBasicAuthMethod(maybeBasicAuthMethod) {
|
|
|
299
324
|
return typeof maybeBasicAuthMethod === "object" && maybeBasicAuthMethod !== null && "basic" in maybeBasicAuthMethod && maybeBasicAuthMethod.basic != null;
|
|
300
325
|
}
|
|
301
326
|
|
|
302
|
-
// src/http/guards/isHmacMethod.ts
|
|
303
|
-
function isHmacMethod(maybeHmacMethod) {
|
|
304
|
-
return typeof maybeHmacMethod === "object" && maybeHmacMethod !== null && "hmac" in maybeHmacMethod && maybeHmacMethod.hmac != null;
|
|
305
|
-
}
|
|
306
|
-
|
|
307
327
|
// src/http/guards/isJwtAuthMethod.ts
|
|
308
328
|
function isJwtAuthMethod(maybeJwtAuthMethod) {
|
|
309
329
|
return typeof maybeJwtAuthMethod === "object" && maybeJwtAuthMethod !== null && "jwt" in maybeJwtAuthMethod && maybeJwtAuthMethod.jwt != null;
|
|
@@ -433,26 +453,6 @@ function hasFeatureChecks(maybeFeatureAuth) {
|
|
|
433
453
|
return typeof maybeFeatureAuth === "object" && maybeFeatureAuth !== null && "requiredFeatures" in maybeFeatureAuth && Array.isArray(maybeFeatureAuth.requiredFeatures) && maybeFeatureAuth.requiredFeatures.length > 0;
|
|
434
454
|
}
|
|
435
455
|
|
|
436
|
-
// src/http/guards/hasPermissionChecks.ts
|
|
437
|
-
function hasPermissionChecks(maybePermissionedAuth) {
|
|
438
|
-
return typeof maybePermissionedAuth === "object" && maybePermissionedAuth !== null && ("allowedPermissions" in maybePermissionedAuth || "forbiddenPermissions" in maybePermissionedAuth);
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
// src/http/guards/hasRoleChecks.ts
|
|
442
|
-
function hasRoleChecks(maybeRoledAuth) {
|
|
443
|
-
if (typeof maybeRoledAuth !== "object" || maybeRoledAuth === null) {
|
|
444
|
-
return false;
|
|
445
|
-
}
|
|
446
|
-
const hasAllowedRoles = "allowedRoles" in maybeRoledAuth && maybeRoledAuth.allowedRoles instanceof Set && maybeRoledAuth.allowedRoles.size > 0;
|
|
447
|
-
const hasForbiddenRoles = "forbiddenRoles" in maybeRoledAuth && maybeRoledAuth.forbiddenRoles instanceof Set && maybeRoledAuth.forbiddenRoles.size > 0;
|
|
448
|
-
return hasAllowedRoles || hasForbiddenRoles;
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
// src/http/guards/hasScopeChecks.ts
|
|
452
|
-
function hasScopeChecks(maybePermissionedAuth) {
|
|
453
|
-
return typeof maybePermissionedAuth === "object" && maybePermissionedAuth !== null && "requiredScope" in maybePermissionedAuth && maybePermissionedAuth.requiredScope != null;
|
|
454
|
-
}
|
|
455
|
-
|
|
456
456
|
// src/http/guards/hasSubscriptionChecks.ts
|
|
457
457
|
function hasSubscriptionChecks(maybeSubscriptionAuth) {
|
|
458
458
|
return typeof maybeSubscriptionAuth === "object" && maybeSubscriptionAuth !== null && "requireActiveSubscription" in maybeSubscriptionAuth && maybeSubscriptionAuth.requireActiveSubscription === true;
|
|
@@ -1247,6 +1247,40 @@ function validateContractDetails(contractDetails, schemaValidator) {
|
|
|
1247
1247
|
if (!isHttpContractDetails(contractDetails) && !isPathParamHttpContractDetails(contractDetails)) {
|
|
1248
1248
|
throw new Error("Contract details are malformed for route definition");
|
|
1249
1249
|
}
|
|
1250
|
+
const access = contractDetails["access"];
|
|
1251
|
+
const auth = contractDetails["auth"];
|
|
1252
|
+
if (access != null) {
|
|
1253
|
+
const validAccess = ["public", "authenticated", "protected", "internal"];
|
|
1254
|
+
if (!validAccess.includes(access)) {
|
|
1255
|
+
throw new Error(
|
|
1256
|
+
`Route '${contractDetails.name}': invalid access level '${access}'. Must be one of: ${validAccess.join(", ")}`
|
|
1257
|
+
);
|
|
1258
|
+
}
|
|
1259
|
+
if (access === "public" && auth != null) {
|
|
1260
|
+
throw new Error(
|
|
1261
|
+
`Route '${contractDetails.name}': access 'public' cannot have auth configured`
|
|
1262
|
+
);
|
|
1263
|
+
}
|
|
1264
|
+
if (access === "protected") {
|
|
1265
|
+
if (!auth) {
|
|
1266
|
+
throw new Error(
|
|
1267
|
+
`Route '${contractDetails.name}': access 'protected' requires auth with roles, permissions, or scope`
|
|
1268
|
+
);
|
|
1269
|
+
}
|
|
1270
|
+
if (!hasPermissionChecks(auth) && !hasRoleChecks(auth) && !hasScopeChecks(auth)) {
|
|
1271
|
+
throw new Error(
|
|
1272
|
+
`Route '${contractDetails.name}': access 'protected' requires at least one of allowedRoles, forbiddenRoles, allowedPermissions, forbiddenPermissions, or requiredScope`
|
|
1273
|
+
);
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
if (access === "internal") {
|
|
1277
|
+
if (!auth || !isHmacMethod(auth)) {
|
|
1278
|
+
throw new Error(
|
|
1279
|
+
`Route '${contractDetails.name}': access 'internal' requires HMAC auth`
|
|
1280
|
+
);
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1250
1284
|
if (contractDetails.versions) {
|
|
1251
1285
|
const parserTypes = Object.values(contractDetails.versions).map(
|
|
1252
1286
|
(version) => discriminateBody(schemaValidator, version.body)?.parserType
|
|
@@ -4168,6 +4202,44 @@ function generateOpenApiSpecs(schemaValidator, serverUrls, serverDescriptions, a
|
|
|
4168
4202
|
);
|
|
4169
4203
|
}
|
|
4170
4204
|
|
|
4205
|
+
// src/http/telemetry/auditLogger.ts
|
|
4206
|
+
import { createHash } from "crypto";
|
|
4207
|
+
var AuditLogger = class {
|
|
4208
|
+
#otel;
|
|
4209
|
+
constructor(otel) {
|
|
4210
|
+
this.#otel = otel;
|
|
4211
|
+
}
|
|
4212
|
+
append(entry) {
|
|
4213
|
+
try {
|
|
4214
|
+
this.#otel.info(
|
|
4215
|
+
{
|
|
4216
|
+
"log.type": "audit",
|
|
4217
|
+
"audit.timestamp": entry.timestamp,
|
|
4218
|
+
"audit.userId": entry.userId ?? "",
|
|
4219
|
+
"audit.tenantId": entry.tenantId ?? "",
|
|
4220
|
+
"audit.route": entry.route,
|
|
4221
|
+
"audit.method": entry.method,
|
|
4222
|
+
"audit.bodyHash": entry.bodyHash,
|
|
4223
|
+
"audit.status": entry.status,
|
|
4224
|
+
"audit.duration": entry.duration,
|
|
4225
|
+
"audit.redactedFields": entry.redactedFields.join(","),
|
|
4226
|
+
"audit.eventType": entry.eventType,
|
|
4227
|
+
_meta: true
|
|
4228
|
+
},
|
|
4229
|
+
`audit:${entry.eventType} ${entry.method} ${entry.route} ${entry.status}`
|
|
4230
|
+
);
|
|
4231
|
+
} catch (error) {
|
|
4232
|
+
console.error("Failed to emit audit log via OTEL:", error);
|
|
4233
|
+
}
|
|
4234
|
+
}
|
|
4235
|
+
static hashBody(body) {
|
|
4236
|
+
if (body === void 0 || body === null || body === "") {
|
|
4237
|
+
return "";
|
|
4238
|
+
}
|
|
4239
|
+
return createHash("sha256").update(body).digest("hex");
|
|
4240
|
+
}
|
|
4241
|
+
};
|
|
4242
|
+
|
|
4171
4243
|
// src/http/telemetry/evaluateTelemetryOptions.ts
|
|
4172
4244
|
function evaluateTelemetryOptions(telemetryOptions) {
|
|
4173
4245
|
return {
|
|
@@ -4188,6 +4260,73 @@ function evaluateTelemetryOptions(telemetryOptions) {
|
|
|
4188
4260
|
function metricsDefinitions(metrics2) {
|
|
4189
4261
|
return metrics2;
|
|
4190
4262
|
}
|
|
4263
|
+
|
|
4264
|
+
// src/http/rateLimit/rateLimiter.ts
|
|
4265
|
+
var READ_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "OPTIONS"]);
|
|
4266
|
+
var RateLimiter = class {
|
|
4267
|
+
constructor(cache) {
|
|
4268
|
+
this.cache = cache;
|
|
4269
|
+
}
|
|
4270
|
+
/**
|
|
4271
|
+
* Check whether the given key is within its rate limit.
|
|
4272
|
+
*
|
|
4273
|
+
* The counter is read from the cache, incremented, and written back.
|
|
4274
|
+
* If the cache is unreachable the limiter **fails open** (allows the
|
|
4275
|
+
* request) so that a cache outage does not take down the service.
|
|
4276
|
+
*
|
|
4277
|
+
* @param key - The rate limit key (see {@link RateLimiter.buildKey}).
|
|
4278
|
+
* @param limit - Maximum number of requests allowed in the window.
|
|
4279
|
+
* @param windowMs - Window duration in milliseconds.
|
|
4280
|
+
*/
|
|
4281
|
+
async check(key, limit, windowMs) {
|
|
4282
|
+
try {
|
|
4283
|
+
const now = Date.now();
|
|
4284
|
+
let counter;
|
|
4285
|
+
try {
|
|
4286
|
+
const record = await this.cache.readRecord(key);
|
|
4287
|
+
counter = record.value;
|
|
4288
|
+
if (now >= counter.resetAt) {
|
|
4289
|
+
counter = { count: 0, resetAt: now + windowMs };
|
|
4290
|
+
}
|
|
4291
|
+
} catch {
|
|
4292
|
+
counter = { count: 0, resetAt: now + windowMs };
|
|
4293
|
+
}
|
|
4294
|
+
counter.count += 1;
|
|
4295
|
+
const ttl = Math.max(counter.resetAt - now, 1);
|
|
4296
|
+
await this.cache.putRecord({
|
|
4297
|
+
key,
|
|
4298
|
+
value: counter,
|
|
4299
|
+
ttlMilliseconds: ttl
|
|
4300
|
+
});
|
|
4301
|
+
const allowed = counter.count <= limit;
|
|
4302
|
+
const remaining = Math.max(limit - counter.count, 0);
|
|
4303
|
+
return { allowed, remaining, resetAt: counter.resetAt };
|
|
4304
|
+
} catch {
|
|
4305
|
+
console.warn(`[RateLimiter] Cache error for key "${key}". Failing open.`);
|
|
4306
|
+
return { allowed: true, remaining: limit, resetAt: 0 };
|
|
4307
|
+
}
|
|
4308
|
+
}
|
|
4309
|
+
/**
|
|
4310
|
+
* Build a rate limit key from request context parts.
|
|
4311
|
+
*
|
|
4312
|
+
* Format: `ratelimit:{tenantId}:{route}:{userId}:{operationType}`
|
|
4313
|
+
*
|
|
4314
|
+
* When `tenantId` or `userId` is `null`, the placeholder `"anon"` is used.
|
|
4315
|
+
*/
|
|
4316
|
+
static buildKey(parts) {
|
|
4317
|
+
const tenant = parts.tenantId ?? "anon";
|
|
4318
|
+
const user = parts.userId ?? "anon";
|
|
4319
|
+
return `ratelimit:${tenant}:${parts.route}:${user}:${parts.operationType}`;
|
|
4320
|
+
}
|
|
4321
|
+
/**
|
|
4322
|
+
* Determine whether an HTTP method is a read or write operation.
|
|
4323
|
+
*
|
|
4324
|
+
* GET, HEAD, and OPTIONS are reads; everything else is a write.
|
|
4325
|
+
*/
|
|
4326
|
+
static operationType(method) {
|
|
4327
|
+
return READ_METHODS.has(method.toUpperCase()) ? "read" : "write";
|
|
4328
|
+
}
|
|
4329
|
+
};
|
|
4191
4330
|
export {
|
|
4192
4331
|
ATTR_API_NAME,
|
|
4193
4332
|
ATTR_APPLICATION_ID,
|
|
@@ -4196,12 +4335,14 @@ export {
|
|
|
4196
4335
|
ATTR_HTTP_RESPONSE_STATUS_CODE,
|
|
4197
4336
|
ATTR_HTTP_ROUTE,
|
|
4198
4337
|
ATTR_SERVICE_NAME,
|
|
4338
|
+
AuditLogger,
|
|
4199
4339
|
ForklaunchExpressLikeApplication,
|
|
4200
4340
|
ForklaunchExpressLikeRouter,
|
|
4201
4341
|
HTTPStatuses,
|
|
4202
4342
|
OPENAPI_DEFAULT_VERSION,
|
|
4203
4343
|
OpenTelemetryCollector,
|
|
4204
4344
|
PinoLogger,
|
|
4345
|
+
RateLimiter,
|
|
4205
4346
|
createContext,
|
|
4206
4347
|
createHmacToken,
|
|
4207
4348
|
delete_,
|