@browserlessapi/inferlo 0.5.0 → 0.7.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 (138) hide show
  1. package/README.md +14 -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 +44 -3
  6. package/dist/ai/index.js +164 -9
  7. package/dist/ai/index.js.map +1 -1
  8. package/dist/ai/public.d.ts +6 -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/api-version.d.ts +32 -0
  15. package/dist/api-version.js +45 -0
  16. package/dist/api-version.js.map +1 -0
  17. package/dist/app.d.ts +60 -6
  18. package/dist/app.js +310 -66
  19. package/dist/app.js.map +1 -1
  20. package/dist/astro.d.ts +60 -0
  21. package/dist/astro.js +68 -0
  22. package/dist/astro.js.map +1 -0
  23. package/dist/audit.d.ts +57 -0
  24. package/dist/audit.js +99 -0
  25. package/dist/audit.js.map +1 -0
  26. package/dist/auth.d.ts +1 -0
  27. package/dist/auth.js +1 -0
  28. package/dist/auth.js.map +1 -1
  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/cache.d.ts +96 -0
  33. package/dist/cache.js +247 -0
  34. package/dist/cache.js.map +1 -0
  35. package/dist/cli/main.js +28 -7
  36. package/dist/cli/main.js.map +1 -1
  37. package/dist/client-ip.d.ts +36 -1
  38. package/dist/client-ip.js +80 -1
  39. package/dist/client-ip.js.map +1 -1
  40. package/dist/db/memory.js +2 -1
  41. package/dist/db/memory.js.map +1 -1
  42. package/dist/db/postgres.js +4 -1
  43. package/dist/db/postgres.js.map +1 -1
  44. package/dist/db/table.d.ts +37 -5
  45. package/dist/db/table.js +96 -15
  46. package/dist/db/table.js.map +1 -1
  47. package/dist/db/types.d.ts +2 -0
  48. package/dist/describe.d.ts +54 -2
  49. package/dist/describe.js +34 -3
  50. package/dist/describe.js.map +1 -1
  51. package/dist/events.d.ts +134 -0
  52. package/dist/events.js +196 -0
  53. package/dist/events.js.map +1 -0
  54. package/dist/fastify.d.ts +10 -2
  55. package/dist/fastify.js +22 -4
  56. package/dist/fastify.js.map +1 -1
  57. package/dist/h3.d.ts +76 -0
  58. package/dist/h3.js +61 -0
  59. package/dist/h3.js.map +1 -0
  60. package/dist/hapi.d.ts +45 -0
  61. package/dist/hapi.js +57 -0
  62. package/dist/hapi.js.map +1 -0
  63. package/dist/index.d.ts +25 -5
  64. package/dist/index.js +9 -1
  65. package/dist/index.js.map +1 -1
  66. package/dist/jobs.d.ts +70 -10
  67. package/dist/jobs.js +78 -13
  68. package/dist/jobs.js.map +1 -1
  69. package/dist/koa.d.ts +51 -0
  70. package/dist/koa.js +45 -0
  71. package/dist/koa.js.map +1 -0
  72. package/dist/lambda.d.ts +110 -0
  73. package/dist/lambda.js +169 -0
  74. package/dist/lambda.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/mcp.d.ts +1 -1
  79. package/dist/mcp.js +7 -3
  80. package/dist/mcp.js.map +1 -1
  81. package/dist/model.d.ts +23 -6
  82. package/dist/model.js +35 -6
  83. package/dist/model.js.map +1 -1
  84. package/dist/module.d.ts +52 -0
  85. package/dist/module.js +152 -0
  86. package/dist/module.js.map +1 -0
  87. package/dist/nextjs.d.ts +35 -0
  88. package/dist/nextjs.js +33 -0
  89. package/dist/nextjs.js.map +1 -0
  90. package/dist/openapi.d.ts +3 -0
  91. package/dist/openapi.js +12 -3
  92. package/dist/openapi.js.map +1 -1
  93. package/dist/permissions.d.ts +26 -0
  94. package/dist/permissions.js +78 -0
  95. package/dist/permissions.js.map +1 -0
  96. package/dist/rate-limit.d.ts +5 -3
  97. package/dist/rate-limit.js +3 -3
  98. package/dist/rate-limit.js.map +1 -1
  99. package/dist/react-router.d.ts +41 -0
  100. package/dist/react-router.js +42 -0
  101. package/dist/react-router.js.map +1 -0
  102. package/dist/redis-events.d.ts +27 -0
  103. package/dist/redis-events.js +163 -0
  104. package/dist/redis-events.js.map +1 -0
  105. package/dist/redis-jobs.d.ts +24 -0
  106. package/dist/redis-jobs.js +203 -0
  107. package/dist/redis-jobs.js.map +1 -0
  108. package/dist/redis.d.ts +30 -0
  109. package/dist/redis.js +106 -0
  110. package/dist/redis.js.map +1 -0
  111. package/dist/request-matcher.d.ts +4 -0
  112. package/dist/request-matcher.js +14 -0
  113. package/dist/request-matcher.js.map +1 -0
  114. package/dist/response-body.d.ts +6 -0
  115. package/dist/response-body.js +7 -0
  116. package/dist/response-body.js.map +1 -0
  117. package/dist/route.d.ts +50 -2
  118. package/dist/route.js +24 -0
  119. package/dist/route.js.map +1 -1
  120. package/dist/schema.d.ts +2 -2
  121. package/dist/serve.d.ts +26 -2
  122. package/dist/serve.js +111 -10
  123. package/dist/serve.js.map +1 -1
  124. package/dist/service-client.d.ts +91 -0
  125. package/dist/service-client.js +177 -0
  126. package/dist/service-client.js.map +1 -0
  127. package/dist/sessions.d.ts +74 -0
  128. package/dist/sessions.js +172 -0
  129. package/dist/sessions.js.map +1 -0
  130. package/dist/sveltekit.d.ts +72 -0
  131. package/dist/sveltekit.js +68 -0
  132. package/dist/sveltekit.js.map +1 -0
  133. package/dist/tracing.d.ts +122 -0
  134. package/dist/tracing.js +210 -0
  135. package/dist/tracing.js.map +1 -0
  136. package/llms.txt +8 -3
  137. package/package.json +78 -4
  138. package/templates/app/AGENTS.md +10 -6
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,96 @@
1
+ /**
2
+ * Application cache: ctx.cache in routes and jobs, app.cache everywhere else.
3
+ *
4
+ * const stats = await cache.getOrSet("stats:daily", { ttlMs: 60_000, tags: ["tickets"] }, () => computeStats(db));
5
+ * await cache.invalidate("tickets"); // after a ticket changes
6
+ *
7
+ * Values are stored as JSON, in memory (one process, the default) or in the app's Postgres database (shared by
8
+ * every instance), chosen with createApp({ cache: { store } }).
9
+ */
10
+ import type { Adapter } from "./db/types.ts";
11
+ export interface CacheConfig {
12
+ /**
13
+ * Where entries are kept. "memory" (default): in this process, for one instance and tests.
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.
16
+ */
17
+ store?: "memory" | "database" | "redis";
18
+ /** Most entries the memory store keeps; the least recently used go first. Default 10000. */
19
+ maxEntries?: number;
20
+ }
21
+ export interface CacheSetOptions {
22
+ /** How long the entry is kept, in milliseconds (at least 1, at most 365 days). */
23
+ ttlMs: number;
24
+ /** Tags for cache.invalidate(tag), for example ["tickets", `user:${user.id}`]. */
25
+ tags?: readonly string[];
26
+ }
27
+ export interface Cache {
28
+ /** Where entries are kept: "memory", "database" or "redis". */
29
+ readonly store: "memory" | "database" | "redis";
30
+ /** The cached value, or undefined when there is none or it expired. */
31
+ get<T = unknown>(key: string): Promise<T | undefined>;
32
+ /** Store a JSON value (not undefined; use null for "nothing") for ttlMs. Dates are stored as ISO strings. */
33
+ set(key: string, value: unknown, options: CacheSetOptions): Promise<void>;
34
+ /**
35
+ * The cached value, or else the result of compute(), stored for ttlMs. Concurrent calls for the same key in this
36
+ * process share one compute() call. Errors from compute() are thrown and nothing is stored.
37
+ */
38
+ getOrSet<T>(key: string, options: CacheSetOptions, compute: () => T | Promise<T>): Promise<T>;
39
+ /** Remove one entry. */
40
+ delete(key: string): Promise<void>;
41
+ /** Remove every entry stored with any of these tags. */
42
+ invalidate(tags: string | readonly string[]): Promise<void>;
43
+ /** Remove every entry. */
44
+ clear(): Promise<void>;
45
+ }
46
+ /** Where a Cache keeps its entries, as JSON text. */
47
+ export interface CacheStore {
48
+ get(key: string): Promise<string | undefined>;
49
+ set(key: string, json: string, ttlMs: number, tags: readonly string[]): Promise<void>;
50
+ delete(key: string): Promise<void>;
51
+ invalidate(tags: readonly string[]): Promise<void>;
52
+ clear(): Promise<void>;
53
+ }
54
+ /** Check createApp({ cache }) at startup. Raises IL6028. */
55
+ export declare function resolveCacheConfig(config: unknown): {
56
+ store: "memory" | "database" | "redis";
57
+ maxEntries: number;
58
+ };
59
+ /** A Cache over a store: checks keys, options and values, stores JSON, shares concurrent getOrSet calls. */
60
+ export declare function createCache(store: CacheStore, name: "memory" | "database" | "redis"): Cache;
61
+ /** Entries in this process, least recently used removed first once maxEntries is reached. */
62
+ export declare function memoryCacheStore(maxEntries?: number, now?: () => number): CacheStore;
63
+ /** Entries in the inferlo_cache table (created on first use), shared by every instance of the app. */
64
+ export declare function databaseCacheStore(adapter: Adapter): CacheStore;
65
+ /** The route option cache: route.get("/tickets/stats", { cache: { ttlMs: 60_000, tags: ["tickets"] } }). */
66
+ export interface RouteCache {
67
+ /** How long a response is kept, in milliseconds (at least 1, at most 365 days). */
68
+ ttlMs: number;
69
+ /**
70
+ * Who shares a cached response. "user" (default): each signed-in user gets their own (requests without a 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.
74
+ */
75
+ by?: "user" | "tenant" | "public";
76
+ /** Tags for ctx.cache.invalidate(tag), so a change removes the cached responses at once. */
77
+ tags?: readonly string[];
78
+ }
79
+ /** Check a route's cache option at startup. Raises IL6028. */
80
+ export declare function checkRouteCache(option: unknown, method: string, where: string): void;
81
+ /** A cached route response: the handler's JSON result and the headers it set itself. */
82
+ export interface CachedResponse {
83
+ body: unknown;
84
+ headers: [string, string][];
85
+ etag: string;
86
+ }
87
+ /**
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
90
+ * query (sorted, so ?a=1&b=2 and ?b=2&a=1 share an entry), hashed to a short key.
91
+ */
92
+ export declare function routeCacheKey(method: string, path: string, option: RouteCache, userId: string | undefined, url: URL, tenantId?: string | null): Promise<string>;
93
+ /** A weak ETag for a JSON body. */
94
+ export declare function etagFor(json: string): Promise<string>;
95
+ /** True when an If-None-Match header lists the ETag (or is *). */
96
+ export declare function etagMatches(ifNoneMatch: string | null, etag: string): boolean;
package/dist/cache.js ADDED
@@ -0,0 +1,247 @@
1
+ import { InferloError } from "./errors.js";
2
+ const MAX_TTL_MS = 365 * 86_400_000;
3
+ const bad = (message, fix) => new InferloError("IL6028", message, { fix });
4
+ /** Check createApp({ cache }) at startup. Raises IL6028. */
5
+ export function resolveCacheConfig(config) {
6
+ if (config === undefined)
7
+ return { store: "memory", maxEntries: 10_000 };
8
+ const c = config;
9
+ const fix = 'Use createApp({ cache: { store: "memory" } }) (the default, with an optional maxEntries), { store: "database" } or { store: "redis" }.';
10
+ if (typeof c !== "object" || c === null || Array.isArray(c))
11
+ throw bad("createApp({ cache }) must be an object.", fix);
12
+ for (const key of Object.keys(c))
13
+ if (key !== "store" && key !== "maxEntries")
14
+ throw bad(`Unknown cache option "${key}".`, fix);
15
+ const store = c.store ?? "memory";
16
+ if (store !== "memory" && store !== "database" && store !== "redis")
17
+ throw bad(`Invalid cache store ${JSON.stringify(store)}.`, fix);
18
+ const maxEntries = c.maxEntries ?? 10_000;
19
+ if (!Number.isInteger(maxEntries) || maxEntries < 1)
20
+ throw bad("cache maxEntries must be a whole number of at least 1.", fix);
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
+ return { store, maxEntries };
24
+ }
25
+ function checkKey(key) {
26
+ if (typeof key !== "string" || key.length === 0 || key.length > 512) {
27
+ throw bad(`A cache key must be a string of 1 to 512 characters, got ${typeof key === "string" ? `${key.length} characters` : typeof key}.`, 'Use a short, stable key such as `ticket:${id}`.');
28
+ }
29
+ }
30
+ function checkTags(tags) {
31
+ const list = typeof tags === "string" ? [tags] : tags;
32
+ if (!Array.isArray(list) || list.some((t) => typeof t !== "string" || t.length === 0 || t.length > 256)) {
33
+ throw bad("Cache tags must be strings of 1 to 256 characters.", 'Use tags: ["tickets", `user:${user.id}`] and cache.invalidate("tickets").');
34
+ }
35
+ return list;
36
+ }
37
+ function checkOptions(options) {
38
+ const o = options;
39
+ if (typeof o !== "object" || o === null || !Number.isInteger(o.ttlMs) || o.ttlMs < 1 || o.ttlMs > MAX_TTL_MS) {
40
+ throw bad("Cache entries need ttlMs, a whole number of milliseconds from 1 to 365 days.", 'Pass { ttlMs: 60_000 } (one minute), optionally with tags: ["tickets"].');
41
+ }
42
+ return { ttlMs: o.ttlMs, tags: o.tags === undefined ? [] : checkTags(o.tags) };
43
+ }
44
+ function toJSON(key, value) {
45
+ let json;
46
+ try {
47
+ json = JSON.stringify(value);
48
+ }
49
+ catch (error) {
50
+ throw bad(`The value for cache key "${key}" cannot be stored as JSON: ${error.message}`, "Cache plain objects, arrays, strings, numbers, booleans and null.");
51
+ }
52
+ if (json === undefined)
53
+ throw bad(`The value for cache key "${key}" is ${value === undefined ? "undefined" : typeof value}, which cannot be cached.`, "Cache null for \"nothing\", or a plain JSON value.");
54
+ return json;
55
+ }
56
+ /** A Cache over a store: checks keys, options and values, stores JSON, shares concurrent getOrSet calls. */
57
+ export function createCache(store, name) {
58
+ const running = new Map();
59
+ const cache = {
60
+ store: name,
61
+ async get(key) {
62
+ checkKey(key);
63
+ const json = await store.get(key);
64
+ return json === undefined ? undefined : JSON.parse(json);
65
+ },
66
+ async set(key, value, options) {
67
+ checkKey(key);
68
+ const { ttlMs, tags } = checkOptions(options);
69
+ await store.set(key, toJSON(key, value), ttlMs, tags);
70
+ },
71
+ async getOrSet(key, options, compute) {
72
+ checkKey(key);
73
+ const { ttlMs, tags } = checkOptions(options);
74
+ if (typeof compute !== "function")
75
+ throw bad("cache.getOrSet needs a function that computes the value.", "Use cache.getOrSet(key, { ttlMs }, () => compute()).");
76
+ const pending = running.get(key);
77
+ if (pending)
78
+ return pending;
79
+ const work = (async () => {
80
+ const json = await store.get(key);
81
+ if (json !== undefined)
82
+ return JSON.parse(json);
83
+ const value = await compute();
84
+ const text = toJSON(key, value);
85
+ await store.set(key, text, ttlMs, tags);
86
+ // Callers get the stored form, so a cache hit and a miss return the same thing (dates as strings).
87
+ return JSON.parse(text);
88
+ })();
89
+ running.set(key, work);
90
+ try {
91
+ return await work;
92
+ }
93
+ finally {
94
+ running.delete(key);
95
+ }
96
+ },
97
+ async delete(key) {
98
+ checkKey(key);
99
+ await store.delete(key);
100
+ },
101
+ async invalidate(tags) {
102
+ const list = checkTags(tags);
103
+ if (list.length)
104
+ await store.invalidate(list);
105
+ },
106
+ async clear() {
107
+ await store.clear();
108
+ },
109
+ };
110
+ return Object.freeze(cache);
111
+ }
112
+ /** Entries in this process, least recently used removed first once maxEntries is reached. */
113
+ export function memoryCacheStore(maxEntries = 10_000, now = Date.now) {
114
+ const entries = new Map();
115
+ return {
116
+ async get(key) {
117
+ const entry = entries.get(key);
118
+ if (!entry)
119
+ return undefined;
120
+ entries.delete(key);
121
+ if (entry.expiresAt <= now())
122
+ return undefined;
123
+ entries.set(key, entry); // most recently used last
124
+ return entry.json;
125
+ },
126
+ async set(key, json, ttlMs, tags) {
127
+ entries.delete(key);
128
+ entries.set(key, { json, expiresAt: now() + ttlMs, tags });
129
+ if (entries.size > maxEntries) {
130
+ const time = now();
131
+ for (const [k, v] of entries)
132
+ if (v.expiresAt <= time)
133
+ entries.delete(k);
134
+ for (const k of entries.keys()) {
135
+ if (entries.size <= maxEntries)
136
+ break;
137
+ entries.delete(k);
138
+ }
139
+ }
140
+ },
141
+ async delete(key) {
142
+ entries.delete(key);
143
+ },
144
+ async invalidate(tags) {
145
+ for (const [k, v] of entries)
146
+ if (v.tags.some((t) => tags.includes(t)))
147
+ entries.delete(k);
148
+ },
149
+ async clear() {
150
+ entries.clear();
151
+ },
152
+ };
153
+ }
154
+ /** Entries in the inferlo_cache table (created on first use), shared by every instance of the app. */
155
+ export function databaseCacheStore(adapter) {
156
+ const sql = adapter.sql?.bind(adapter);
157
+ if (!sql) {
158
+ throw bad(`cache store "database" needs a Postgres database, but the app uses the ${adapter.dialect} adapter.`, 'Use postgres() from "@browserlessapi/inferlo/postgres" for createApp({ db }), or cache: { store: "memory" }.');
159
+ }
160
+ let ready;
161
+ const create = () => sql('CREATE TABLE IF NOT EXISTS "inferlo_cache" ("key" text PRIMARY KEY, "value" text NOT NULL, "tags" text[] NOT NULL DEFAULT \'{}\', "expires_at" timestamptz NOT NULL)')
162
+ .then(() => sql('CREATE INDEX IF NOT EXISTS "inferlo_cache_tags" ON "inferlo_cache" USING gin ("tags")'))
163
+ .catch(
164
+ // Two instances creating the table at the same moment: the other one won.
165
+ () => sql('SELECT 1 FROM "inferlo_cache" LIMIT 1'));
166
+ const query = async (text, params = []) => {
167
+ ready ??= create().catch((error) => {
168
+ ready = undefined;
169
+ throw error;
170
+ });
171
+ await ready;
172
+ return sql(text, params);
173
+ };
174
+ let writes = 0;
175
+ return {
176
+ async get(key) {
177
+ const rows = await query('SELECT "value" FROM "inferlo_cache" WHERE "key" = $1 AND "expires_at" > now()', [key]);
178
+ return rows.length ? String(rows[0].value) : undefined;
179
+ },
180
+ async set(key, json, ttlMs, tags) {
181
+ await query(`INSERT INTO "inferlo_cache" ("key", "value", "tags", "expires_at") VALUES ($1, $2, $3::text[], now() + $4 * interval '1 millisecond')
182
+ ON CONFLICT ("key") DO UPDATE SET "value" = EXCLUDED."value", "tags" = EXCLUDED."tags", "expires_at" = EXCLUDED."expires_at"`, [key, json, [...tags], ttlMs]);
183
+ // Expired rows are never read; remove them now and then so the table does not grow without bound.
184
+ if (++writes % 100 === 0)
185
+ await query('DELETE FROM "inferlo_cache" WHERE "expires_at" <= now()');
186
+ },
187
+ async delete(key) {
188
+ await query('DELETE FROM "inferlo_cache" WHERE "key" = $1', [key]);
189
+ },
190
+ async invalidate(tags) {
191
+ await query('DELETE FROM "inferlo_cache" WHERE "tags" && $1::text[]', [[...tags]]);
192
+ },
193
+ async clear() {
194
+ await query('DELETE FROM "inferlo_cache"');
195
+ },
196
+ };
197
+ }
198
+ /** Check a route's cache option at startup. Raises IL6028. */
199
+ export function checkRouteCache(option, method, where) {
200
+ const o = option;
201
+ const problems = [];
202
+ if (method !== "GET")
203
+ problems.push("only GET routes can be cached");
204
+ if (typeof o !== "object" || o === null)
205
+ problems.push("it must be an object");
206
+ else {
207
+ for (const key of Object.keys(o))
208
+ if (key !== "ttlMs" && key !== "by" && key !== "tags")
209
+ problems.push(`unknown option "${key}"`);
210
+ if (!Number.isInteger(o.ttlMs) || o.ttlMs < 1 || o.ttlMs > MAX_TTL_MS)
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 !== "tenant" && o.by !== "public")
213
+ problems.push('by must be "user", "tenant" or "public"');
214
+ if (o.tags !== undefined && (!Array.isArray(o.tags) || o.tags.some((t) => typeof t !== "string" || t.length === 0 || t.length > 256)))
215
+ problems.push("tags must be strings of 1 to 256 characters");
216
+ }
217
+ if (problems.length) {
218
+ throw bad(`Invalid cache option on ${where}: ${problems.join(", ")}.`, 'Use route.get(path, { cache: { ttlMs: 60_000, by: "user", tags: ["tickets"] } }).');
219
+ }
220
+ }
221
+ const base64url = (bytes) => btoa(String.fromCharCode(...new Uint8Array(bytes))).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
222
+ async function sha256(text) {
223
+ return base64url(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(text)));
224
+ }
225
+ /**
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
228
+ * query (sorted, so ?a=1&b=2 and ?b=2&a=1 share an entry), hashed to a short key.
229
+ */
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}`;
233
+ const query = [...url.searchParams].sort(([a, x], [b, y]) => (a < b ? -1 : a > b ? 1 : x < y ? -1 : x > y ? 1 : 0));
234
+ return `route:${await sha256(JSON.stringify([method, path, who, url.pathname, query]))}`;
235
+ }
236
+ /** A weak ETag for a JSON body. */
237
+ export async function etagFor(json) {
238
+ return `W/"${(await sha256(json)).slice(0, 27)}"`;
239
+ }
240
+ /** True when an If-None-Match header lists the ETag (or is *). */
241
+ export function etagMatches(ifNoneMatch, etag) {
242
+ if (!ifNoneMatch)
243
+ return false;
244
+ const bare = (tag) => tag.trim().replace(/^W\//, "");
245
+ return ifNoneMatch.split(",").some((tag) => tag.trim() === "*" || bare(tag) === bare(etag));
246
+ }
247
+ //# sourceMappingURL=cache.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache.js","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":"AAUA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAiD3C,MAAM,UAAU,GAAG,GAAG,GAAG,UAAU,CAAC;AACpC,MAAM,GAAG,GAAG,CAAC,OAAe,EAAE,GAAW,EAAE,EAAE,CAAC,IAAI,YAAY,CAAC,QAAQ,EAAE,OAAO,EAAE,EAAE,GAAG,EAAE,CAAC,CAAC;AAE3F,4DAA4D;AAC5D,MAAM,UAAU,kBAAkB,CAAC,MAAe;IAChD,IAAI,MAAM,KAAK,SAAS;QAAE,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,EAAE,CAAC;IACzE,MAAM,CAAC,GAAG,MAAqB,CAAC;IAChC,MAAM,GAAG,GAAG,wIAAwI,CAAC;IACrJ,IAAI,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,KAAK,IAAI,IAAI,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC;QAAE,MAAM,GAAG,CAAC,yCAAyC,EAAE,GAAG,CAAC,CAAC;IACvH,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC;QAAE,IAAI,GAAG,KAAK,OAAO,IAAI,GAAG,KAAK,YAAY;YAAE,MAAM,GAAG,CAAC,yBAAyB,GAAG,IAAI,EAAE,GAAG,CAAC,CAAC;IAChI,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,IAAI,QAAQ,CAAC;IAClC,IAAI,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,UAAU,IAAI,KAAK,KAAK,OAAO;QAAE,MAAM,GAAG,CAAC,uBAAuB,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;IACrI,MAAM,UAAU,GAAG,CAAC,CAAC,UAAU,IAAI,MAAM,CAAC;IAC1C,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,UAAU,CAAC,IAAI,UAAU,GAAG,CAAC;QAAE,MAAM,GAAG,CAAC,wDAAwD,EAAE,GAAG,CAAC,CAAC;IAC9H,IAAI,KAAK,KAAK,QAAQ,IAAI,CAAC,CAAC,UAAU,KAAK,SAAS;QAAE,MAAM,GAAG,CAAC,oDAAoD,EAAE,uDAAuD,KAAK,SAAS,CAAC,CAAC;IAC7L,OAAO,EAAE,KAAK,EAAE,UAAU,EAAE,CAAC;AAC/B,CAAC;AAED,SAAS,QAAQ,CAAC,GAAY;IAC5B,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,CAAC,MAAM,KAAK,CAAC,IAAI,GAAG,CAAC,MAAM,GAAG,GAAG,EAAE,CAAC;QACpE,MAAM,GAAG,CAAC,4DAA4D,OAAO,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,MAAM,aAAa,CAAC,CAAC,CAAC,OAAO,GAAG,GAAG,EAAE,iDAAiD,CAAC,CAAC;IACjM,CAAC;AACH,CAAC;AAED,SAAS,SAAS,CAAC,IAAa;IAC9B,MAAM,IAAI,GAAG,OAAO,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IACtD,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,CAAC,MAAM,KAAK,CAAC,IAAI,CAAC,CAAC,MAAM,GAAG,GAAG,CAAC,EAAE,CAAC;QACxG,MAAM,GAAG,CAAC,oDAAoD,EAAE,2EAA2E,CAAC,CAAC;IAC/I,CAAC;IACD,OAAO,IAAyB,CAAC;AACnC,CAAC;AAED,SAAS,YAAY,CAAC,OAAgB;IACpC,MAAM,CAAC,GAAG,OAA0B,CAAC;IACrC,IAAI,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,KAAK,GAAG,CAAC,IAAI,CAAC,CAAC,KAAK,GAAG,UAAU,EAAE,CAAC;QAC7G,MAAM,GAAG,CAAC,8EAA8E,EAAE,yEAAyE,CAAC,CAAC;IACvK,CAAC;IACD,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC;AACjF,CAAC;AAED,SAAS,MAAM,CAAC,GAAW,EAAE,KAAc;IACzC,IAAI,IAAwB,CAAC;IAC7B,IAAI,CAAC;QACH,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;IAC/B,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,GAAG,CAAC,4BAA4B,GAAG,+BAAgC,KAAe,CAAC,OAAO,EAAE,EAAE,mEAAmE,CAAC,CAAC;IAC3K,CAAC;IACD,IAAI,IAAI,KAAK,SAAS;QAAE,MAAM,GAAG,CAAC,4BAA4B,GAAG,QAAQ,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,OAAO,KAAK,2BAA2B,EAAE,oDAAoD,CAAC,CAAC;IAC5M,OAAO,IAAI,CAAC;AACd,CAAC;AAED,4GAA4G;AAC5G,MAAM,UAAU,WAAW,CAAC,KAAiB,EAAE,IAAqC;IAClF,MAAM,OAAO,GAAG,IAAI,GAAG,EAA4B,CAAC;IACpD,MAAM,KAAK,GAAU;QACnB,KAAK,EAAE,IAAI;QACX,KAAK,CAAC,GAAG,CAAI,GAAW;YACtB,QAAQ,CAAC,GAAG,CAAC,CAAC;YACd,MAAM,IAAI,GAAG,MAAM,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;YAClC,OAAO,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAO,CAAC;QAClE,CAAC;QACD,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,EAAE,OAAO;YAC3B,QAAQ,CAAC,GAAG,CAAC,CAAC;YACd,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,YAAY,CAAC,OAAO,CAAC,CAAC;YAC9C,MAAM,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,CAAC;QACxD,CAAC;QACD,KAAK,CAAC,QAAQ,CAAI,GAAW,EAAE,OAAwB,EAAE,OAA6B;YACpF,QAAQ,CAAC,GAAG,CAAC,CAAC;YACd,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,YAAY,CAAC,OAAO,CAAC,CAAC;YAC9C,IAAI,OAAO,OAAO,KAAK,UAAU;gBAAE,MAAM,GAAG,CAAC,0DAA0D,EAAE,sDAAsD,CAAC,CAAC;YACjK,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;YACjC,IAAI,OAAO;gBAAE,OAAO,OAAqB,CAAC;YAC1C,MAAM,IAAI,GAAG,CAAC,KAAK,IAAI,EAAE;gBACvB,MAAM,IAAI,GAAG,MAAM,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;gBAClC,IAAI,IAAI,KAAK,SAAS;oBAAE,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAM,CAAC;gBACrD,MAAM,KAAK,GAAG,MAAM,OAAO,EAAE,CAAC;gBAC9B,MAAM,IAAI,GAAG,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;gBAChC,MAAM,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,CAAC,CAAC;gBACxC,mGAAmG;gBACnG,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAM,CAAC;YAC/B,CAAC,CAAC,EAAE,CAAC;YACL,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;YACvB,IAAI,CAAC;gBACH,OAAO,MAAM,IAAI,CAAC;YACpB,CAAC;oBAAS,CAAC;gBACT,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YACtB,CAAC;QACH,CAAC;QACD,KAAK,CAAC,MAAM,CAAC,GAAG;YACd,QAAQ,CAAC,GAAG,CAAC,CAAC;YACd,MAAM,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QAC1B,CAAC;QACD,KAAK,CAAC,UAAU,CAAC,IAAI;YACnB,MAAM,IAAI,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC;YAC7B,IAAI,IAAI,CAAC,MAAM;gBAAE,MAAM,KAAK,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC;QAChD,CAAC;QACD,KAAK,CAAC,KAAK;YACT,MAAM,KAAK,CAAC,KAAK,EAAE,CAAC;QACtB,CAAC;KACF,CAAC;IACF,OAAO,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAC9B,CAAC;AAED,6FAA6F;AAC7F,MAAM,UAAU,gBAAgB,CAAC,UAAU,GAAG,MAAM,EAAE,MAAoB,IAAI,CAAC,GAAG;IAChF,MAAM,OAAO,GAAG,IAAI,GAAG,EAAwE,CAAC;IAChG,OAAO;QACL,KAAK,CAAC,GAAG,CAAC,GAAG;YACX,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;YAC/B,IAAI,CAAC,KAAK;gBAAE,OAAO,SAAS,CAAC;YAC7B,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YACpB,IAAI,KAAK,CAAC,SAAS,IAAI,GAAG,EAAE;gBAAE,OAAO,SAAS,CAAC;YAC/C,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC,CAAC,0BAA0B;YACnD,OAAO,KAAK,CAAC,IAAI,CAAC;QACpB,CAAC;QACD,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI;YAC9B,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YACpB,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,GAAG,EAAE,GAAG,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;YAC3D,IAAI,OAAO,CAAC,IAAI,GAAG,UAAU,EAAE,CAAC;gBAC9B,MAAM,IAAI,GAAG,GAAG,EAAE,CAAC;gBACnB,KAAK,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,OAAO;oBAAE,IAAI,CAAC,CAAC,SAAS,IAAI,IAAI;wBAAE,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;gBACzE,KAAK,MAAM,CAAC,IAAI,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC;oBAC/B,IAAI,OAAO,CAAC,IAAI,IAAI,UAAU;wBAAE,MAAM;oBACtC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;gBACpB,CAAC;YACH,CAAC;QACH,CAAC;QACD,KAAK,CAAC,MAAM,CAAC,GAAG;YACd,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QACtB,CAAC;QACD,KAAK,CAAC,UAAU,CAAC,IAAI;YACnB,KAAK,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,OAAO;gBAAE,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC;oBAAE,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;QAC5F,CAAC;QACD,KAAK,CAAC,KAAK;YACT,OAAO,CAAC,KAAK,EAAE,CAAC;QAClB,CAAC;KACF,CAAC;AACJ,CAAC;AAED,sGAAsG;AACtG,MAAM,UAAU,kBAAkB,CAAC,OAAgB;IACjD,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC;IACvC,IAAI,CAAC,GAAG,EAAE,CAAC;QACT,MAAM,GAAG,CAAC,0EAA0E,OAAO,CAAC,OAAO,WAAW,EAAE,8GAA8G,CAAC,CAAC;IAClO,CAAC;IACD,IAAI,KAAmC,CAAC;IACxC,MAAM,MAAM,GAAG,GAAG,EAAE,CAClB,GAAG,CAAC,sKAAsK,CAAC;SACxK,IAAI,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,uFAAuF,CAAC,CAAC;SACxG,KAAK;IACJ,0EAA0E;IAC1E,GAAG,EAAE,CAAC,GAAG,CAAC,uCAAuC,CAAC,CACnD,CAAC;IACN,MAAM,KAAK,GAAG,KAAK,EAAE,IAAY,EAAE,SAAoB,EAAE,EAAE,EAAE;QAC3D,KAAK,KAAK,MAAM,EAAE,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE;YACjC,KAAK,GAAG,SAAS,CAAC;YAClB,MAAM,KAAK,CAAC;QACd,CAAC,CAAC,CAAC;QACH,MAAM,KAAK,CAAC;QACZ,OAAO,GAAG,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IAC3B,CAAC,CAAC;IACF,IAAI,MAAM,GAAG,CAAC,CAAC;IACf,OAAO;QACL,KAAK,CAAC,GAAG,CAAC,GAAG;YACX,MAAM,IAAI,GAAG,MAAM,KAAK,CAAC,+EAA+E,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;YACjH,OAAO,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAE,IAAI,CAAC,CAAC,CAAuB,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;QAChF,CAAC;QACD,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI;YAC9B,MAAM,KAAK,CACT;sIAC8H,EAC9H,CAAC,GAAG,EAAE,IAAI,EAAE,CAAC,GAAG,IAAI,CAAC,EAAE,KAAK,CAAC,CAC9B,CAAC;YACF,kGAAkG;YAClG,IAAI,EAAE,MAAM,GAAG,GAAG,KAAK,CAAC;gBAAE,MAAM,KAAK,CAAC,yDAAyD,CAAC,CAAC;QACnG,CAAC;QACD,KAAK,CAAC,MAAM,CAAC,GAAG;YACd,MAAM,KAAK,CAAC,8CAA8C,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;QACrE,CAAC;QACD,KAAK,CAAC,UAAU,CAAC,IAAI;YACnB,MAAM,KAAK,CAAC,wDAAwD,EAAE,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;QACrF,CAAC;QACD,KAAK,CAAC,KAAK;YACT,MAAM,KAAK,CAAC,6BAA6B,CAAC,CAAC;QAC7C,CAAC;KACF,CAAC;AACJ,CAAC;AAiBD,8DAA8D;AAC9D,MAAM,UAAU,eAAe,CAAC,MAAe,EAAE,MAAc,EAAE,KAAa;IAC5E,MAAM,CAAC,GAAG,MAAoB,CAAC;IAC/B,MAAM,QAAQ,GAAa,EAAE,CAAC;IAC9B,IAAI,MAAM,KAAK,KAAK;QAAE,QAAQ,CAAC,IAAI,CAAC,+BAA+B,CAAC,CAAC;IACrE,IAAI,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,KAAK,IAAI;QAAE,QAAQ,CAAC,IAAI,CAAC,sBAAsB,CAAC,CAAC;SAC1E,CAAC;QACJ,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC;YAAE,IAAI,GAAG,KAAK,OAAO,IAAI,GAAG,KAAK,IAAI,IAAI,GAAG,KAAK,MAAM;gBAAE,QAAQ,CAAC,IAAI,CAAC,mBAAmB,GAAG,GAAG,CAAC,CAAC;QAClI,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,KAAK,GAAG,CAAC,IAAI,CAAC,CAAC,KAAK,GAAG,UAAU;YAAE,QAAQ,CAAC,IAAI,CAAC,iEAAiE,CAAC,CAAC;QACxJ,IAAI,CAAC,CAAC,EAAE,KAAK,SAAS,IAAI,CAAC,CAAC,EAAE,KAAK,MAAM,IAAI,CAAC,CAAC,EAAE,KAAK,QAAQ,IAAI,CAAC,CAAC,EAAE,KAAK,QAAQ;YAAE,QAAQ,CAAC,IAAI,CAAC,yCAAyC,CAAC,CAAC;QAC9I,IAAI,CAAC,CAAC,IAAI,KAAK,SAAS,IAAI,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,CAAC,MAAM,KAAK,CAAC,IAAI,CAAC,CAAC,MAAM,GAAG,GAAG,CAAC,CAAC;YAAE,QAAQ,CAAC,IAAI,CAAC,6CAA6C,CAAC,CAAC;IACtM,CAAC;IACD,IAAI,QAAQ,CAAC,MAAM,EAAE,CAAC;QACpB,MAAM,GAAG,CAAC,2BAA2B,KAAK,KAAK,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,mFAAmF,CAAC,CAAC;IAC9J,CAAC;AACH,CAAC;AASD,MAAM,SAAS,GAAG,CAAC,KAAkB,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,YAAY,CAAC,GAAG,IAAI,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;AAEzJ,KAAK,UAAU,MAAM,CAAC,IAAY;IAChC,OAAO,SAAS,CAAC,MAAM,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,SAAS,EAAE,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAC1F,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,MAAc,EAAE,IAAY,EAAE,MAAkB,EAAE,MAA0B,EAAE,GAAQ,EAAE,QAAwB;IAClJ,MAAM,EAAE,GAAG,MAAM,CAAC,EAAE,IAAI,MAAM,CAAC;IAC/B,MAAM,GAAG,GAAG,EAAE,KAAK,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,KAAK,QAAQ,IAAI,QAAQ,CAAC,CAAC,CAAC,UAAU,QAAQ,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,QAAQ,MAAM,EAAE,CAAC;IACpJ,MAAM,KAAK,GAAG,CAAC,GAAG,GAAG,CAAC,YAAY,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACpH,OAAO,SAAS,MAAM,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,MAAM,EAAE,IAAI,EAAE,GAAG,EAAE,GAAG,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;AAC3F,CAAC;AAED,mCAAmC;AACnC,MAAM,CAAC,KAAK,UAAU,OAAO,CAAC,IAAY;IACxC,OAAO,MAAM,CAAC,MAAM,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC;AACpD,CAAC;AAED,kEAAkE;AAClE,MAAM,UAAU,WAAW,CAAC,WAA0B,EAAE,IAAY;IAClE,IAAI,CAAC,WAAW;QAAE,OAAO,KAAK,CAAC;IAC/B,MAAM,IAAI,GAAG,CAAC,GAAW,EAAE,EAAE,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;IAC7D,OAAO,WAAW,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,IAAI,EAAE,KAAK,GAAG,IAAI,IAAI,CAAC,GAAG,CAAC,KAAK,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;AAC9F,CAAC"}
package/dist/cli/main.js CHANGED
@@ -109,9 +109,24 @@ async function cmdDescribe(args) {
109
109
  const app = await loadApp(args);
110
110
  const d = app.describe();
111
111
  print(d, Boolean(args.flags.json), () => {
112
- const lines = [`${d.name} ${d.version} (inferlo ${d.inferlo}, db: ${d.database}, auth: ${d.auth ? "on" : "off"})`, "", "Models"];
112
+ const lines = [`${d.name} ${d.version} (inferlo ${d.inferlo}, db: ${d.database}, auth: ${d.auth ? "on" : "off"})`];
113
+ if (d.modules?.length) {
114
+ lines.push("", "Modules");
115
+ for (const m of d.modules) {
116
+ lines.push(` ${m.name}${m.imports.length ? ` imports: ${m.imports.join(", ")}` : ""}${m.description ? ` ${m.description}` : ""}`);
117
+ const parts = [
118
+ m.models.length ? `models: ${m.models.join(", ")}` : "",
119
+ m.routes.length ? `routes: ${m.routes.length}` : "",
120
+ m.jobs.length ? `jobs: ${m.jobs.join(", ")}` : "",
121
+ m.provides.length ? `provides: ${m.provides.join(", ")}` : "",
122
+ ].filter(Boolean);
123
+ if (parts.length)
124
+ lines.push(` ${parts.join("; ")}`);
125
+ }
126
+ }
127
+ lines.push("", "Models");
113
128
  for (const m of d.models) {
114
- lines.push(` ${m.name}`);
129
+ lines.push(` ${m.name}${m.module ? ` [${m.module}]` : ""}${m.tenant ? " (tenant-scoped)" : ""}`);
115
130
  for (const [k, f] of Object.entries(m.fields)) {
116
131
  const extra = [f.required ? "required" : "", f.unique ? "unique" : "", f.references ? `-> ${f.references}` : "", f.values ? f.values.join("|") : ""].filter(Boolean).join(", ");
117
132
  lines.push(` ${k.padEnd(16)} ${f.type}${extra ? ` (${extra})` : ""}`);
@@ -119,7 +134,7 @@ async function cmdDescribe(args) {
119
134
  }
120
135
  lines.push("", "Routes");
121
136
  for (const r of d.routes)
122
- lines.push(` ${r.method.padEnd(6)} ${r.path.padEnd(28)} auth=${r.auth}${r.tool ? ` tool=${r.tool}` : ""}${r.deprecated ? ` deprecated${r.deprecated !== true && r.deprecated.sunset ? ` (sunset ${r.deprecated.sunset})` : ""}` : ""}${r.name ? ` (${r.name})` : ""}`);
137
+ lines.push(` ${r.method.padEnd(6)} ${r.path.padEnd(28)} auth=${r.auth}${r.permission ? ` permission=${r.permission}` : ""}${r.cache ? ` cache=${r.cache.ttlMs}ms/${r.cache.by}` : ""}${r.tool ? ` tool=${r.tool}` : ""}${r.deprecated ? ` deprecated${r.deprecated !== true && r.deprecated.sunset ? ` (sunset ${r.deprecated.sunset})` : ""}` : ""}${r.name ? ` (${r.name})` : ""}${r.module ? ` [${r.module}]` : ""}`);
123
138
  if (d.jobs?.length) {
124
139
  lines.push("", `Jobs (queue: ${d.jobQueue})`);
125
140
  for (const j of d.jobs)
@@ -129,7 +144,13 @@ async function cmdDescribe(args) {
129
144
  }
130
145
  lines.push("", `AI: ${d.ai.configured ? Object.entries(d.ai.aliases).map(([a, ids]) => `${a} = ${ids.join(" > ")}`).join("; ") || "configured" : "not configured"}`);
131
146
  if (d.endpoints.openapi)
132
- lines.push(`OpenAPI: ${d.endpoints.openapi}`);
147
+ lines.push(`OpenAPI: ${d.endpoints.openapi}${d.versions?.some((v) => v.openapi) ? ` (per version: ${d.versions.map((v) => v.openapi).filter(Boolean).join(", ")})` : ""}`);
148
+ if (d.permissions?.length)
149
+ lines.push(`Permissions: ${d.permissions.map((p) => (p.roles ? `${p.name} (${p.roles.join(", ")})` : p.name)).join(", ")}`);
150
+ if (d.versions?.length)
151
+ lines.push(`API versions: ${d.versions.map((v) => v.name).join(", ")}`);
152
+ if (d.cache)
153
+ lines.push(`Cache: ${d.cache}`);
133
154
  if (d.endpoints.mcp)
134
155
  lines.push(`MCP: ${d.endpoints.mcp}`);
135
156
  if (d.endpoints.health)
@@ -311,9 +332,9 @@ async function cmdWorker(args) {
311
332
  fix: 'Define jobs with job("name", { input }).handle(fn) in src/jobs.ts and register them: createApp({ jobs: [jobs] }).',
312
333
  });
313
334
  }
314
- if (app.jobs.queue !== "database") {
315
- throw new InferloError("IL8008", `inferlo worker needs the database job queue, but the app keeps jobs in ${app.jobs.queue}, where a separate process cannot see them.`, {
316
- fix: 'Use createApp({ jobQueue: { store: "database" }, db: postgres({ url }) }), or run jobs in the server process with app.jobs.start().',
335
+ if (app.jobs.queue !== "database" && app.jobs.queue !== "redis") {
336
+ throw new InferloError("IL8008", `inferlo worker needs a shared job queue (database or Redis), but the app keeps jobs in ${app.jobs.queue}, where a separate process cannot see them.`, {
337
+ fix: 'Use createApp({ jobQueue: { store: "database" }, db: postgres({ url }) }) or createApp({ redis, jobQueue: { store: "redis" } }), or run jobs in the server process with app.jobs.start().',
317
338
  });
318
339
  }
319
340
  const say = (event, text) => console.log(json ? JSON.stringify({ ok: true, event, jobs: names, queue: app.jobs.queue }) : text);