@alisio/server 0.9.0 → 0.10.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 (64) hide show
  1. package/dist/index.d.ts +25 -0
  2. package/dist/index.js +40 -2
  3. package/dist/plugins/catalog-schema.d.ts +37 -0
  4. package/dist/plugins/catalog-schema.js +141 -0
  5. package/dist/plugins/catalog-service.d.ts +89 -0
  6. package/dist/plugins/catalog-service.js +306 -0
  7. package/dist/plugins/jobs.d.ts +41 -0
  8. package/dist/plugins/jobs.js +94 -0
  9. package/dist/plugins/sdk-version.d.ts +6 -0
  10. package/dist/plugins/sdk-version.js +28 -0
  11. package/dist/plugins/staging.d.ts +13 -0
  12. package/dist/plugins/staging.js +48 -0
  13. package/dist/routes/plugin-catalog.d.ts +103 -0
  14. package/dist/routes/plugin-catalog.js +436 -0
  15. package/dist/routes/server-restart.d.ts +7 -0
  16. package/dist/routes/server-restart.js +24 -0
  17. package/dist/web/assets/{AgentPicker-Dnt56QuW.js → AgentPicker-B2dNJHaW.js} +1 -1
  18. package/dist/web/assets/{AgentsModal-w9FC_Qnw.js → AgentsModal-BWrVQpCo.js} +1 -1
  19. package/dist/web/assets/{ArtifactPanel-x0WO1Jrn.js → ArtifactPanel-ZkBV87x7.js} +3 -3
  20. package/dist/web/assets/{BtwPanel-DCnyqvqF.js → BtwPanel-BtIHip-u.js} +1 -1
  21. package/dist/web/assets/{ChangelogDialog-B7_Imxij.js → ChangelogDialog-NBwCG0Bo.js} +1 -1
  22. package/dist/web/assets/{ContextTab-MaeId0Vq.js → ContextTab-Bi8MZwMV.js} +1 -1
  23. package/dist/web/assets/{CopyButton-C701fVen.js → CopyButton-Bh3Dsk-o.js} +1 -1
  24. package/dist/web/assets/{GoalBar-2u4r5hjt.js → GoalBar-DVwQsVtS.js} +1 -1
  25. package/dist/web/assets/{MemoryTab-MDHnMK7j.js → MemoryTab-CjZTbnFG.js} +1 -1
  26. package/dist/web/assets/{PermissionsMenu-Cn5x9qqW.js → PermissionsMenu-B03X019W.js} +1 -1
  27. package/dist/web/assets/{PlanReviewPanel-6Ke6aA-x.js → PlanReviewPanel-CM1uxJVs.js} +1 -1
  28. package/dist/web/assets/{PlanViewer-CNB31hZF.js → PlanViewer-DxAqn6IP.js} +1 -1
  29. package/dist/web/assets/{ProofCard-CGzCT48Z.js → ProofCard-B2nxuxaT.js} +1 -1
  30. package/dist/web/assets/{ScratchTab-DcTzefmZ.js → ScratchTab-BLkjPbJC.js} +1 -1
  31. package/dist/web/assets/SettingsModal-BJj7ELxX.js +3 -0
  32. package/dist/web/assets/{SpreadsheetPanel-SfCpgyiY.js → SpreadsheetPanel-BJLNla1t.js} +1 -1
  33. package/dist/web/assets/{TasksTab-B50lA0Fx.js → TasksTab-DhKGW5By.js} +1 -1
  34. package/dist/web/assets/{TextPreview-B63NbGfK.js → TextPreview-CcGkaf6g.js} +1 -1
  35. package/dist/web/assets/{TrajectoryTab-D2E8DaIt.js → TrajectoryTab-U37sw2D0.js} +1 -1
  36. package/dist/web/assets/{agents.module-CrFdY3Vj.js → agents.module-C5jXUIc1.js} +1 -1
  37. package/dist/web/assets/{app-BH2h7887.js → app-CGSX2FCK.js} +3 -3
  38. package/dist/web/assets/{es-sJS5auEJ.js → es-DP4ejr3u.js} +1 -1
  39. package/dist/web/assets/{goal-WebPe6na.js → goal-Cib1wj1f.js} +1 -1
  40. package/dist/web/assets/{i18n-CNPXQ8kr.js → i18n-DqOZSMVh.js} +2 -2
  41. package/dist/web/assets/{icons-D2ozMmbS.js → icons-BZSZMbqh.js} +1 -1
  42. package/dist/web/assets/{index-BF8cv-HX.js → index-CSQo6Bhi.js} +6 -6
  43. package/dist/web/assets/{jsxRuntime-BwIKcin2.js → jsxRuntime-BWhXelbu.js} +1 -1
  44. package/dist/web/assets/{panel-DF57sVn-.js → panel-IZ40pPYp.js} +1 -1
  45. package/dist/web/assets/settings-BAW93knU.css +1 -0
  46. package/dist/web/assets/settings.module-ErkNzIk4.js +1 -0
  47. package/dist/web/assets/{shared-DM5LTOJl.js → shared-C2fqplXu.js} +1 -1
  48. package/dist/web/assets/{strings-CNoJKZqb.js → strings-BUuRkgRy.js} +1 -1
  49. package/dist/web/assets/{tasks-D13Pmq9A.js → tasks-Bw0p2a6l.js} +1 -1
  50. package/dist/web/assets/{view-UpLwcG_p.js → view-B95Eg9o2.js} +1 -1
  51. package/dist/web/assets/view-BeVPW5cE.js +1 -0
  52. package/dist/web/assets/{view-B-6zUlTX.js → view-BkR3NRaK.js} +1 -1
  53. package/dist/web/assets/{view-BeUe5oNy.js → view-C99qRBgc.js} +1 -1
  54. package/dist/web/assets/{view-CfyBjrdk.js → view-CI66xheW.js} +1 -1
  55. package/dist/web/assets/{view-BM0IB6Wb.js → view-CSAgn7-g.js} +1 -1
  56. package/dist/web/assets/{view-1mNrG8x0.js → view-DJAfpDXs.js} +1 -1
  57. package/dist/web/assets/{view-JquKQHkw.js → view-DrHx2F_3.js} +1 -1
  58. package/dist/web/assets/{view-CTmPqVpP.js → view-Du7qfIhE.js} +1 -1
  59. package/dist/web/index.html +7 -7
  60. package/package.json +4 -4
  61. package/dist/web/assets/SettingsModal-BEuZITsr.js +0 -1
  62. package/dist/web/assets/settings-DdWABXsT.css +0 -1
  63. package/dist/web/assets/settings.module-BClxMWCR.js +0 -1
  64. package/dist/web/assets/view-vG-2EBMR.js +0 -1
package/dist/index.d.ts CHANGED
@@ -3,6 +3,7 @@ import { type AppOptions } from "@alisio/core";
3
3
  import { type FolderPicker } from "./host/folder-picker.ts";
4
4
  import { type ServerAppOptions } from "./host/workspace-host.ts";
5
5
  import { type Logger } from "./log.ts";
6
+ import { type CatalogServiceOptions } from "./plugins/catalog-service.ts";
6
7
  export { isLoopbackHost } from "./auth/guard.ts";
7
8
  export { ApprovalBridge } from "./bridges/approval-bridge.ts";
8
9
  export { InteractionBridge } from "./bridges/interaction-bridge.ts";
@@ -12,6 +13,11 @@ export { HttpError } from "./http/errors.ts";
12
13
  export { contentSecurityPolicy, PLACEHOLDER_HTML } from "./http/static.ts";
13
14
  export { createLogger, type Logger, type LogLevel, logLevel } from "./log.ts";
14
15
  export { PROTOCOL_VERSION } from "./routes/health.ts";
16
+ /**
17
+ * Exit code of a host that restarts on request (`POST /api/server/restart`): its supervisor (the
18
+ * desktop app) takes it as "start me again", not as a crash. 75 is `EX_TEMPFAIL`.
19
+ */
20
+ export declare const RESTART_EXIT_CODE = 75;
15
21
  export interface ServerOptions {
16
22
  /** Bind address (default `127.0.0.1`). Anything but loopback requires `allowRemote`. */
17
23
  host?: string;
@@ -64,6 +70,25 @@ export interface ServerOptions {
64
70
  timeoutMs?: number;
65
71
  maxBytes?: number;
66
72
  };
73
+ /**
74
+ * Restart on request. `supervised` (default: env `ALISIO_SUPERVISED=1`, set by the desktop app)
75
+ * says a supervisor respawns this process when it exits with `RESTART_EXIT_CODE`. `exit`
76
+ * replaces the default (close the server, then `process.exit`) for tests.
77
+ */
78
+ restart?: {
79
+ supervised?: boolean;
80
+ exit?: (code: number) => void;
81
+ };
82
+ /**
83
+ * Plugin catalog knobs (the CLI leaves them alone; tests inject a fake catalog, npm and fetch):
84
+ * catalog URL (else `ALISIO_PLUGIN_CATALOG_URL`), cache TTL, fetch, clock and SDK version.
85
+ */
86
+ pluginCatalog?: CatalogServiceOptions & {
87
+ installRunner?: import("@alisio/core").InstallRunner;
88
+ sdkVersion?: string;
89
+ maxUploadBytes?: number;
90
+ jobTtlMs?: number;
91
+ };
67
92
  /** Background-task runtime knobs (notification timing, kill grace, limits): tests tune them. */
68
93
  tasks?: Pick<NonNullable<AppOptions["tasks"]>, "timing" | "sweepDelayMs" | "killGraceMs" | "maxPerProcess">;
69
94
  }
package/dist/index.js CHANGED
@@ -21,6 +21,8 @@ import { HttpError, toApiError } from "./http/errors.js";
21
21
  import { Router } from "./http/router.js";
22
22
  import { StaticAssets } from "./http/static.js";
23
23
  import { createLogger } from "./log.js";
24
+ import { CatalogService } from "./plugins/catalog-service.js";
25
+ import { installedSdkVersion } from "./plugins/sdk-version.js";
24
26
  import { registerAgentDefinitionRoutes } from "./routes/agent-definitions.js";
25
27
  import { registerApprovalRoutes } from "./routes/approvals.js";
26
28
  import { registerArtifactViewRoutes, VIEW_PREFIX, viewerBaseHeaders, } from "./routes/artifact-view.js";
@@ -37,11 +39,13 @@ import { registerGoalRoutes } from "./routes/goal.js";
37
39
  import { registerHealthRoutes } from "./routes/health.js";
38
40
  import { registerIconThemeRoutes } from "./routes/icon-theme.js";
39
41
  import { registerManagementRoutes, WorkspaceRecycler } from "./routes/management.js";
42
+ import { registerPluginCatalogRoutes } from "./routes/plugin-catalog.js";
40
43
  import { registerPluginViewRoutes } from "./routes/plugin-views.js";
41
44
  import { registerPromptRoutes } from "./routes/prompts.js";
42
45
  import { registerProofRoutes } from "./routes/proof.js";
43
46
  import { registerProviderRoutes } from "./routes/providers.js";
44
47
  import { registerScratchRoutes } from "./routes/scratch.js";
48
+ import { registerRestartRoutes } from "./routes/server-restart.js";
45
49
  import { registerSessionViewRoutes } from "./routes/session-views.js";
46
50
  import { registerSessionRoutes } from "./routes/sessions.js";
47
51
  import { registerSideQuestionRoutes } from "./routes/side-questions.js";
@@ -58,6 +62,11 @@ export { HttpError } from "./http/errors.js";
58
62
  export { contentSecurityPolicy, PLACEHOLDER_HTML } from "./http/static.js";
59
63
  export { createLogger, logLevel } from "./log.js";
60
64
  export { PROTOCOL_VERSION } from "./routes/health.js";
65
+ /**
66
+ * Exit code of a host that restarts on request (`POST /api/server/restart`): its supervisor (the
67
+ * desktop app) takes it as "start me again", not as a crash. 75 is `EX_TEMPFAIL`.
68
+ */
69
+ export const RESTART_EXIT_CODE = 75;
61
70
  const REQUEST_ID = /^[A-Za-z0-9_-]{1,64}$/;
62
71
  /** Security headers of §11.2 (the CSP comes from the served web build). */
63
72
  function securityHeaders(res, csp, api) {
@@ -108,6 +117,7 @@ export async function startServer(options = {}) {
108
117
  // itself, the user decides.
109
118
  new GoalStore(catalog.db).pauseOrphans(Date.now());
110
119
  let shutdown;
120
+ let runningServer;
111
121
  let sessions;
112
122
  const inflight = new InflightTracker();
113
123
  let recycler;
@@ -444,6 +454,31 @@ export async function startServer(options = {}) {
444
454
  registerTaskRoutes(router, { sessions });
445
455
  registerApprovalRoutes(router, { approvals, interactions });
446
456
  registerManagementRoutes(router, management, recycler);
457
+ const supervised = options.restart?.supervised ?? process.env.ALISIO_SUPERVISED === "1";
458
+ registerRestartRoutes(router, {
459
+ ...(base.readOnly ? { readOnly: true } : {}),
460
+ ...(supervised
461
+ ? {
462
+ restart: () => {
463
+ if (options.restart?.exit)
464
+ return options.restart.exit(RESTART_EXIT_CODE);
465
+ // After the response is out: stop cleanly, then leave for the supervisor to respawn.
466
+ setTimeout(() => void runningServer?.close().finally(() => process.exit(RESTART_EXIT_CODE)), 100);
467
+ },
468
+ }
469
+ : {}),
470
+ });
471
+ {
472
+ const { installRunner, sdkVersion, maxUploadBytes, jobTtlMs, ...catalogOptions } = options.pluginCatalog ?? {};
473
+ const resolvedSdk = sdkVersion ?? installedSdkVersion();
474
+ registerPluginCatalogRoutes(router, management, recycler, {
475
+ service: new CatalogService(catalogOptions),
476
+ ...(resolvedSdk ? { sdkVersion: resolvedSdk } : {}),
477
+ ...(installRunner ? { installRunner } : {}),
478
+ ...(maxUploadBytes ? { maxUploadBytes } : {}),
479
+ ...(jobTtlMs ? { jobTtlMs } : {}),
480
+ });
481
+ }
447
482
  registerAgentDefinitionRoutes(router, management, recycler);
448
483
  registerProviderRoutes(router, management, recycler);
449
484
  registerEventRoutes(router, {
@@ -511,7 +546,9 @@ export async function startServer(options = {}) {
511
546
  // Every write is JSON (it forces a CORS preflight) except the raw image upload, which
512
547
  // checks its own image content types (also non-safelisted, so also preflighted).
513
548
  const upload = method === "POST" &&
514
- (url.pathname === "/api/blobs" || /^\/api\/sessions\/[^/]+\/datasets$/.test(url.pathname));
549
+ (url.pathname === "/api/blobs" ||
550
+ url.pathname === "/api/plugins/local/inspect" ||
551
+ /^\/api\/sessions\/[^/]+\/datasets$/.test(url.pathname));
515
552
  if (!["GET", "HEAD", "OPTIONS"].includes(method) && !upload) {
516
553
  const type = String(req.headers["content-type"] ?? "")
517
554
  .split(";")[0]
@@ -566,7 +603,7 @@ export async function startServer(options = {}) {
566
603
  guard = new AuthGuard({ host, port, ...(options.token ? { token: options.token } : {}) });
567
604
  const displayHost = host === "0.0.0.0" || host === "::" ? "127.0.0.1" : host;
568
605
  const url = `http://${displayHost.includes(":") ? `[${displayHost}]` : displayHost}:${port}`;
569
- return {
606
+ runningServer = {
570
607
  host,
571
608
  port,
572
609
  url,
@@ -598,6 +635,7 @@ export async function startServer(options = {}) {
598
635
  return shutdown;
599
636
  },
600
637
  };
638
+ return runningServer;
601
639
  }
602
640
  /** Writes a JSON response. */
603
641
  export function sendJson(res, status, body) {
@@ -0,0 +1,37 @@
1
+ export declare const SUPPORTED_SCHEMA_VERSION = 1;
2
+ export interface CatalogPlugin {
3
+ id: string;
4
+ slug: string;
5
+ name: string;
6
+ package: string;
7
+ version: string;
8
+ description: string;
9
+ categories: string[];
10
+ tags: string[];
11
+ /** The plugin's declared `@alisio/sdk` range. */
12
+ sdk?: string;
13
+ author?: string;
14
+ license?: string;
15
+ homepage?: string;
16
+ npmUrl?: string;
17
+ repository?: string;
18
+ publishedAt?: string;
19
+ featured: boolean;
20
+ /** Where the cover is fetched from (on the catalog origin); never sent to the browser. */
21
+ coverSource?: string;
22
+ }
23
+ export interface SanitizedCatalog {
24
+ generatedAt?: string;
25
+ categories: Array<{
26
+ id: string;
27
+ count: number;
28
+ }>;
29
+ plugins: CatalogPlugin[];
30
+ }
31
+ export declare function isLoopbackHostname(hostname: string): boolean;
32
+ /** A plain `https:` URL without credentials (loopback `http:` is accepted only for tests/dev). */
33
+ export declare function safeUrl(value: unknown, max?: number, allowHttpLoopback?: boolean): URL | undefined;
34
+ /** Whether `url` is on the same origin as the catalog document itself. */
35
+ export declare function sameOrigin(url: URL, catalogUrl: URL): boolean;
36
+ /** Validates a parsed catalog document; throws a short error for an unusable document. */
37
+ export declare function sanitizeCatalog(raw: unknown, catalogUrl: string): SanitizedCatalog;
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Validation and sanitization of the published plugin catalog (`catalog.json`). The catalog is
3
+ * remote, untrusted data: every field is type-checked and length-capped, links must be `https:`,
4
+ * the cover must live on the catalog's own origin, and an entry with an invalid npm name or
5
+ * version is dropped. Counts are recomputed, never trusted.
6
+ */
7
+ import { comparePluginVersions, parsePluginSpec } from "@alisio/core";
8
+ export const SUPPORTED_SCHEMA_VERSION = 1;
9
+ const MAX_PLUGINS = 500;
10
+ const SLUG_RE = /^[a-z0-9][a-z0-9._-]{0,63}$/;
11
+ const TAG_RE = /^[\p{L}\p{N}][\p{L}\p{N} ._+#/-]{0,39}$/u;
12
+ const isRecord = (value) => !!value && typeof value === "object" && !Array.isArray(value);
13
+ function text(value, max) {
14
+ if (typeof value !== "string")
15
+ return undefined;
16
+ // Control characters have no place in a catalog label.
17
+ const cleaned = value.replace(/[\u0000-\u001f\u007f]/g, " ").trim();
18
+ return cleaned.length > 0 && cleaned.length <= max ? cleaned : undefined;
19
+ }
20
+ export function isLoopbackHostname(hostname) {
21
+ return (hostname === "localhost" ||
22
+ hostname === "127.0.0.1" ||
23
+ hostname === "[::1]" ||
24
+ hostname === "::1");
25
+ }
26
+ /** A plain `https:` URL without credentials (loopback `http:` is accepted only for tests/dev). */
27
+ export function safeUrl(value, max = 500, allowHttpLoopback = false) {
28
+ if (typeof value !== "string" || value.length > max)
29
+ return undefined;
30
+ let url;
31
+ try {
32
+ url = new URL(value);
33
+ }
34
+ catch {
35
+ return undefined;
36
+ }
37
+ if (url.username || url.password)
38
+ return undefined;
39
+ if (url.protocol === "https:")
40
+ return url;
41
+ if (url.protocol === "http:" && allowHttpLoopback && isLoopbackHostname(url.hostname))
42
+ return url;
43
+ return undefined;
44
+ }
45
+ /** Whether `url` is on the same origin as the catalog document itself. */
46
+ export function sameOrigin(url, catalogUrl) {
47
+ return url.origin === catalogUrl.origin;
48
+ }
49
+ function stringList(value, itemMax, cap, pattern) {
50
+ if (!Array.isArray(value))
51
+ return [];
52
+ const out = [];
53
+ for (const item of value) {
54
+ const entry = text(item, itemMax);
55
+ if (!entry || (pattern && !pattern.test(entry)) || out.includes(entry))
56
+ continue;
57
+ out.push(entry);
58
+ if (out.length >= cap)
59
+ break;
60
+ }
61
+ return out;
62
+ }
63
+ function sanitizePlugin(raw, catalogUrl, loopbackOk) {
64
+ if (!isRecord(raw))
65
+ return undefined;
66
+ const slug = text(raw.slug, 64);
67
+ const name = text(raw.name, 100);
68
+ const pkg = text(raw.package, 214);
69
+ const version = text(raw.version, 64);
70
+ if (!slug || !SLUG_RE.test(slug) || !name || !pkg || !version)
71
+ return undefined;
72
+ if (comparePluginVersions(version, version) === undefined)
73
+ return undefined;
74
+ try {
75
+ const parsed = parsePluginSpec(pkg);
76
+ if (parsed.version !== undefined || parsed.name !== pkg)
77
+ return undefined;
78
+ }
79
+ catch {
80
+ return undefined;
81
+ }
82
+ const link = (value) => safeUrl(value, 500, loopbackOk)?.href;
83
+ const cover = safeUrl(raw.cover, 500, loopbackOk);
84
+ const sdk = text(raw.sdk, 60);
85
+ const author = text(raw.author, 100);
86
+ const license = text(raw.license, 60);
87
+ const homepage = link(raw.homepage);
88
+ const npmUrl = link(raw.npmUrl);
89
+ const repository = link(raw.repository);
90
+ const publishedAt = text(raw.publishedAt, 40);
91
+ return {
92
+ id: text(raw.id, 100) ?? slug,
93
+ slug,
94
+ name,
95
+ package: pkg,
96
+ version,
97
+ description: text(raw.description, 600) ?? "",
98
+ categories: stringList(raw.categories, 40, 8, /^[a-z0-9][a-z0-9-]*$/),
99
+ tags: stringList(raw.tags, 40, 12, TAG_RE),
100
+ ...(sdk ? { sdk } : {}),
101
+ ...(author ? { author } : {}),
102
+ ...(license ? { license } : {}),
103
+ ...(homepage ? { homepage } : {}),
104
+ ...(npmUrl ? { npmUrl } : {}),
105
+ ...(repository ? { repository } : {}),
106
+ ...(publishedAt ? { publishedAt } : {}),
107
+ featured: raw.featured === true,
108
+ ...(cover && sameOrigin(cover, catalogUrl) ? { coverSource: cover.href } : {}),
109
+ };
110
+ }
111
+ /** Validates a parsed catalog document; throws a short error for an unusable document. */
112
+ export function sanitizeCatalog(raw, catalogUrl) {
113
+ if (!isRecord(raw))
114
+ throw new Error("The plugin catalog is not a JSON object");
115
+ if (raw.schemaVersion !== SUPPORTED_SCHEMA_VERSION)
116
+ throw new Error(`Unsupported plugin catalog schema version (expected ${SUPPORTED_SCHEMA_VERSION})`);
117
+ if (!Array.isArray(raw.plugins))
118
+ throw new Error("The plugin catalog has no plugin list");
119
+ const base = new URL(catalogUrl);
120
+ const loopbackOk = base.protocol === "http:" && isLoopbackHostname(base.hostname);
121
+ const plugins = [];
122
+ const slugs = new Set();
123
+ const packages = new Set();
124
+ for (const entry of raw.plugins.slice(0, MAX_PLUGINS)) {
125
+ const plugin = sanitizePlugin(entry, base, loopbackOk);
126
+ if (!plugin || slugs.has(plugin.slug) || packages.has(plugin.package))
127
+ continue;
128
+ slugs.add(plugin.slug);
129
+ packages.add(plugin.package);
130
+ plugins.push(plugin);
131
+ }
132
+ const counts = new Map();
133
+ for (const plugin of plugins)
134
+ for (const id of plugin.categories)
135
+ counts.set(id, (counts.get(id) ?? 0) + 1);
136
+ const categories = [...counts.entries()]
137
+ .map(([id, count]) => ({ id, count }))
138
+ .sort((a, b) => b.count - a.count || a.id.localeCompare(b.id));
139
+ const generatedAt = text(raw.generatedAt, 40);
140
+ return { ...(generatedAt ? { generatedAt } : {}), categories, plugins };
141
+ }
@@ -0,0 +1,89 @@
1
+ import { type CatalogPlugin, type SanitizedCatalog } from "./catalog-schema.ts";
2
+ export declare const DEFAULT_CATALOG_URL = "https://gustavogutierrez.github.io/alisio-plugins/catalog.json";
3
+ export declare const CATALOG_TTL_MS: number;
4
+ export declare const CATALOG_TIMEOUT_MS = 10000;
5
+ export declare const MAX_CATALOG_BYTES: number;
6
+ export declare const MAX_COVER_BYTES: number;
7
+ export interface CatalogMeta {
8
+ etag?: string;
9
+ lastModified?: string;
10
+ /** When the catalog content was last downloaded (ms since epoch). */
11
+ fetchedAt: number;
12
+ /** When the catalog was last confirmed current (download or 304). */
13
+ checkedAt: number;
14
+ }
15
+ export interface CatalogResult {
16
+ catalog: SanitizedCatalog;
17
+ meta: {
18
+ checkedAt: number;
19
+ fetchedAt: number;
20
+ fromCache: boolean;
21
+ stale: boolean;
22
+ offline: boolean;
23
+ error?: string;
24
+ };
25
+ }
26
+ export interface CatalogServiceOptions {
27
+ /** Catalog document URL (default: `ALISIO_PLUGIN_CATALOG_URL`, else the published catalog). */
28
+ url?: string;
29
+ fetch?: typeof fetch;
30
+ now?: () => number;
31
+ ttlMs?: number;
32
+ timeoutMs?: number;
33
+ }
34
+ export declare class CatalogUnavailableError extends Error {
35
+ }
36
+ export interface Cover {
37
+ body: Buffer;
38
+ etag: string;
39
+ contentType: string;
40
+ }
41
+ /** Image types a cover may have, recognised by their bytes (never by the origin's header). */
42
+ declare const COVER_TYPES: readonly [{
43
+ readonly type: "image/svg+xml";
44
+ readonly ext: "svg";
45
+ }, {
46
+ readonly type: "image/webp";
47
+ readonly ext: "webp";
48
+ }, {
49
+ readonly type: "image/png";
50
+ readonly ext: "png";
51
+ }, {
52
+ readonly type: "image/jpeg";
53
+ readonly ext: "jpg";
54
+ }];
55
+ export declare function sniffCover(body: Buffer): (typeof COVER_TYPES)[number] | undefined;
56
+ export declare class CatalogService {
57
+ private options;
58
+ private inflight;
59
+ private coverInflight;
60
+ private fetcher;
61
+ private clock;
62
+ constructor(options?: CatalogServiceOptions);
63
+ get url(): string;
64
+ private get ttl();
65
+ private get cacheFile();
66
+ private coverFile;
67
+ private coverMetaFile;
68
+ private readCache;
69
+ /** The catalog: from cache when fresh, else revalidated (`refresh` forces the check). */
70
+ get(refresh?: boolean): Promise<CatalogResult>;
71
+ private load;
72
+ private result;
73
+ private revalidate;
74
+ private assertSameOrigin;
75
+ /** A plugin of the current catalog by slug (cache, revalidated only when stale). */
76
+ plugin(slug: string): Promise<{
77
+ plugin: CatalogPlugin;
78
+ result: CatalogResult;
79
+ } | undefined>;
80
+ /**
81
+ * The cover SVG of a catalog plugin. It is fetched from the sanitized catalog URL only and kept
82
+ * on disk; it is revalidated (conditionally) only when the catalog content changed since it was
83
+ * last checked, and an unreachable origin falls back to the copy on disk.
84
+ */
85
+ cover(plugin: CatalogPlugin, catalogFetchedAt: number): Promise<Cover | undefined>;
86
+ private loadCover;
87
+ private etagOf;
88
+ }
89
+ export {};