@toapi/server 1.4.0 → 1.5.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.
@@ -1 +1 @@
1
- {"version":3,"file":"create-request-handler.d.ts","sourceRoot":"","sources":["../src/create-request-handler.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAClD,OAAO,KAAK,EAAE,IAAI,IAAI,QAAQ,EAAE,MAAM,eAAe,CAAC;AACtD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAE/C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AACrD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AAC7C,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAK9C,UAAU,OAAO;IACf,uCAAuC;IACvC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,kEAAkE;IAClE,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAWD,wBAAgB,oBAAoB,CAClC,GAAG,EAAE,aAAa,CAAC,MAAM,CAAC,QAAQ,EAAE,YAAY,CAAC,SAAS,CAAC,CAAC,CAAC,EAC7D,OAAO,GAAE,OAAY,SAmBF,OAAO,uBAuL3B;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAOrD;AAuGD,wBAAsB,cAAc,CAAC,IAAI,EACvC,OAAO,EAAE,OAAO,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,IAAI,CAAC,EACrC,GAAG,EAAE,QAAQ,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,IAAI,CAAC,mDAOnC"}
1
+ {"version":3,"file":"create-request-handler.d.ts","sourceRoot":"","sources":["../src/create-request-handler.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAClD,OAAO,KAAK,EAAE,IAAI,IAAI,QAAQ,EAAE,MAAM,eAAe,CAAC;AACtD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAE/C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AACrD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AAC7C,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAM9C,UAAU,OAAO;IACf,uCAAuC;IACvC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,kEAAkE;IAClE,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAWD,wBAAgB,oBAAoB,CAClC,GAAG,EAAE,aAAa,CAAC,MAAM,CAAC,QAAQ,EAAE,YAAY,CAAC,SAAS,CAAC,CAAC,CAAC,EAC7D,OAAO,GAAE,OAAY,SAmBF,OAAO,uBA0L3B;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAOrD;AAuGD,wBAAsB,cAAc,CAAC,IAAI,EACvC,OAAO,EAAE,OAAO,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,IAAI,CAAC,EACrC,GAAG,EAAE,QAAQ,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,IAAI,CAAC,mDAOnC"}
@@ -4,6 +4,7 @@ import { HttpError } from "@toapi/common";
4
4
  import { CookieStore } from "@toapi/common";
5
5
  import { generateOpenAPISchema } from "./openapi.js";
6
6
  import { streamRevalidatedTags } from "./revalidation-stream.js";
7
+ import { encodeTags } from "./hash-tags.js";
7
8
  const DEFAULT_TTL = 60 * 60 * 24 * 14;
8
9
  const authUsed = Symbol("TApi.authUsed");
9
10
  const headersSchema = z
@@ -102,6 +103,7 @@ export function createRequestHandler(api, options = {}) {
102
103
  }
103
104
  }
104
105
  }
106
+ await encodeTags(api.revalidationStreamConfig?.hashSecret, res);
105
107
  return res;
106
108
  }
107
109
  catch (error) {
@@ -130,6 +132,7 @@ export function createRequestHandler(api, options = {}) {
130
132
  errorLog(error);
131
133
  }
132
134
  }
135
+ await encodeTags(api.revalidationStreamConfig?.hashSecret, res);
133
136
  return res;
134
137
  }
135
138
  catch (error) {
@@ -162,6 +165,7 @@ export function createRequestHandler(api, options = {}) {
162
165
  errorLog(error);
163
166
  }
164
167
  }
168
+ await encodeTags(api.revalidationStreamConfig?.hashSecret, res);
165
169
  return res;
166
170
  }
167
171
  catch (error) {
@@ -0,0 +1,8 @@
1
+ import { TResponse, type MaybePromise } from "@toapi/common";
2
+ export declare function hashTag(te: TextEncoder, secret: string, tag: string): Promise<string>;
3
+ type Encode = (tags: string[]) => MaybePromise<Uint8Array>;
4
+ export declare function hashEncoder(secret: string): Encode;
5
+ export declare function textEncoder(): Encode;
6
+ export declare function encodeTags(secret: string | undefined, res: TResponse<unknown>): Promise<void>;
7
+ export {};
8
+ //# sourceMappingURL=hash-tags.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hash-tags.d.ts","sourceRoot":"","sources":["../src/hash-tags.ts"],"names":[],"mappings":"AAAA,OAAO,EAAe,SAAS,EAAE,KAAK,YAAY,EAAE,MAAM,eAAe,CAAC;AAE1E,wBAAsB,OAAO,CAAC,EAAE,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,mBAKzE;AAED,KAAK,MAAM,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,YAAY,CAAC,UAAU,CAAC,CAAC;AAE3D,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAUlD;AAED,wBAAgB,WAAW,IAAI,MAAM,CAOpC;AAED,wBAAsB,UAAU,CAC9B,MAAM,EAAE,MAAM,GAAG,SAAS,EAC1B,GAAG,EAAE,SAAS,CAAC,OAAO,CAAC,iBAYxB"}
@@ -0,0 +1,34 @@
1
+ import { TAGS_HEADER, TResponse } from "@toapi/common";
2
+ export async function hashTag(te, secret, tag) {
3
+ const digest = await crypto.subtle.digest("SHA-256", te.encode(secret + tag));
4
+ const dv = new DataView(digest);
5
+ const num = dv.getBigUint64(0);
6
+ return num.toString(36);
7
+ }
8
+ export function hashEncoder(secret) {
9
+ const te = new TextEncoder();
10
+ return async (tags) => {
11
+ if (!tags.length)
12
+ return te.encode("\n");
13
+ const hashedTags = await Promise.all(tags.map((tag) => hashTag(te, secret, tag)));
14
+ const tagsString = hashedTags.join(" ");
15
+ return te.encode(tagsString + "\n");
16
+ };
17
+ }
18
+ export function textEncoder() {
19
+ const te = new TextEncoder();
20
+ return (tags) => {
21
+ if (!tags.length)
22
+ return te.encode("\n");
23
+ const tagsString = tags.join(" ");
24
+ return te.encode(tagsString + "\n");
25
+ };
26
+ }
27
+ export async function encodeTags(secret, res) {
28
+ if (!secret)
29
+ return;
30
+ const tags = res.cache?.tags ?? res.headers.get(TAGS_HEADER)?.split(" ") ?? [];
31
+ const te = new TextEncoder();
32
+ const hashedTags = await Promise.all(tags.map((tag) => hashTag(te, secret, tag)));
33
+ res.headers.set(TAGS_HEADER, hashedTags.join(" "));
34
+ }
@@ -1,8 +1,10 @@
1
+ import { type MaybePromise } from "@toapi/common";
1
2
  import type { Cache } from "./cache.js";
2
3
  export interface RevalidationStreamConfig {
3
4
  throttleTimeout?: number;
4
5
  keepaliveInterval?: number;
5
- filter?: (req: Request) => (tag: string) => boolean;
6
+ filter?: (req: Request) => MaybePromise<(tag: string) => boolean>;
7
+ hashSecret?: string;
6
8
  }
7
9
  interface Options {
8
10
  cache: Cache;
@@ -1 +1 @@
1
- {"version":3,"file":"revalidation-stream.d.ts","sourceRoot":"","sources":["../src/revalidation-stream.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAKxC,MAAM,WAAW,wBAAwB;IACvC,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,MAAM,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;CACrD;AAED,UAAU,OAAO;IACf,KAAK,EAAE,KAAK,CAAC;IACb,MAAM,CAAC,EAAE,wBAAwB,CAAC;IAClC,GAAG,EAAE,OAAO,CAAC;CACd;AAED,wBAAgB,qBAAqB,CAAC,EAAE,KAAK,EAAE,MAAW,EAAE,GAAG,EAAE,EAAE,OAAO,YAwEzE"}
1
+ {"version":3,"file":"revalidation-stream.d.ts","sourceRoot":"","sources":["../src/revalidation-stream.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,YAAY,EAClB,MAAM,eAAe,CAAC;AACvB,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAMxC,MAAM,WAAW,wBAAwB;IACvC,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,MAAM,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,YAAY,CAAC,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC;IAClE,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,UAAU,OAAO;IACf,KAAK,EAAE,KAAK,CAAC;IACb,MAAM,CAAC,EAAE,wBAAwB,CAAC;IAClC,GAAG,EAAE,OAAO,CAAC;CACd;AAED,wBAAgB,qBAAqB,CAAC,EAAE,KAAK,EAAE,MAAW,EAAE,GAAG,EAAE,EAAE,OAAO,YAsEzE"}
@@ -1,19 +1,22 @@
1
- import { SESSION_COOKIE_NAME, TAGS_CONTENT_TYPE } from "@toapi/common";
1
+ import { SESSION_COOKIE_NAME, TAGS_CONTENT_TYPE, } from "@toapi/common";
2
+ import { hashEncoder, textEncoder } from "./hash-tags.js";
2
3
  const DEFAULT_KEEPALIVE_INTERVAL = 10 * 1000;
3
4
  const DEFAULT_THROTTLE_TIMEOUT = 500;
4
5
  export function streamRevalidatedTags({ cache, config = {}, req }) {
5
6
  const { throttleTimeout = DEFAULT_THROTTLE_TIMEOUT, keepaliveInterval = DEFAULT_KEEPALIVE_INTERVAL, filter = () => () => true, } = config;
6
- const filterTag = filter(req);
7
7
  const id = crypto.randomUUID();
8
8
  let interval = null;
9
9
  let timeout = null;
10
10
  let unsubscribe = () => { };
11
11
  const stream = new ReadableStream({
12
12
  async start(controller) {
13
+ const filterTag = await filter(req);
13
14
  let queue = new Set();
14
- const textEncoder = new TextEncoder();
15
+ const encode = config.hashSecret
16
+ ? hashEncoder(config.hashSecret)
17
+ : textEncoder();
15
18
  // subscribe to tag invalidations
16
- unsubscribe = cache.subscribe((tags, meta) => {
19
+ unsubscribe = cache.subscribe(async (tags, meta) => {
17
20
  // ignore our own invalidations
18
21
  if (meta &&
19
22
  typeof meta === "object" &&
@@ -27,10 +30,10 @@ export function streamRevalidatedTags({ cache, config = {}, req }) {
27
30
  }
28
31
  // send tags to client
29
32
  if (queue.size > 0 && !timeout) {
30
- controller.enqueue(textEncoder.encode(`${Array.from(queue).join(" ")}\n`));
33
+ controller.enqueue(await encode(Array.from(queue)));
31
34
  queue = new Set();
32
- timeout = setTimeout(() => {
33
- controller.enqueue(textEncoder.encode(`${Array.from(queue).join(" ")}\n`));
35
+ timeout = setTimeout(async () => {
36
+ controller.enqueue(await encode(Array.from(queue)));
34
37
  queue = new Set();
35
38
  timeout = null;
36
39
  }, throttleTimeout);
@@ -39,7 +42,7 @@ export function streamRevalidatedTags({ cache, config = {}, req }) {
39
42
  // keepalive. The first one is sent right away so the response headers
40
43
  // are flushed immediately instead of only once the interval first
41
44
  // fires — consumers skip empty lines, so this is a no-op for them.
42
- const keepalive = () => controller.enqueue(textEncoder.encode("\n"));
45
+ const keepalive = () => controller.enqueue(new Uint8Array([10]));
43
46
  keepalive();
44
47
  interval = setInterval(keepalive, keepaliveInterval);
45
48
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@toapi/server",
3
- "version": "1.4.0",
3
+ "version": "1.5.0",
4
4
  "author": {
5
5
  "name": "Michel Smola",
6
6
  "email": "michel.smola@farbenmeer.de"
@@ -20,7 +20,7 @@
20
20
  }
21
21
  },
22
22
  "dependencies": {
23
- "@toapi/common": "^1.4.0",
23
+ "@toapi/common": "^1.5.0",
24
24
  "zod-openapi": "^6.0.0"
25
25
  },
26
26
  "devDependencies": {