@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.
@@ -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.identifier = identifier;
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.client = buildClientInfo(ip);
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.token = verified;
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 ?? ((context) => resolveKey(context.state));
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
- notify(oldest);
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: (value) => value
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.body = body;
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 ?? ((id) => new Session(id));
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
- const control = {
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
- context.state.session = activeSession;
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.csrf = token;
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;