@browserlessapi/inferlo 0.6.0 → 0.8.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 (140) hide show
  1. package/README.md +9 -5
  2. package/dist/ai/ai-sdk.d.ts +48 -0
  3. package/dist/ai/ai-sdk.js +235 -0
  4. package/dist/ai/ai-sdk.js.map +1 -0
  5. package/dist/ai/index.d.ts +37 -3
  6. package/dist/ai/index.js +96 -7
  7. package/dist/ai/index.js.map +1 -1
  8. package/dist/ai/public.d.ts +5 -1
  9. package/dist/ai/public.js +2 -0
  10. package/dist/ai/public.js.map +1 -1
  11. package/dist/ai/ui.d.ts +47 -0
  12. package/dist/ai/ui.js +33 -0
  13. package/dist/ai/ui.js.map +1 -0
  14. package/dist/app.d.ts +55 -4
  15. package/dist/app.js +439 -39
  16. package/dist/app.js.map +1 -1
  17. package/dist/astro.d.ts +60 -0
  18. package/dist/astro.js +68 -0
  19. package/dist/astro.js.map +1 -0
  20. package/dist/audit.d.ts +8 -1
  21. package/dist/audit.js +9 -2
  22. package/dist/audit.js.map +1 -1
  23. package/dist/auth.d.ts +1 -0
  24. package/dist/auth.js +1 -0
  25. package/dist/auth.js.map +1 -1
  26. package/dist/authjs.d.ts +70 -0
  27. package/dist/authjs.js +94 -0
  28. package/dist/authjs.js.map +1 -0
  29. package/dist/azure.d.ts +66 -0
  30. package/dist/azure.js +119 -0
  31. package/dist/azure.js.map +1 -0
  32. package/dist/better-auth.d.ts +89 -0
  33. package/dist/better-auth.js +75 -0
  34. package/dist/better-auth.js.map +1 -0
  35. package/dist/cache.d.ts +13 -9
  36. package/dist/cache.js +11 -9
  37. package/dist/cache.js.map +1 -1
  38. package/dist/cli/main.js +5 -5
  39. package/dist/cli/main.js.map +1 -1
  40. package/dist/client-ip.d.ts +41 -1
  41. package/dist/client-ip.js +85 -1
  42. package/dist/client-ip.js.map +1 -1
  43. package/dist/cross-site.d.ts +14 -0
  44. package/dist/cross-site.js +56 -0
  45. package/dist/cross-site.js.map +1 -0
  46. package/dist/db/memory.js +2 -1
  47. package/dist/db/memory.js.map +1 -1
  48. package/dist/db/postgres.js +23 -6
  49. package/dist/db/postgres.js.map +1 -1
  50. package/dist/db/table.d.ts +37 -5
  51. package/dist/db/table.js +96 -15
  52. package/dist/db/table.js.map +1 -1
  53. package/dist/db/types.d.ts +2 -0
  54. package/dist/describe.d.ts +23 -5
  55. package/dist/describe.js +20 -2
  56. package/dist/describe.js.map +1 -1
  57. package/dist/disk-storage.d.ts +6 -0
  58. package/dist/disk-storage.js +96 -0
  59. package/dist/disk-storage.js.map +1 -0
  60. package/dist/events.d.ts +134 -0
  61. package/dist/events.js +196 -0
  62. package/dist/events.js.map +1 -0
  63. package/dist/files.d.ts +23 -0
  64. package/dist/files.js +168 -0
  65. package/dist/files.js.map +1 -0
  66. package/dist/index.d.ts +26 -8
  67. package/dist/index.js +10 -2
  68. package/dist/index.js.map +1 -1
  69. package/dist/jobs.d.ts +56 -10
  70. package/dist/jobs.js +71 -13
  71. package/dist/jobs.js.map +1 -1
  72. package/dist/jwt.d.ts +83 -0
  73. package/dist/jwt.js +312 -0
  74. package/dist/jwt.js.map +1 -0
  75. package/dist/langchain.d.ts +58 -0
  76. package/dist/langchain.js +220 -0
  77. package/dist/langchain.js.map +1 -0
  78. package/dist/model.d.ts +23 -6
  79. package/dist/model.js +41 -7
  80. package/dist/model.js.map +1 -1
  81. package/dist/oidc.d.ts +70 -0
  82. package/dist/oidc.js +379 -0
  83. package/dist/oidc.js.map +1 -0
  84. package/dist/openapi.js +17 -2
  85. package/dist/openapi.js.map +1 -1
  86. package/dist/passport.d.ts +54 -0
  87. package/dist/passport.js +68 -0
  88. package/dist/passport.js.map +1 -0
  89. package/dist/postgres-js.d.ts +39 -0
  90. package/dist/postgres-js.js +71 -0
  91. package/dist/postgres-js.js.map +1 -0
  92. package/dist/rate-limit.d.ts +5 -3
  93. package/dist/rate-limit.js +3 -3
  94. package/dist/rate-limit.js.map +1 -1
  95. package/dist/redis-events.d.ts +27 -0
  96. package/dist/redis-events.js +163 -0
  97. package/dist/redis-events.js.map +1 -0
  98. package/dist/redis-jobs.d.ts +24 -0
  99. package/dist/redis-jobs.js +203 -0
  100. package/dist/redis-jobs.js.map +1 -0
  101. package/dist/redis-sockets.d.ts +24 -0
  102. package/dist/redis-sockets.js +75 -0
  103. package/dist/redis-sockets.js.map +1 -0
  104. package/dist/redis.d.ts +30 -0
  105. package/dist/redis.js +106 -0
  106. package/dist/redis.js.map +1 -0
  107. package/dist/response-body.d.ts +6 -0
  108. package/dist/response-body.js +7 -0
  109. package/dist/response-body.js.map +1 -0
  110. package/dist/route.d.ts +117 -3
  111. package/dist/route.js +27 -0
  112. package/dist/route.js.map +1 -1
  113. package/dist/schema.d.ts +23 -3
  114. package/dist/schema.js +70 -0
  115. package/dist/schema.js.map +1 -1
  116. package/dist/serve.d.ts +24 -1
  117. package/dist/serve.js +200 -8
  118. package/dist/serve.js.map +1 -1
  119. package/dist/service-client.d.ts +91 -0
  120. package/dist/service-client.js +177 -0
  121. package/dist/service-client.js.map +1 -0
  122. package/dist/sessions.d.ts +2 -0
  123. package/dist/sessions.js +36 -4
  124. package/dist/sessions.js.map +1 -1
  125. package/dist/sockets.d.ts +58 -0
  126. package/dist/sockets.js +111 -0
  127. package/dist/sockets.js.map +1 -0
  128. package/dist/storage.d.ts +47 -0
  129. package/dist/storage.js +123 -0
  130. package/dist/storage.js.map +1 -0
  131. package/dist/tracing.d.ts +7 -3
  132. package/dist/tracing.js +8 -2
  133. package/dist/tracing.js.map +1 -1
  134. package/dist/websocket.d.ts +81 -0
  135. package/dist/websocket.js +251 -0
  136. package/dist/websocket.js.map +1 -0
  137. package/llms.txt +2 -0
  138. package/package.json +77 -5
  139. package/templates/app/AGENTS.md +9 -5
  140. package/templates/app/test/notes.test.ts +2 -0
package/dist/authjs.js ADDED
@@ -0,0 +1,94 @@
1
+ import { checkCrossSite, originOf } from "./cross-site.js";
2
+ import { InferloError } from "./errors.js";
3
+ const fix = 'Pass Auth from "@auth/core" and your Auth.js configuration: authJsSession(Auth, { secret, providers: [...] }).';
4
+ const idFix = "Add the user id to the session in the Auth.js configuration: callbacks: { session: ({ session, token }) => ({ ...session, user: { ...session.user, id: token.sub } }) } (with the database strategy: ({ session, user }) => ({ ...session, user: { ...session.user, id: user.id } })), or pass authJsSession(Auth, config, { user: (session) => ... }).";
5
+ /** Whether the request carries a cookie named like Auth.js's session token (authjs or next-auth, chunked or not). */
6
+ function hasSessionCookie(req) {
7
+ const header = req.headers.get("cookie");
8
+ return header !== null && /(?:^|;\s*)[^=;]*session-token(?:\.\d+)?=/.test(header);
9
+ }
10
+ function defaultUser(session) {
11
+ const user = session.user ?? {};
12
+ if (typeof user.id !== "string" || !user.id)
13
+ throw new InferloError("IL6044", "The Auth.js session has no user id.", { fix: idFix });
14
+ const roles = Array.isArray(user.roles) ? user.roles.filter((r) => typeof r === "string") : [];
15
+ const out = { id: user.id, roles };
16
+ if (typeof user.email === "string")
17
+ out.email = user.email;
18
+ if (typeof user.name === "string")
19
+ out.name = user.name;
20
+ // Apps that register their own user type (Register) map the session with options.user.
21
+ return out;
22
+ }
23
+ /**
24
+ * Auth.js sessions as the app's auth: createApp({ auth: session, middleware: [session.middleware] }) with
25
+ * session = authJsSession(Auth, config). Routes get the user that options.user returns for the session.
26
+ */
27
+ export function authJsSession(Auth, config, options = {}) {
28
+ if (typeof Auth !== "function")
29
+ throw new InferloError("IL6043", 'authJsSession() needs Auth from "@auth/core" as its first argument.', { fix });
30
+ if (!config || typeof config !== "object" || Array.isArray(config) || !Array.isArray(config.providers)) {
31
+ throw new InferloError("IL6043", "authJsSession() needs the Auth.js configuration, with its providers, as its second argument.", { fix });
32
+ }
33
+ if (!options || typeof options !== "object" || Array.isArray(options))
34
+ throw new InferloError("IL6043", "authJsSession() options must be an object.", { fix });
35
+ for (const key of Object.keys(options)) {
36
+ if (!["user", "trustedOrigins", "roles", "tenant"].includes(key)) {
37
+ throw new InferloError("IL6043", `authJsSession() has no option "${key}".`, { fix: "Use user, trustedOrigins, roles or tenant." });
38
+ }
39
+ }
40
+ for (const key of ["user", "roles", "tenant"]) {
41
+ if (options[key] !== undefined && typeof options[key] !== "function")
42
+ throw new InferloError("IL6043", `authJsSession() ${key} must be a function.`, { fix });
43
+ }
44
+ const basePath = config.basePath === undefined ? "/auth" : config.basePath;
45
+ if (typeof basePath !== "string" || !basePath.startsWith("/") || !basePath.replace(/\/+$/, "")) {
46
+ throw new InferloError("IL6043", `Auth.js's basePath must be a path such as "/auth", got ${JSON.stringify(basePath)}.`, {
47
+ fix: 'Leave basePath out of the Auth.js configuration or set it to a path under the root such as "/auth", so the app\'s own routes stay reachable.',
48
+ });
49
+ }
50
+ const base = basePath.replace(/\/+$/, "");
51
+ if (options.trustedOrigins !== undefined && !Array.isArray(options.trustedOrigins)) {
52
+ throw new InferloError("IL6043", "authJsSession() trustedOrigins must be a list of origins.", { fix: 'Use trustedOrigins: ["https://admin.example.com"].' });
53
+ }
54
+ const trusted = new Set((options.trustedOrigins ?? []).map((value) => {
55
+ const origin = originOf(value);
56
+ if (!origin || origin !== String(value).replace(/\/+$/, "")) {
57
+ throw new InferloError("IL6043", `authJsSession() trustedOrigins: ${JSON.stringify(value)} is not an origin.`, { fix: 'Use origins without a path, such as "https://admin.example.com".' });
58
+ }
59
+ return origin;
60
+ }));
61
+ const toUser = options.user ?? defaultUser;
62
+ return {
63
+ basePath: base,
64
+ middleware: async ({ req, url }, next) => {
65
+ const path = url.pathname;
66
+ if (path === base || path.startsWith(base + "/"))
67
+ return Auth(req, config);
68
+ return next();
69
+ },
70
+ async resolve(req, { db, url }) {
71
+ if (!hasSessionCookie(req))
72
+ return null;
73
+ // Auth.js reads and decrypts its own cookie: ask its session endpoint, as its framework adapters do.
74
+ const headers = new Headers();
75
+ for (const name of ["cookie", "host", "x-forwarded-host", "x-forwarded-proto", "user-agent"]) {
76
+ const value = req.headers.get(name);
77
+ if (value !== null)
78
+ headers.set(name, value);
79
+ }
80
+ const response = await Auth(new Request(new URL(`${base}/session`, url), { headers }), config);
81
+ if (response.status !== 200) {
82
+ throw new InferloError("IL6044", `Auth.js answered ${response.status} for the session; check its configuration (secret, trustHost, basePath).`, { fix: "Fix the Auth.js configuration; its log says what is wrong." });
83
+ }
84
+ const session = (await response.json());
85
+ if (!session || !session.user)
86
+ return null;
87
+ checkCrossSite(req, url, trusted, hasSessionCookie, "authJsSession(Auth, config, { trustedOrigins: [...] })");
88
+ return (await toUser(session, { db, req })) ?? null;
89
+ },
90
+ ...(options.roles ? { roles: options.roles } : {}),
91
+ ...(options.tenant ? { tenant: options.tenant } : {}),
92
+ };
93
+ }
94
+ //# sourceMappingURL=authjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"authjs.js","sourceRoot":"","sources":["../src/authjs.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Inferlo on Azure Functions (Node.js programming model v4), without a runtime dependency on @azure/functions:
3
+ *
4
+ * // src/functions/inferlo.ts
5
+ * import { app as azure } from "@azure/functions";
6
+ * import { toAzureHandler } from "@browserlessapi/inferlo/azure";
7
+ * import { app } from "../app.ts";
8
+ *
9
+ * azure.setup({ enableHttpStream: true });
10
+ * azure.http("inferlo", {
11
+ * route: "{*path}",
12
+ * methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"],
13
+ * authLevel: "anonymous",
14
+ * handler: toAzureHandler(app, { prefix: "/api" }),
15
+ * });
16
+ *
17
+ * The handler runs the whole Inferlo app (routes, auth, validation, rate limits, OpenAPI and MCP) for HTTP
18
+ * trigger requests and answers with an HttpResponseInit. With enableHttpStream the answer (AI streams included)
19
+ * is streamed to the caller as the app writes it.
20
+ */
21
+ import type { App } from "./app.ts";
22
+ /** The parts of an Azure Functions HttpRequest (@azure/functions 4.x) that Inferlo reads. */
23
+ export interface AzureHttpRequest {
24
+ readonly method: string;
25
+ readonly url: string;
26
+ readonly headers: {
27
+ get(name: string): string | null;
28
+ entries?(): IterableIterator<[string, string]>;
29
+ } & Iterable<[string, string]>;
30
+ arrayBuffer(): Promise<ArrayBuffer>;
31
+ }
32
+ /** A cookie in the Azure Functions response format (Cookie from @azure/functions). */
33
+ export interface AzureCookie {
34
+ name: string;
35
+ value: string;
36
+ domain?: string;
37
+ path?: string;
38
+ expires?: Date | number;
39
+ secure?: boolean;
40
+ httpOnly?: boolean;
41
+ sameSite?: "Strict" | "Lax" | "None";
42
+ maxAge?: number;
43
+ }
44
+ /** The answer, an HttpResponseInit for @azure/functions 4.x. */
45
+ export interface AzureHttpResponseInit {
46
+ status: number;
47
+ headers: Record<string, string>;
48
+ cookies: AzureCookie[];
49
+ /** The answer's body stream (async iterable, which every @azure/functions 4.x version accepts as a body). */
50
+ body?: ReadableStream<Uint8Array> & AsyncIterable<Uint8Array>;
51
+ }
52
+ /** An Azure Functions HTTP trigger handler: handler: toAzureHandler(app, { prefix: "/api" }). */
53
+ export type AzureHttpHandler = (request: AzureHttpRequest, context?: unknown) => Promise<AzureHttpResponseInit>;
54
+ export interface AzureHandlerOptions {
55
+ /**
56
+ * A path prefix removed before the app routes the request. Azure Functions puts every HTTP function under
57
+ * "/api" unless host.json sets extensions.http.routePrefix, so use { prefix: "/api" } with the default host.json.
58
+ * Requests outside it keep their path.
59
+ */
60
+ prefix?: string;
61
+ }
62
+ /**
63
+ * An Azure Functions v4 HTTP trigger handler that runs a whole Inferlo app:
64
+ * azure.http("inferlo", { route: "{*path}", methods: [...], authLevel: "anonymous", handler: toAzureHandler(app, { prefix: "/api" }) }).
65
+ */
66
+ export declare function toAzureHandler(app: Pick<App<any>, "fetch">, options?: AzureHandlerOptions): AzureHttpHandler;
package/dist/azure.js ADDED
@@ -0,0 +1,119 @@
1
+ import { clientIps } from "./client-ip.js";
2
+ import { InferloError } from "./errors.js";
3
+ const fix = 'Pass the app from createApp: handler: toAzureHandler(app, { prefix: "/api" }), with route: "{*path}" on azure.http(...).';
4
+ /** The caller's address: the last X-Forwarded-For entry, which the Azure front end adds, without its port. */
5
+ function azureClientIp(forwarded) {
6
+ const last = forwarded?.split(",").pop()?.trim();
7
+ if (!last)
8
+ return undefined;
9
+ const bracketed = /^\[([^\]]+)\](?::\d+)?$/.exec(last);
10
+ if (bracketed)
11
+ return bracketed[1];
12
+ // IPv4 with a port ("203.0.113.9:51234"); a bare IPv6 address has more than one colon and no port.
13
+ const v4 = /^(\d{1,3}(?:\.\d{1,3}){3})(?::\d+)?$/.exec(last);
14
+ return v4 ? v4[1] : last;
15
+ }
16
+ async function toRequest(request, prefix) {
17
+ const headers = new Headers();
18
+ for (const [name, value] of request.headers)
19
+ headers.append(name, value);
20
+ const original = new URL(request.url);
21
+ // The Functions host runs behind a TLS front end: use the scheme and host the caller used.
22
+ const forwardedProto = headers.get("x-forwarded-proto")?.split(",")[0]?.trim();
23
+ const proto = forwardedProto === "https" || forwardedProto === "http" ? forwardedProto : original.protocol.slice(0, -1);
24
+ const host = headers.get("x-forwarded-host")?.split(",")[0]?.trim() || headers.get("host") || original.host;
25
+ let url;
26
+ try {
27
+ url = new URL(`${proto}://${host}`);
28
+ }
29
+ catch {
30
+ url = new URL(original.origin);
31
+ }
32
+ url.pathname = original.pathname;
33
+ url.search = original.search;
34
+ const path = url.pathname;
35
+ if (prefix && (path === prefix || path.startsWith(prefix + "/")))
36
+ url.pathname = path.slice(prefix.length) || "/";
37
+ const method = request.method.toUpperCase();
38
+ const body = method === "GET" || method === "HEAD" ? undefined : new Uint8Array(await request.arrayBuffer());
39
+ const result = new Request(url, { method, headers, body: body && body.length ? body : undefined });
40
+ const ip = azureClientIp(headers.get("x-forwarded-for"));
41
+ if (ip)
42
+ clientIps.set(result, ip);
43
+ return result;
44
+ }
45
+ const SAME_SITE = { strict: "Strict", lax: "Lax", none: "None" };
46
+ /** Parses a Set-Cookie header into the Cookie object Azure Functions sends (it would keep only one Set-Cookie header). */
47
+ function parseSetCookie(header) {
48
+ const [pair, ...attributes] = header.split(";");
49
+ const eq = pair.indexOf("=");
50
+ if (eq < 1)
51
+ return undefined;
52
+ const cookie = { name: pair.slice(0, eq).trim(), value: pair.slice(eq + 1).trim() };
53
+ for (const attribute of attributes) {
54
+ const at = attribute.indexOf("=");
55
+ const key = (at === -1 ? attribute : attribute.slice(0, at)).trim().toLowerCase();
56
+ const value = at === -1 ? "" : attribute.slice(at + 1).trim();
57
+ if (key === "domain" && value)
58
+ cookie.domain = value;
59
+ else if (key === "path" && value)
60
+ cookie.path = value;
61
+ else if (key === "expires" && value) {
62
+ const time = Date.parse(value);
63
+ if (!Number.isNaN(time))
64
+ cookie.expires = new Date(time);
65
+ }
66
+ else if (key === "max-age" && /^-?\d+$/.test(value))
67
+ cookie.maxAge = Number(value);
68
+ else if (key === "secure")
69
+ cookie.secure = true;
70
+ else if (key === "httponly")
71
+ cookie.httpOnly = true;
72
+ else if (key === "samesite" && SAME_SITE[value.toLowerCase()])
73
+ cookie.sameSite = SAME_SITE[value.toLowerCase()];
74
+ }
75
+ return cookie;
76
+ }
77
+ function toResponseInit(response, method) {
78
+ const headers = {};
79
+ for (const [name, value] of response.headers)
80
+ if (name !== "set-cookie")
81
+ headers[name] = value;
82
+ const cookies = [];
83
+ for (const header of response.headers.getSetCookie()) {
84
+ const cookie = parseSetCookie(header);
85
+ if (cookie)
86
+ cookies.push(cookie);
87
+ }
88
+ const init = { status: response.status, headers, cookies };
89
+ if (response.body && method !== "HEAD")
90
+ init.body = response.body;
91
+ return init;
92
+ }
93
+ /**
94
+ * An Azure Functions v4 HTTP trigger handler that runs a whole Inferlo app:
95
+ * azure.http("inferlo", { route: "{*path}", methods: [...], authLevel: "anonymous", handler: toAzureHandler(app, { prefix: "/api" }) }).
96
+ */
97
+ export function toAzureHandler(app, options = {}) {
98
+ if (!app || typeof app.fetch !== "function") {
99
+ throw new InferloError("IL5026", "toAzureHandler() needs an Inferlo app (the value returned by createApp).", { fix });
100
+ }
101
+ if (options === null ||
102
+ typeof options !== "object" ||
103
+ Object.keys(options).some((key) => key !== "prefix") ||
104
+ (options.prefix !== undefined && (typeof options.prefix !== "string" || !options.prefix.startsWith("/")))) {
105
+ throw new InferloError("IL5026", 'toAzureHandler() options are { prefix }, and prefix starts with /, such as "/api".', { fix });
106
+ }
107
+ const prefix = (options.prefix ?? "").replace(/\/+$/, "").replace(/\/{2,}/g, "/");
108
+ return async (request) => {
109
+ const r = request;
110
+ if (!r || typeof r.method !== "string" || typeof r.url !== "string" || typeof r.arrayBuffer !== "function" || !r.headers) {
111
+ throw new InferloError("IL5026", "The handler from toAzureHandler() received something that is not an Azure Functions HTTP request.", {
112
+ fix: "Use it as the handler of an HTTP trigger: azure.http(name, { route: \"{*path}\", handler: toAzureHandler(app, { prefix: \"/api\" }) }). Other triggers (queues, timers, blobs) need their own handler.",
113
+ });
114
+ }
115
+ const response = await app.fetch(await toRequest(request, prefix));
116
+ return toResponseInit(response, request.method.toUpperCase());
117
+ };
118
+ }
119
+ //# sourceMappingURL=azure.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"azure.js","sourceRoot":"","sources":["../src/azure.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Better Auth sessions as an Inferlo app's auth, without a runtime dependency on Better Auth:
3
+ *
4
+ * import { betterAuth } from "better-auth";
5
+ * import { betterAuthSession } from "@browserlessapi/inferlo/better-auth";
6
+ *
7
+ * export const auth = betterAuth({ database: pool, emailAndPassword: { enabled: true } });
8
+ * export const session = betterAuthSession(auth, {
9
+ * user: ({ user }) => ({ id: user.id, email: user.email, roles: user.role ? [user.role] : [] }),
10
+ * });
11
+ * createApp({ auth: session, middleware: [session.middleware], ... });
12
+ *
13
+ * session.middleware answers Better Auth's own endpoints (sign-up, sign-in, sign-out, OAuth callbacks) under its
14
+ * basePath, "/api/auth" by default. Inferlo routes see the signed-in user from Better Auth's session cookie (or its
15
+ * bearer plugin). Requests that change data (POST, PUT, PATCH, DELETE) with the session cookie are refused with
16
+ * IL3007 when a browser sends them from another site, so other sites cannot act for a signed-in user.
17
+ */
18
+ import type { AuthConfig, Middleware } from "./app.ts";
19
+ import type { AppDb, AppUser } from "./route.ts";
20
+ /** The user Better Auth returns with a session. Plugins add fields, such as role (admin plugin). */
21
+ export interface BetterAuthUser {
22
+ id: string;
23
+ email: string;
24
+ name: string;
25
+ emailVerified?: boolean;
26
+ image?: string | null;
27
+ role?: string | null;
28
+ [field: string]: unknown;
29
+ }
30
+ /** The session Better Auth returns. Plugins add fields, such as activeOrganizationId (organization plugin). */
31
+ export interface BetterAuthSessionData {
32
+ id: string;
33
+ userId: string;
34
+ expiresAt: Date;
35
+ activeOrganizationId?: string | null;
36
+ [field: string]: unknown;
37
+ }
38
+ /** The parts of a Better Auth instance (what betterAuth({ ... }) returns) that Inferlo uses. */
39
+ export interface BetterAuthLike {
40
+ handler(request: Request): Promise<Response>;
41
+ api: {
42
+ getSession(context: {
43
+ headers: Headers;
44
+ }): Promise<{
45
+ user: BetterAuthUser;
46
+ session: BetterAuthSessionData;
47
+ } | null | undefined>;
48
+ };
49
+ options: {
50
+ basePath?: string;
51
+ baseURL?: unknown;
52
+ trustedOrigins?: unknown;
53
+ advanced?: unknown;
54
+ };
55
+ }
56
+ export interface BetterAuthSessionOptions {
57
+ /**
58
+ * The app's user for a Better Auth session, or null to refuse it (for example a disabled account). Default:
59
+ * { id, email, name, roles }, with roles from the admin plugin's comma-separated user.role.
60
+ */
61
+ user?: (data: {
62
+ user: BetterAuthUser;
63
+ session: BetterAuthSessionData;
64
+ }, ctx: {
65
+ db: AppDb;
66
+ req: Request;
67
+ }) => AppUser | null | undefined | Promise<AppUser | null | undefined>;
68
+ /**
69
+ * Origins ("https://admin.example.com") whose pages may change data with the session cookie, besides the app's
70
+ * own. Default: Better Auth's trustedOrigins when it is a list, and the origin of its baseURL.
71
+ */
72
+ trustedOrigins?: string[];
73
+ /** Roles of a user, checked when a route uses auth: "<role>". Default: user.roles ?? []. */
74
+ roles?: (user: AppUser) => string[];
75
+ /** The tenant of a user, for tenant-scoped models. Default: user.tenantId. */
76
+ tenant?: (user: AppUser) => string | null | undefined;
77
+ }
78
+ /** createApp({ auth: session, middleware: [session.middleware] }) for a Better Auth instance. */
79
+ export interface BetterAuthSession extends AuthConfig {
80
+ /** Answers Better Auth's own endpoints under basePath with auth.handler; other requests go on to the app. */
81
+ middleware: Middleware;
82
+ /** The path Better Auth's endpoints are under, such as "/api/auth". */
83
+ basePath: string;
84
+ }
85
+ /**
86
+ * Better Auth sessions as the app's auth: createApp({ auth: session, middleware: [session.middleware] }) with
87
+ * session = betterAuthSession(auth). Routes get the user that options.user returns for the session.
88
+ */
89
+ export declare function betterAuthSession(auth: BetterAuthLike, options?: BetterAuthSessionOptions): BetterAuthSession;
@@ -0,0 +1,75 @@
1
+ import { checkCrossSite, originOf } from "./cross-site.js";
2
+ import { InferloError } from "./errors.js";
3
+ const fix = 'Pass the instance betterAuth({ ... }) returns: betterAuthSession(auth, { user: ({ user }) => ({ id: user.id, email: user.email }) }).';
4
+ function defaultUser({ user }) {
5
+ const roles = typeof user.role === "string" ? user.role.split(",").map((r) => r.trim()).filter(Boolean) : [];
6
+ // Apps that register their own user type (Register) map the session with options.user.
7
+ return { id: user.id, email: user.email, name: user.name, roles };
8
+ }
9
+ /** Whether the request carries a cookie named like Better Auth's session token (its prefix is configurable). */
10
+ function hasSessionCookie(req) {
11
+ const header = req.headers.get("cookie");
12
+ return header !== null && /(?:^|;\s*)(?:__Secure-|__Host-)?[^=;]*session_token=/.test(header);
13
+ }
14
+ /**
15
+ * Better Auth sessions as the app's auth: createApp({ auth: session, middleware: [session.middleware] }) with
16
+ * session = betterAuthSession(auth). Routes get the user that options.user returns for the session.
17
+ */
18
+ export function betterAuthSession(auth, options = {}) {
19
+ if (!auth || typeof auth !== "object" || typeof auth.handler !== "function" || typeof auth.api?.getSession !== "function" || !auth.options || typeof auth.options !== "object") {
20
+ throw new InferloError("IL6041", "betterAuthSession() needs a Better Auth instance.", { fix });
21
+ }
22
+ if (!options || typeof options !== "object" || Array.isArray(options))
23
+ throw new InferloError("IL6041", "betterAuthSession() options must be an object.", { fix });
24
+ for (const key of Object.keys(options)) {
25
+ if (!["user", "trustedOrigins", "roles", "tenant"].includes(key)) {
26
+ throw new InferloError("IL6041", `betterAuthSession() has no option "${key}".`, { fix: "Use user, trustedOrigins, roles or tenant." });
27
+ }
28
+ }
29
+ for (const key of ["user", "roles", "tenant"]) {
30
+ if (options[key] !== undefined && typeof options[key] !== "function")
31
+ throw new InferloError("IL6041", `betterAuthSession() ${key} must be a function.`, { fix });
32
+ }
33
+ const basePath = auth.options.basePath === undefined ? "/api/auth" : auth.options.basePath;
34
+ if (typeof basePath !== "string" || !basePath.startsWith("/") || !basePath.replace(/\/+$/, "")) {
35
+ throw new InferloError("IL6041", `Better Auth's basePath must be a path such as "/api/auth", got ${JSON.stringify(basePath)}.`, { fix: 'Leave basePath out of betterAuth({ ... }) or set it to a path under the root such as "/api/auth", so the app\'s own routes stay reachable.' });
36
+ }
37
+ const base = basePath.replace(/\/+$/, "");
38
+ let trusted;
39
+ if (options.trustedOrigins !== undefined) {
40
+ if (!Array.isArray(options.trustedOrigins))
41
+ throw new InferloError("IL6041", "betterAuthSession() trustedOrigins must be a list of origins.", { fix: 'Use trustedOrigins: ["https://admin.example.com"].' });
42
+ trusted = options.trustedOrigins.map((value) => {
43
+ const origin = originOf(value);
44
+ if (!origin || origin !== value.replace(/\/+$/, "")) {
45
+ throw new InferloError("IL6041", `betterAuthSession() trustedOrigins: ${JSON.stringify(value)} is not an origin.`, { fix: 'Use origins without a path, such as "https://admin.example.com".' });
46
+ }
47
+ return origin;
48
+ });
49
+ }
50
+ else {
51
+ const fromAuth = Array.isArray(auth.options.trustedOrigins) ? auth.options.trustedOrigins : [];
52
+ trusted = [...fromAuth, auth.options.baseURL].map(originOf).filter((o) => o !== null);
53
+ }
54
+ const trustedSet = new Set(trusted);
55
+ const toUser = options.user ?? defaultUser;
56
+ return {
57
+ basePath: base,
58
+ middleware: async ({ req, url }, next) => {
59
+ const path = url.pathname;
60
+ if (path === base || path.startsWith(base + "/"))
61
+ return auth.handler(req);
62
+ return next();
63
+ },
64
+ async resolve(req, { db, url }) {
65
+ const found = await auth.api.getSession({ headers: req.headers });
66
+ if (!found || !found.user || !found.session)
67
+ return null;
68
+ checkCrossSite(req, url, trustedSet, hasSessionCookie, "betterAuthSession(auth, { trustedOrigins: [...] }) (or Better Auth's trustedOrigins)");
69
+ return (await toUser(found, { db, req })) ?? null;
70
+ },
71
+ ...(options.roles ? { roles: options.roles } : {}),
72
+ ...(options.tenant ? { tenant: options.tenant } : {}),
73
+ };
74
+ }
75
+ //# sourceMappingURL=better-auth.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"better-auth.js","sourceRoot":"","sources":["../src/better-auth.ts"],"names":[],"mappings":"AAkBA,OAAO,EAAE,cAAc,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAC3D,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AA4D3C,MAAM,GAAG,GAAG,uIAAuI,CAAC;AAEpJ,SAAS,WAAW,CAAC,EAAE,IAAI,EAA4B;IACrD,MAAM,KAAK,GAAG,OAAO,IAAI,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IAC7G,uFAAuF;IACvF,OAAO,EAAE,EAAE,EAAE,IAAI,CAAC,EAAE,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,KAAK,EAAa,CAAC;AAC/E,CAAC;AAED,gHAAgH;AAChH,SAAS,gBAAgB,CAAC,GAAY;IACpC,MAAM,MAAM,GAAG,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;IACzC,OAAO,MAAM,KAAK,IAAI,IAAI,sDAAsD,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;AAChG,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,iBAAiB,CAAC,IAAoB,EAAE,UAAoC,EAAE;IAC5F,IAAI,CAAC,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,OAAO,IAAI,CAAC,OAAO,KAAK,UAAU,IAAI,OAAO,IAAI,CAAC,GAAG,EAAE,UAAU,KAAK,UAAU,IAAI,CAAC,IAAI,CAAC,OAAO,IAAI,OAAO,IAAI,CAAC,OAAO,KAAK,QAAQ,EAAE,CAAC;QAC/K,MAAM,IAAI,YAAY,CAAC,QAAQ,EAAE,mDAAmD,EAAE,EAAE,GAAG,EAAE,CAAC,CAAC;IACjG,CAAC;IACD,IAAI,CAAC,OAAO,IAAI,OAAO,OAAO,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC;QAAE,MAAM,IAAI,YAAY,CAAC,QAAQ,EAAE,gDAAgD,EAAE,EAAE,GAAG,EAAE,CAAC,CAAC;IACnK,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;QACvC,IAAI,CAAC,CAAC,MAAM,EAAE,gBAAgB,EAAE,OAAO,EAAE,QAAQ,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;YACjE,MAAM,IAAI,YAAY,CAAC,QAAQ,EAAE,sCAAsC,GAAG,IAAI,EAAE,EAAE,GAAG,EAAE,4CAA4C,EAAE,CAAC,CAAC;QACzI,CAAC;IACH,CAAC;IACD,KAAK,MAAM,GAAG,IAAI,CAAC,MAAM,EAAE,OAAO,EAAE,QAAQ,CAAU,EAAE,CAAC;QACvD,IAAI,OAAO,CAAC,GAAG,CAAC,KAAK,SAAS,IAAI,OAAO,OAAO,CAAC,GAAG,CAAC,KAAK,UAAU;YAAE,MAAM,IAAI,YAAY,CAAC,QAAQ,EAAE,uBAAuB,GAAG,sBAAsB,EAAE,EAAE,GAAG,EAAE,CAAC,CAAC;IACpK,CAAC;IACD,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC;IAC3F,IAAI,OAAO,QAAQ,KAAK,QAAQ,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,EAAE,CAAC;QAC/F,MAAM,IAAI,YAAY,CAAC,QAAQ,EAAE,kEAAkE,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,GAAG,EAAE,EAAE,GAAG,EAAE,4IAA4I,EAAE,CAAC,CAAC;IACzR,CAAC;IACD,MAAM,IAAI,GAAG,QAAQ,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;IAE1C,IAAI,OAAiB,CAAC;IACtB,IAAI,OAAO,CAAC,cAAc,KAAK,SAAS,EAAE,CAAC;QACzC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,cAAc,CAAC;YAAE,MAAM,IAAI,YAAY,CAAC,QAAQ,EAAE,+DAA+D,EAAE,EAAE,GAAG,EAAE,oDAAoD,EAAE,CAAC,CAAC;QAC7M,OAAO,GAAG,OAAO,CAAC,cAAc,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE;YAC7C,MAAM,MAAM,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC;YAC/B,IAAI,CAAC,MAAM,IAAI,MAAM,KAAK,KAAK,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,EAAE,CAAC;gBACpD,MAAM,IAAI,YAAY,CAAC,QAAQ,EAAE,uCAAuC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,oBAAoB,EAAE,EAAE,GAAG,EAAE,kEAAkE,EAAE,CAAC,CAAC;YAClM,CAAC;YACD,OAAO,MAAM,CAAC;QAChB,CAAC,CAAC,CAAC;IACL,CAAC;SAAM,CAAC;QACN,MAAM,QAAQ,GAAG,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,cAAc,CAAC,CAAC,CAAC,EAAE,CAAC;QAC/F,OAAO,GAAG,CAAC,GAAG,QAAQ,EAAE,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAe,EAAE,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC;IACrG,CAAC;IACD,MAAM,UAAU,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,CAAC;IACpC,MAAM,MAAM,GAAG,OAAO,CAAC,IAAI,IAAI,WAAW,CAAC;IAE3C,OAAO;QACL,QAAQ,EAAE,IAAI;QACd,UAAU,EAAE,KAAK,EAAE,EAAE,GAAG,EAAE,GAAG,EAAE,EAAE,IAAI,EAAE,EAAE;YACvC,MAAM,IAAI,GAAG,GAAG,CAAC,QAAQ,CAAC;YAC1B,IAAI,IAAI,KAAK,IAAI,IAAI,IAAI,CAAC,UAAU,CAAC,IAAI,GAAG,GAAG,CAAC;gBAAE,OAAO,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;YAC3E,OAAO,IAAI,EAAE,CAAC;QAChB,CAAC;QACD,KAAK,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,EAAE,EAAE,GAAG,EAAE;YAC5B,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,EAAE,OAAO,EAAE,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC;YAClE,IAAI,CAAC,KAAK,IAAI,CAAC,KAAK,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO;gBAAE,OAAO,IAAI,CAAC;YACzD,cAAc,CAAC,GAAG,EAAE,GAAG,EAAE,UAAU,EAAE,gBAAgB,EAAE,sFAAsF,CAAC,CAAC;YAC/I,OAAO,CAAC,MAAM,MAAM,CAAC,KAAK,EAAE,EAAE,EAAE,EAAE,GAAG,EAAE,CAAC,CAAC,IAAI,IAAI,CAAC;QACpD,CAAC;QACD,GAAG,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAClD,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KACtD,CAAC;AACJ,CAAC"}
package/dist/cache.d.ts CHANGED
@@ -12,8 +12,9 @@ export interface CacheConfig {
12
12
  /**
13
13
  * Where entries are kept. "memory" (default): in this process, for one instance and tests.
14
14
  * "database": in the inferlo_cache table of the app's Postgres database, shared by every instance.
15
+ * "redis": in the Redis of createApp({ redis }), shared by every instance.
15
16
  */
16
- store?: "memory" | "database";
17
+ store?: "memory" | "database" | "redis";
17
18
  /** Most entries the memory store keeps; the least recently used go first. Default 10000. */
18
19
  maxEntries?: number;
19
20
  }
@@ -24,8 +25,8 @@ export interface CacheSetOptions {
24
25
  tags?: readonly string[];
25
26
  }
26
27
  export interface Cache {
27
- /** Where entries are kept: "memory" or "database". */
28
- readonly store: "memory" | "database";
28
+ /** Where entries are kept: "memory", "database" or "redis". */
29
+ readonly store: "memory" | "database" | "redis";
29
30
  /** The cached value, or undefined when there is none or it expired. */
30
31
  get<T = unknown>(key: string): Promise<T | undefined>;
31
32
  /** Store a JSON value (not undefined; use null for "nothing") for ttlMs. Dates are stored as ISO strings. */
@@ -52,11 +53,11 @@ export interface CacheStore {
52
53
  }
53
54
  /** Check createApp({ cache }) at startup. Raises IL6028. */
54
55
  export declare function resolveCacheConfig(config: unknown): {
55
- store: "memory" | "database";
56
+ store: "memory" | "database" | "redis";
56
57
  maxEntries: number;
57
58
  };
58
59
  /** A Cache over a store: checks keys, options and values, stores JSON, shares concurrent getOrSet calls. */
59
- export declare function createCache(store: CacheStore, name: "memory" | "database"): Cache;
60
+ export declare function createCache(store: CacheStore, name: "memory" | "database" | "redis"): Cache;
60
61
  /** Entries in this process, least recently used removed first once maxEntries is reached. */
61
62
  export declare function memoryCacheStore(maxEntries?: number, now?: () => number): CacheStore;
62
63
  /** Entries in the inferlo_cache table (created on first use), shared by every instance of the app. */
@@ -67,9 +68,11 @@ export interface RouteCache {
67
68
  ttlMs: number;
68
69
  /**
69
70
  * Who shares a cached response. "user" (default): each signed-in user gets their own (requests without a user
70
- * share one). "public": every caller gets the same response; only for data that does not depend on the user.
71
+ * share one). "tenant": the users of one tenant (organisation) share one, in multi-tenant apps (users without a
72
+ * tenant get their own). "public": every caller gets the same response; only for data that does not depend on
73
+ * the user.
71
74
  */
72
- by?: "user" | "public";
75
+ by?: "user" | "tenant" | "public";
73
76
  /** Tags for ctx.cache.invalidate(tag), so a change removes the cached responses at once. */
74
77
  tags?: readonly string[];
75
78
  }
@@ -82,10 +85,11 @@ export interface CachedResponse {
82
85
  etag: string;
83
86
  }
84
87
  /**
85
- * The cache key of one request to a cached route: the route, who shares the response and the full URL path and
88
+ * The cache key of one request to a cached route: the route, who shares the response (the user, the tenant or
89
+ * everyone) and the full URL path and
86
90
  * query (sorted, so ?a=1&b=2 and ?b=2&a=1 share an entry), hashed to a short key.
87
91
  */
88
- export declare function routeCacheKey(method: string, path: string, option: RouteCache, userId: string | undefined, url: URL): Promise<string>;
92
+ export declare function routeCacheKey(method: string, path: string, option: RouteCache, userId: string | undefined, url: URL, tenantId?: string | null): Promise<string>;
89
93
  /** A weak ETag for a JSON body. */
90
94
  export declare function etagFor(json: string): Promise<string>;
91
95
  /** True when an If-None-Match header lists the ETag (or is *). */
package/dist/cache.js CHANGED
@@ -6,20 +6,20 @@ export function resolveCacheConfig(config) {
6
6
  if (config === undefined)
7
7
  return { store: "memory", maxEntries: 10_000 };
8
8
  const c = config;
9
- const fix = 'Use createApp({ cache: { store: "memory" } }) (the default, with an optional maxEntries) or { store: "database" }.';
9
+ const fix = 'Use createApp({ cache: { store: "memory" } }) (the default, with an optional maxEntries), { store: "database" } or { store: "redis" }.';
10
10
  if (typeof c !== "object" || c === null || Array.isArray(c))
11
11
  throw bad("createApp({ cache }) must be an object.", fix);
12
12
  for (const key of Object.keys(c))
13
13
  if (key !== "store" && key !== "maxEntries")
14
14
  throw bad(`Unknown cache option "${key}".`, fix);
15
15
  const store = c.store ?? "memory";
16
- if (store !== "memory" && store !== "database")
16
+ if (store !== "memory" && store !== "database" && store !== "redis")
17
17
  throw bad(`Invalid cache store ${JSON.stringify(store)}.`, fix);
18
18
  const maxEntries = c.maxEntries ?? 10_000;
19
19
  if (!Number.isInteger(maxEntries) || maxEntries < 1)
20
20
  throw bad("cache maxEntries must be a whole number of at least 1.", fix);
21
- if (store === "database" && c.maxEntries !== undefined)
22
- throw bad("cache maxEntries only applies to the memory store.", 'Remove maxEntries from createApp({ cache: { store: "database" } }).');
21
+ if (store !== "memory" && c.maxEntries !== undefined)
22
+ throw bad("cache maxEntries only applies to the memory store.", `Remove maxEntries from createApp({ cache: { store: "${store}" } }).`);
23
23
  return { store, maxEntries };
24
24
  }
25
25
  function checkKey(key) {
@@ -209,8 +209,8 @@ export function checkRouteCache(option, method, where) {
209
209
  problems.push(`unknown option "${key}"`);
210
210
  if (!Number.isInteger(o.ttlMs) || o.ttlMs < 1 || o.ttlMs > MAX_TTL_MS)
211
211
  problems.push("ttlMs must be a whole number of milliseconds from 1 to 365 days");
212
- if (o.by !== undefined && o.by !== "user" && o.by !== "public")
213
- problems.push('by must be "user" or "public"');
212
+ if (o.by !== undefined && o.by !== "user" && o.by !== "tenant" && o.by !== "public")
213
+ problems.push('by must be "user", "tenant" or "public"');
214
214
  if (o.tags !== undefined && (!Array.isArray(o.tags) || o.tags.some((t) => typeof t !== "string" || t.length === 0 || t.length > 256)))
215
215
  problems.push("tags must be strings of 1 to 256 characters");
216
216
  }
@@ -223,11 +223,13 @@ async function sha256(text) {
223
223
  return base64url(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(text)));
224
224
  }
225
225
  /**
226
- * The cache key of one request to a cached route: the route, who shares the response and the full URL path and
226
+ * The cache key of one request to a cached route: the route, who shares the response (the user, the tenant or
227
+ * everyone) and the full URL path and
227
228
  * query (sorted, so ?a=1&b=2 and ?b=2&a=1 share an entry), hashed to a short key.
228
229
  */
229
- export async function routeCacheKey(method, path, option, userId, url) {
230
- const who = (option.by ?? "user") === "public" ? "public" : userId === undefined ? "anonymous" : `user:${userId}`;
230
+ export async function routeCacheKey(method, path, option, userId, url, tenantId) {
231
+ const by = option.by ?? "user";
232
+ const who = by === "public" ? "public" : by === "tenant" && tenantId ? `tenant:${tenantId}` : userId === undefined ? "anonymous" : `user:${userId}`;
231
233
  const query = [...url.searchParams].sort(([a, x], [b, y]) => (a < b ? -1 : a > b ? 1 : x < y ? -1 : x > y ? 1 : 0));
232
234
  return `route:${await sha256(JSON.stringify([method, path, who, url.pathname, query]))}`;
233
235
  }