@orkestrel/middleware 0.0.5 → 0.0.6
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/src/core/index.cjs +79 -65
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +40 -23
- package/dist/src/core/index.d.ts +40 -23
- package/dist/src/core/index.js +79 -66
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +340 -254
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +51 -0
- package/dist/src/server/index.d.ts +51 -0
- package/dist/src/server/index.js +338 -255
- package/dist/src/server/index.js.map +1 -1
- package/package.json +24 -22
package/dist/src/core/index.cjs
CHANGED
|
@@ -306,6 +306,26 @@ function detectEncodings(candidates) {
|
|
|
306
306
|
return supported;
|
|
307
307
|
}
|
|
308
308
|
/**
|
|
309
|
+
* Compress bytes with the host-independent `CompressionStream` primitive.
|
|
310
|
+
*
|
|
311
|
+
* @param bytes - The uncompressed response bytes
|
|
312
|
+
* @param encoding - The negotiated actionable coding
|
|
313
|
+
* @returns The compressed bytes, or the original bytes when the platform
|
|
314
|
+
* stream unexpectedly has no readable body
|
|
315
|
+
*
|
|
316
|
+
* @example
|
|
317
|
+
* ```ts
|
|
318
|
+
* const bytes = new TextEncoder().encode('compress me')
|
|
319
|
+
* const compressed = await compressBytes(bytes, 'gzip')
|
|
320
|
+
* ```
|
|
321
|
+
*/
|
|
322
|
+
async function compressBytes(bytes, encoding) {
|
|
323
|
+
const source = new Response(bytes).body;
|
|
324
|
+
if (source === null) return bytes;
|
|
325
|
+
const compressed = await new Response(source.pipeThrough(new CompressionStream(encoding))).arrayBuffer();
|
|
326
|
+
return new Uint8Array(compressed);
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
309
329
|
* Whether a response is eligible for the compression/ETag buffering pipeline
|
|
310
330
|
* (ruling J) — the shared cheap-skip predicate both batteries apply before
|
|
311
331
|
* ever touching `response.arrayBuffer()`.
|
|
@@ -569,7 +589,7 @@ function isPreflight(method, headers) {
|
|
|
569
589
|
* ```
|
|
570
590
|
*/
|
|
571
591
|
function buildClientInfo(ip) {
|
|
572
|
-
return { ip };
|
|
592
|
+
return { ...ip !== void 0 ? { ip } : {} };
|
|
573
593
|
}
|
|
574
594
|
/**
|
|
575
595
|
* Constant-time string equality — `createCSRF`'s double-submit token
|
|
@@ -665,6 +685,34 @@ function restoreSession(value) {
|
|
|
665
685
|
return session;
|
|
666
686
|
}
|
|
667
687
|
//#endregion
|
|
688
|
+
//#region src/core/shapers.ts
|
|
689
|
+
/**
|
|
690
|
+
* The `@orkestrel/database` column shape for a
|
|
691
|
+
* {@link import('./types.js').SessionRow} table — pass as-is to
|
|
692
|
+
* `createDatabase({ tables: { sessions: sessionColumns } })` so an app
|
|
693
|
+
* declaring a durable session table never hand-writes the shape.
|
|
694
|
+
*
|
|
695
|
+
* @remarks
|
|
696
|
+
* `lastSeen`/`createdAt` are `integerShape({ min: 0 })` — the table validates
|
|
697
|
+
* them as integers, so
|
|
698
|
+
* {@link import('./stores/DatabaseSessionStore.js').DatabaseSessionStore}'s
|
|
699
|
+
* `now` clock must yield integer milliseconds (`Date.now()`, the implicit
|
|
700
|
+
* default `createSession` clock). A fractional clock (`performance.now()`)
|
|
701
|
+
* fails the write with a validation error; `MemorySessionStore` carries no
|
|
702
|
+
* such column shape and accepts a fractional clock without complaint.
|
|
703
|
+
*
|
|
704
|
+
* @example
|
|
705
|
+
* ```ts
|
|
706
|
+
* const db = createDatabase({ driver, tables: { sessions: sessionColumns } })
|
|
707
|
+
* ```
|
|
708
|
+
*/
|
|
709
|
+
var sessionColumns = {
|
|
710
|
+
id: (0, _orkestrel_contract.stringShape)(),
|
|
711
|
+
session: (0, _orkestrel_contract.jsonShape)(),
|
|
712
|
+
lastSeen: (0, _orkestrel_contract.integerShape)({ min: 0 }),
|
|
713
|
+
createdAt: (0, _orkestrel_contract.integerShape)({ min: 0 })
|
|
714
|
+
};
|
|
715
|
+
//#endregion
|
|
668
716
|
//#region src/core/stores/MemorySessionStore.ts
|
|
669
717
|
/**
|
|
670
718
|
* The default in-process {@link SessionStoreInterface} — a `Map`-backed store
|
|
@@ -742,8 +790,8 @@ var MemorySessionStore = class {
|
|
|
742
790
|
}
|
|
743
791
|
#expired(entry, now) {
|
|
744
792
|
return sessionExpired(entry, now, {
|
|
745
|
-
ttl: this.#ttl,
|
|
746
|
-
lifetime: this.#lifetime
|
|
793
|
+
...this.#ttl !== void 0 ? { ttl: this.#ttl } : {},
|
|
794
|
+
...this.#lifetime !== void 0 ? { lifetime: this.#lifetime } : {}
|
|
747
795
|
});
|
|
748
796
|
}
|
|
749
797
|
#reserve(now) {
|
|
@@ -816,8 +864,8 @@ var DatabaseSessionStore = class {
|
|
|
816
864
|
const row = await this.#table.get(id);
|
|
817
865
|
if (row === void 0) return void 0;
|
|
818
866
|
if (sessionExpired(row, now, {
|
|
819
|
-
ttl: this.#ttl,
|
|
820
|
-
lifetime: this.#lifetime
|
|
867
|
+
...this.#ttl !== void 0 ? { ttl: this.#ttl } : {},
|
|
868
|
+
...this.#lifetime !== void 0 ? { lifetime: this.#lifetime } : {}
|
|
821
869
|
})) {
|
|
822
870
|
await this.#table.remove(id);
|
|
823
871
|
return;
|
|
@@ -932,18 +980,12 @@ function createCompression(options) {
|
|
|
932
980
|
const threshold = options?.threshold ?? 1024;
|
|
933
981
|
const encodings = detectEncodings(options?.encodings ?? DEFAULT_COMPRESSION_ENCODINGS);
|
|
934
982
|
const filter = options?.filter;
|
|
935
|
-
const compress = async (bytes, encoding) => {
|
|
936
|
-
const source = new Response(bytes).body;
|
|
937
|
-
if (source === null) return bytes;
|
|
938
|
-
const compressed = await new Response(source.pipeThrough(new CompressionStream(encoding))).arrayBuffer();
|
|
939
|
-
return new Uint8Array(compressed);
|
|
940
|
-
};
|
|
941
983
|
return async (request, context, next) => {
|
|
942
984
|
return compressResponse(request, context, await next(), {
|
|
943
985
|
threshold,
|
|
944
|
-
filter,
|
|
986
|
+
...filter !== void 0 ? { filter } : {},
|
|
945
987
|
encodings,
|
|
946
|
-
compress
|
|
988
|
+
compress: compressBytes
|
|
947
989
|
});
|
|
948
990
|
};
|
|
949
991
|
}
|
|
@@ -989,7 +1031,7 @@ function createSecurity(options) {
|
|
|
989
1031
|
if (identifierEnabled) {
|
|
990
1032
|
const incoming = request.headers.get(DEFAULT_IDENTIFIER_HEADER);
|
|
991
1033
|
identifier = trust && incoming !== null && (0, _orkestrel_server.isValidRequestId)(incoming) ? incoming : crypto.randomUUID();
|
|
992
|
-
context.state
|
|
1034
|
+
Object.assign(context.state, { identifier });
|
|
993
1035
|
}
|
|
994
1036
|
const response = await next();
|
|
995
1037
|
response.headers.set("x-content-type-options", "nosniff");
|
|
@@ -1122,7 +1164,7 @@ function createForwarded(options) {
|
|
|
1122
1164
|
const trust = hasProxies ? { proxies: options.proxies } : { trusted: options.trusted };
|
|
1123
1165
|
return async (request, context, next) => {
|
|
1124
1166
|
const ip = resolveForwardedFor(request.headers.get("x-forwarded-for") ?? void 0, trust) ?? context.state.connection?.ip;
|
|
1125
|
-
context.state
|
|
1167
|
+
Object.assign(context.state, { client: buildClientInfo(ip) });
|
|
1126
1168
|
return next();
|
|
1127
1169
|
};
|
|
1128
1170
|
}
|
|
@@ -1196,7 +1238,7 @@ function createBearer(options) {
|
|
|
1196
1238
|
}
|
|
1197
1239
|
const verified = await (0, _orkestrel_server.verifyToken)(candidate, secret);
|
|
1198
1240
|
if (verified === void 0) throw new _orkestrel_server.HTTPError(401, "invalid token");
|
|
1199
|
-
context.state
|
|
1241
|
+
Object.assign(context.state, { token: verified });
|
|
1200
1242
|
return next();
|
|
1201
1243
|
};
|
|
1202
1244
|
}
|
|
@@ -1225,20 +1267,14 @@ function createLimiter(options) {
|
|
|
1225
1267
|
const max = options.max;
|
|
1226
1268
|
const window = options.window;
|
|
1227
1269
|
const capacity = options.capacity ?? 1e4;
|
|
1228
|
-
const deriveKey = options.key
|
|
1270
|
+
const deriveKey = options.key;
|
|
1229
1271
|
const message = options.message ?? "rate limit exceeded";
|
|
1230
1272
|
const clock = options.clock ?? Date.now;
|
|
1231
1273
|
const policy = options.policy ?? false;
|
|
1232
1274
|
const evict = options.evict;
|
|
1233
|
-
const notify = (evictedKey) => {
|
|
1234
|
-
if (evict === void 0) return;
|
|
1235
|
-
try {
|
|
1236
|
-
evict(evictedKey);
|
|
1237
|
-
} catch {}
|
|
1238
|
-
};
|
|
1239
1275
|
const buckets = /* @__PURE__ */ new Map();
|
|
1240
1276
|
return async (request, context, next) => {
|
|
1241
|
-
const key = deriveKey(context);
|
|
1277
|
+
const key = deriveKey === void 0 ? resolveKey(context.state) : deriveKey(context);
|
|
1242
1278
|
const now = clock();
|
|
1243
1279
|
let bucket = buckets.get(key);
|
|
1244
1280
|
if (bucket === void 0) {
|
|
@@ -1246,13 +1282,15 @@ function createLimiter(options) {
|
|
|
1246
1282
|
const oldest = buckets.keys().next().value;
|
|
1247
1283
|
if (oldest !== void 0) {
|
|
1248
1284
|
buckets.delete(oldest);
|
|
1249
|
-
|
|
1285
|
+
if (evict !== void 0) try {
|
|
1286
|
+
evict(oldest);
|
|
1287
|
+
} catch {}
|
|
1250
1288
|
}
|
|
1251
1289
|
}
|
|
1252
1290
|
bucket = {
|
|
1253
1291
|
budget: (0, _orkestrel_budget.createBudget)({
|
|
1254
1292
|
max,
|
|
1255
|
-
consume:
|
|
1293
|
+
consume: Number
|
|
1256
1294
|
}),
|
|
1257
1295
|
resetAt: now + window
|
|
1258
1296
|
};
|
|
@@ -1306,7 +1344,7 @@ function createLimiter(options) {
|
|
|
1306
1344
|
function createBody() {
|
|
1307
1345
|
return async (request, context, next) => {
|
|
1308
1346
|
const body = await context.body();
|
|
1309
|
-
context.state
|
|
1347
|
+
if (body !== void 0) Object.assign(context.state, { body });
|
|
1310
1348
|
const contentType = request.headers.get("content-type");
|
|
1311
1349
|
if (contentType !== null && contentType.toLowerCase().startsWith("application/json") && body === void 0) throw new _orkestrel_server.HTTPError(400, "invalid json");
|
|
1312
1350
|
return next();
|
|
@@ -1341,12 +1379,12 @@ function createSession(options) {
|
|
|
1341
1379
|
const transport = options.transport;
|
|
1342
1380
|
const clock = options.clock ?? Date.now;
|
|
1343
1381
|
const store = options.store ?? new MemorySessionStore({
|
|
1344
|
-
ttl: options.ttl,
|
|
1345
|
-
lifetime: options.lifetime,
|
|
1346
|
-
capacity: options.capacity,
|
|
1347
|
-
evict: options.evict
|
|
1382
|
+
...options.ttl !== void 0 ? { ttl: options.ttl } : {},
|
|
1383
|
+
...options.lifetime !== void 0 ? { lifetime: options.lifetime } : {},
|
|
1384
|
+
...options.capacity !== void 0 ? { capacity: options.capacity } : {},
|
|
1385
|
+
...options.evict !== void 0 ? { evict: options.evict } : {}
|
|
1348
1386
|
});
|
|
1349
|
-
const create = options.create
|
|
1387
|
+
const create = options.create;
|
|
1350
1388
|
const mint = options.mint;
|
|
1351
1389
|
const requireSession = options.require ?? false;
|
|
1352
1390
|
const ends = options.ends ?? false;
|
|
@@ -1363,18 +1401,19 @@ function createSession(options) {
|
|
|
1363
1401
|
if (session === void 0) {
|
|
1364
1402
|
if (mint !== void 0 ? await mint(context) : true) {
|
|
1365
1403
|
const id = crypto.randomUUID();
|
|
1366
|
-
session = create(id);
|
|
1404
|
+
session = create === void 0 ? new Session(id) : create(id);
|
|
1367
1405
|
minted = true;
|
|
1368
1406
|
} else if (requireSession) throw new _orkestrel_server.HTTPError(404, "session required");
|
|
1369
1407
|
}
|
|
1370
1408
|
let destroyed = false;
|
|
1371
1409
|
let regenerated;
|
|
1372
1410
|
const activeSession = session;
|
|
1373
|
-
if (activeSession !== void 0) {
|
|
1374
|
-
|
|
1411
|
+
if (activeSession !== void 0) Object.assign(context.state, {
|
|
1412
|
+
session: activeSession,
|
|
1413
|
+
control: {
|
|
1375
1414
|
regenerate() {
|
|
1376
1415
|
if (destroyed) return;
|
|
1377
|
-
const newSession = create(crypto.randomUUID());
|
|
1416
|
+
const newSession = create === void 0 ? new Session(crypto.randomUUID()) : create(crypto.randomUUID());
|
|
1378
1417
|
transferSessionData(activeSession, newSession);
|
|
1379
1418
|
regenerated = newSession;
|
|
1380
1419
|
},
|
|
@@ -1382,10 +1421,8 @@ function createSession(options) {
|
|
|
1382
1421
|
destroyed = true;
|
|
1383
1422
|
regenerated = void 0;
|
|
1384
1423
|
}
|
|
1385
|
-
}
|
|
1386
|
-
|
|
1387
|
-
context.state.control = control;
|
|
1388
|
-
}
|
|
1424
|
+
}
|
|
1425
|
+
});
|
|
1389
1426
|
const response = await next();
|
|
1390
1427
|
if (activeSession !== void 0) if (destroyed) {
|
|
1391
1428
|
await store.delete(activeSession.id);
|
|
@@ -1429,7 +1466,7 @@ function createCSRF(options) {
|
|
|
1429
1466
|
return async (request, context, next) => {
|
|
1430
1467
|
if (safe.includes(context.method)) {
|
|
1431
1468
|
const token = await (0, _orkestrel_server.signToken)(context.state.session?.id ?? crypto.randomUUID(), { secret });
|
|
1432
|
-
context.state
|
|
1469
|
+
Object.assign(context.state, { csrf: token });
|
|
1433
1470
|
const response = await next();
|
|
1434
1471
|
const secure = (0, _orkestrel_server.resolveSecure)(void 0, context.state.connection?.encrypted ?? false);
|
|
1435
1472
|
await (0, _orkestrel_server.writeSignedCookie)(response.headers, cookieName, token, secret, {
|
|
@@ -1587,30 +1624,6 @@ function createMemorySessionStore(options) {
|
|
|
1587
1624
|
return new MemorySessionStore(options);
|
|
1588
1625
|
}
|
|
1589
1626
|
/**
|
|
1590
|
-
* The `@orkestrel/database` column shape for a {@link SessionRow} table — pass
|
|
1591
|
-
* as-is to `createDatabase({ tables: { sessions: sessionColumns } })` so an
|
|
1592
|
-
* app declaring a durable session table never hand-writes the shape.
|
|
1593
|
-
*
|
|
1594
|
-
* @remarks
|
|
1595
|
-
* `lastSeen`/`createdAt` are `integerShape({ min: 0 })` — the table VALIDATES
|
|
1596
|
-
* them as integers, so `DatabaseSessionStore`'s `now` clock must yield
|
|
1597
|
-
* integer milliseconds (`Date.now()`, the implicit default `createSession`
|
|
1598
|
-
* clock). A fractional clock (`performance.now()`) fails the write with a
|
|
1599
|
-
* validation error; `MemorySessionStore` carries no such column shape and
|
|
1600
|
-
* accepts a fractional clock without complaint.
|
|
1601
|
-
*
|
|
1602
|
-
* @example
|
|
1603
|
-
* ```ts
|
|
1604
|
-
* const db = createDatabase({ driver, tables: { sessions: sessionColumns } })
|
|
1605
|
-
* ```
|
|
1606
|
-
*/
|
|
1607
|
-
var sessionColumns = {
|
|
1608
|
-
id: (0, _orkestrel_contract.stringShape)(),
|
|
1609
|
-
session: (0, _orkestrel_contract.jsonShape)(),
|
|
1610
|
-
lastSeen: (0, _orkestrel_contract.integerShape)({ min: 0 }),
|
|
1611
|
-
createdAt: (0, _orkestrel_contract.integerShape)({ min: 0 })
|
|
1612
|
-
};
|
|
1613
|
-
/**
|
|
1614
1627
|
* Create a {@link DatabaseSessionStore} as a {@link SessionStoreInterface} —
|
|
1615
1628
|
* the durable counterpart to `createMemorySessionStore`, over a caller-opened
|
|
1616
1629
|
* `@orkestrel/database` table (declare it with {@link sessionColumns}).
|
|
@@ -1668,6 +1681,7 @@ exports.buildClientInfo = buildClientInfo;
|
|
|
1668
1681
|
exports.buildRateLimitField = buildRateLimitField;
|
|
1669
1682
|
exports.buildRateLimitPolicyField = buildRateLimitPolicyField;
|
|
1670
1683
|
exports.buildRetryAfter = buildRetryAfter;
|
|
1684
|
+
exports.compressBytes = compressBytes;
|
|
1671
1685
|
exports.compressResponse = compressResponse;
|
|
1672
1686
|
exports.createBearer = createBearer;
|
|
1673
1687
|
exports.createBody = createBody;
|