wizards 0.2.29 → 0.2.31

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 (61) hide show
  1. package/apps/runtime/dist/auth/account.js +8 -0
  2. package/apps/runtime/dist/auth/index.js +3 -0
  3. package/apps/runtime/dist/authoring/guide.js +1 -0
  4. package/apps/runtime/dist/engine/steps.js +3 -1
  5. package/apps/runtime/dist/env.js +5 -0
  6. package/apps/runtime/dist/files/storage.js +3 -0
  7. package/apps/runtime/dist/files/web-images.js +101 -0
  8. package/apps/runtime/dist/index.js +4 -0
  9. package/apps/runtime/dist/manage.js +6 -1
  10. package/apps/runtime/dist/plugins/loader.js +9 -2
  11. package/apps/runtime/dist/plugins/pro-keys.js +34 -0
  12. package/apps/runtime/dist/plugins/pro-state.js +83 -0
  13. package/apps/runtime/dist/plugins/pro.js +316 -0
  14. package/apps/runtime/dist/render/downloads.js +5 -3
  15. package/apps/runtime/dist/routes/plugins.js +49 -0
  16. package/apps/runtime/dist/routes/studio.js +3 -0
  17. package/apps/runtime/dist/widgets/step.js +10 -4
  18. package/apps/web/dist/assets/{Account-BaOouMw9.js → Account-v7Iqkep5.js} +1 -1
  19. package/apps/web/dist/assets/{ChangelogDialog-3t_EKUPH.js → ChangelogDialog-CnYW-DMa.js} +1 -1
  20. package/apps/web/dist/assets/{CreditsDialog-BW-RETl-.js → CreditsDialog-BtID6slV.js} +1 -1
  21. package/apps/web/dist/assets/{EditorPage-DrJhgEHO.js → EditorPage-UqOALINq.js} +1 -1
  22. package/apps/web/dist/assets/{FlowPreview-Wu5ovKrU.js → FlowPreview-CxBGhnMi.js} +1 -1
  23. package/apps/web/dist/assets/{Harness-Oh4MEjiK.js → Harness-BBUVWLMx.js} +1 -1
  24. package/apps/web/dist/assets/{HomePage-eYqUUwzG.js → HomePage-E2kbDAWI.js} +1 -1
  25. package/apps/web/dist/assets/{ImportWizard-BSjp83g0.js → ImportWizard-DBxxe467.js} +1 -1
  26. package/apps/web/dist/assets/{LocalRuntime-CqBfKp8q.js → LocalRuntime-DMU_dzV1.js} +1 -1
  27. package/apps/web/dist/assets/{NewWizardPage-Bpqk-BwW.js → NewWizardPage-B9rqko-D.js} +2 -2
  28. package/apps/web/dist/assets/{ProjectPage-SK7e8kxV.js → ProjectPage-cqeSCfvy.js} +1 -1
  29. package/apps/web/dist/assets/{PublicRunner-Cu056QMy.js → PublicRunner-CrRc-QgY.js} +1 -1
  30. package/apps/web/dist/assets/{RunDrawer-BdcgNjPZ.js → RunDrawer-DRKc8Bm5.js} +1 -1
  31. package/apps/web/dist/assets/{RunnerView-JOMD-e27.js → RunnerView-BH2sXBlx.js} +1 -1
  32. package/apps/web/dist/assets/SettingsPage-DeehkMtv.js +1 -0
  33. package/apps/web/dist/assets/{SetupPage-DdWIlUCF.js → SetupPage-D13qI_dd.js} +1 -1
  34. package/apps/web/dist/assets/{SharePage-C5JlCg9u.js → SharePage-8ji9ACWJ.js} +1 -1
  35. package/apps/web/dist/assets/arrow-right-CzoY677S.js +1 -0
  36. package/apps/web/dist/assets/changelog-D536XAHL.js +1 -0
  37. package/apps/web/dist/assets/{circle-alert-BG9P3ewL.js → circle-alert-CN6dFS0d.js} +1 -1
  38. package/apps/web/dist/assets/{definition-CwX2olVF.js → definition-Cx9uG0KU.js} +1 -1
  39. package/apps/web/dist/assets/{esm-MDvpTvd_.js → esm-CSQNLh1s.js} +1 -1
  40. package/apps/web/dist/assets/{external-link-BwCtn1nW.js → external-link-DvkH63pi.js} +1 -1
  41. package/apps/web/dist/assets/{image-DG4q2P1H.js → image-BUbtvL1h.js} +1 -1
  42. package/apps/web/dist/assets/{index-knnf98Fv.js → index-CYZmm7HU.js} +4 -4
  43. package/apps/web/dist/assets/{lock-DH_BntP_.js → lock-COAprdab.js} +1 -1
  44. package/apps/web/dist/assets/{meta-cFJejpIa.js → meta-Daw62MfH.js} +1 -1
  45. package/apps/web/dist/assets/{pen-line-B1dsy3Sr.js → pen-line-JsP-r_aH.js} +1 -1
  46. package/apps/web/dist/assets/{refresh-cw-B9M1g3SH.js → refresh-cw-DLXaS3Ri.js} +1 -1
  47. package/apps/web/dist/assets/{settings-menu-DC825gd1.js → settings-menu-br026gwt.js} +1 -1
  48. package/apps/web/dist/assets/{star-axQV_3EC.js → star-CpnGQZXD.js} +1 -1
  49. package/apps/web/dist/assets/{surface-D7bnQtqR.js → surface-BFwNWkx9.js} +1 -1
  50. package/apps/web/dist/assets/{trash-3S1_XpVa.js → trash-CQZuqdcu.js} +1 -1
  51. package/apps/web/dist/assets/{ui-BZ01zr3i.js → ui-BTkH_7CG.js} +1 -1
  52. package/apps/web/dist/assets/{upload-U0i8fAW3.js → upload-Dok4YeXQ.js} +1 -1
  53. package/apps/web/dist/assets/{video-Db4o719f.js → video-BjQoPIsn.js} +1 -1
  54. package/apps/web/dist/assets/{volume-2-6f7qN_LM.js → volume-2-D-Bn77h4.js} +1 -1
  55. package/apps/web/dist/assets/{wand-sparkles-MelvBtiK.js → wand-sparkles-D3sDsFFG.js} +1 -1
  56. package/apps/web/dist/index.html +3 -3
  57. package/apps/web/dist/mcp/flow.html +1 -1
  58. package/package.json +1 -1
  59. package/apps/web/dist/assets/SettingsPage-BSu-2gL5.js +0 -1
  60. package/apps/web/dist/assets/arrow-right-hy38IBRU.js +0 -1
  61. package/apps/web/dist/assets/changelog-C2q3P5lJ.js +0 -1
@@ -91,6 +91,14 @@ export async function accountToken() {
91
91
  if (cached && cached.expiresAt - Date.now() > 60_000) {
92
92
  return cached.token;
93
93
  }
94
+ // One refresh at a time: a refresh token is used once, a second caller would be refused.
95
+ refreshing ??= refreshAccount().finally(() => {
96
+ refreshing = null;
97
+ });
98
+ return refreshing;
99
+ }
100
+ let refreshing = null;
101
+ async function refreshAccount() {
94
102
  const refresh = await vaultGet(REFRESH);
95
103
  if (!refresh) {
96
104
  return null;
@@ -7,6 +7,7 @@ import { nanoid } from "nanoid";
7
7
  import { control, controlDb, withTenant } from "../db/client.js";
8
8
  import { basePath, env } from "../env.js";
9
9
  import { discovery, managed, tokenRequest, verifyToken, } from "../manage.js";
10
+ import { refreshPro } from "../plugins/pro.js";
10
11
  import { seal, unseal } from "../secrets/crypto.js";
11
12
  import { LOCAL_TENANT } from "../tenants/tenant.js";
12
13
  import { finishLink } from "./account.js";
@@ -278,6 +279,8 @@ export const authRoutes = new Hono()
278
279
  await finishLink(code, rawState);
279
280
  // What is published here goes to the account's cloud, from now on and once for all there is.
280
281
  void firstSync();
282
+ // The Pro modules the account's plan includes.
283
+ void refreshPro();
281
284
  return c.html(closePage("Angemeldet. Du kannst dieses Fenster schließen."));
282
285
  }
283
286
  catch (err) {
@@ -256,6 +256,7 @@ ${sandboxGuideLine()}
256
256
  - Logins on websites: never ask for passwords in page fields. An agent step with the browser tool asks the person at the moment it meets the login (browser_request_credentials); write that into its instructions, and that it must go on without the site when the person skips.
257
257
  - Most people open a wizard on their phone. Keep pages short, and let the phone do the typing: photos with an "image" field (multiple) instead of descriptions, the place with a "location" field instead of an address form, what happened as an "audio" field instead of a long textarea (keep an optional textarea beside it), a sign-off as a "signature" field, a serial or ticket number as a "text" field with scan. Typical: damage report, handover protocol, site inspection, delivery note.
258
258
  - Photos in documents: a document step can place the photos of an earlier "image" field and a "signature" (it gets them as asset:// images); say in its prompt where they go.
259
+ - Pictures from the web (a listing's photos, a product shot an agent found): a document's <img src="https://…"> and image URLs in a widget's data (keys like image, photo, thumbnail, or a URL ending in .jpg/.png/.webp) are fetched once when the step runs and kept with the run — up to 40 per step. The page itself loads nothing from the network, so a URL left as is stays blank.
259
260
  - Looking at photos: an agent step whose instructions name an image field ({{photos}}) gets up to 8 of its photos attached and sees them itself, in the person's order — do not tell it to call read_document for them. Photos in a "file" field, PDFs and scans are read with read_document / scan_documents.
260
261
  - Papers and scans: a "file" field with multiple and camera. Agent steps read uploads with read_document; for invoices and receipts scan_documents returns the fields (vendor, number, date, totals, currency) exactly as printed.
261
262
  - Collecting documents (invoices from mail or portals): the step keeps each file with mail_save / browser_download under a dated path ("invoices/2026-09/2026-09-03_Notion_INV-123.pdf"); its deliverable offers "zip". Split a big job into steps (mail, then statements, then portals) — one step manages about 60 tool calls.
@@ -5,6 +5,7 @@ import { Agent } from "@mastra/core/agent";
5
5
  import { generateText, Output } from "ai";
6
6
  import { z } from "zod";
7
7
  import { extFor, loadAsset, loadAssetText } from "../files/storage.js";
8
+ import { snapshotHtmlImages } from "../files/web-images.js";
8
9
  import { runFilmStep } from "../film/step.js";
9
10
  import { generateImageMedia, generateSpeechMedia, generateVideoMedia, } from "../media/generate.js";
10
11
  import { markMedia } from "../media/marking.js";
@@ -804,7 +805,8 @@ export async function runGenerateStep(step, ctx) {
804
805
  }
805
806
  case "document":
806
807
  case "dashboard": {
807
- const html = await writeHtml(step, ctx, step.asset);
808
+ // Pictures the model took from the web stay: the page itself may load none.
809
+ const html = await snapshotHtmlImages(await writeHtml(step, ctx, step.asset), ctx.saveAsset, step.id, ctx.signal);
808
810
  const ref = await ctx.saveAsset({
809
811
  stepId: step.id,
810
812
  kind: "html",
@@ -222,6 +222,11 @@ export const env = {
222
222
  watch: !manageUrl && str("PLUGINS_WATCH", "1") === "1",
223
223
  /** Plugins' jobs (`server.every`) run. Off (`PLUGIN_JOBS=0`) where another process runs them. */
224
224
  jobs: str("PLUGIN_JOBS", "1") === "1",
225
+ /**
226
+ * Public keys (Ed25519, SPKI DER in base64) whose signature a Pro module may carry, besides
227
+ * the one this runtime ships with (plugins/pro-keys.ts). For testing packages signed elsewhere.
228
+ */
229
+ proKeys: list("PRO_MODULES_KEYS"),
225
230
  },
226
231
  limits: {
227
232
  visitorRunsPerHour: num("LIMIT_VISITOR_RUNS_PER_HOUR", 6),
@@ -11,6 +11,9 @@ const EXT = {
11
11
  "image/png": "png",
12
12
  "image/jpeg": "jpg",
13
13
  "image/webp": "webp",
14
+ "image/gif": "gif",
15
+ "image/avif": "avif",
16
+ "image/svg+xml": "svg",
14
17
  "video/mp4": "mp4",
15
18
  "video/webm": "webm",
16
19
  "video/quicktime": "mov",
@@ -0,0 +1,101 @@
1
+ import { safeFetch } from "../tools/net-guard.js";
2
+ /** Pictures one step may take from the web, and how large one may be. */
3
+ const MAX_IMAGES = 40;
4
+ const MAX_BYTES = 8_000_000;
5
+ const TIMEOUT_MS = 10_000;
6
+ const PARALLEL = 4;
7
+ const IMAGE_EXT = /\.(?:jpe?g|png|webp|gif|avif|svg)(?:[?#]|$)/i;
8
+ /** A key whose value is likely a picture: the extension of its URL may be hidden behind a token. */
9
+ const IMAGE_KEY = /image|photo|picture|thumb|cover|logo|avatar|img|poster|banner/i;
10
+ /** Fetches one picture; null when it is no image, too large, unreachable or private. */
11
+ export async function fetchWebImage(url, signal) {
12
+ const timeout = AbortSignal.timeout(TIMEOUT_MS);
13
+ try {
14
+ const res = await safeFetch(url, {
15
+ signal: signal ? AbortSignal.any([signal, timeout]) : timeout,
16
+ headers: { accept: "image/*" },
17
+ });
18
+ const mime = (res.headers.get("content-type") ?? "").split(";")[0].trim().toLowerCase();
19
+ const length = Number(res.headers.get("content-length") ?? 0);
20
+ if (!res.ok || !mime.startsWith("image/") || length > MAX_BYTES) {
21
+ await res.body?.cancel();
22
+ return null;
23
+ }
24
+ const data = Buffer.from(await res.arrayBuffer());
25
+ return data.byteLength && data.byteLength <= MAX_BYTES ? { mime, data } : null;
26
+ }
27
+ catch {
28
+ return null;
29
+ }
30
+ }
31
+ /** Takes the pictures behind `urls` into the run, a few at a time. */
32
+ async function snapshot(urls, save, stepId, signal) {
33
+ const refs = new Map();
34
+ const list = [...new Set(urls)].slice(0, MAX_IMAGES);
35
+ let next = 0;
36
+ await Promise.all(Array.from({ length: Math.min(PARALLEL, list.length) }, async () => {
37
+ while (next < list.length && !signal?.aborted) {
38
+ const url = list[next++];
39
+ const image = await fetchWebImage(url, signal);
40
+ if (!image) {
41
+ continue;
42
+ }
43
+ const ref = await save({
44
+ stepId,
45
+ kind: "web-image",
46
+ mime: image.mime,
47
+ name: new URL(url).pathname.split("/").pop()?.slice(0, 120) || "image",
48
+ data: image.data,
49
+ });
50
+ refs.set(url, `asset://${ref.id}`);
51
+ }
52
+ }));
53
+ return { refs };
54
+ }
55
+ const IMG_SRC = /(<img\b[^>]*?\ssrc\s*=\s*)(["'])(https?:\/\/[^"']+)\2/gi;
56
+ /** The pictures a generated page loads from the web, kept as assets; its <img> tags then point at them. */
57
+ export async function snapshotHtmlImages(html, save, stepId, signal) {
58
+ const urls = [...html.matchAll(IMG_SRC)].map((m) => m[3]);
59
+ if (!urls.length) {
60
+ return html;
61
+ }
62
+ const { refs } = await snapshot(urls, save, stepId, signal);
63
+ return html.replace(IMG_SRC, (m, head, q, url) => {
64
+ const ref = refs.get(url);
65
+ return ref ? `${head}${q}${ref}${q}` : m;
66
+ });
67
+ }
68
+ function isImageUrl(value, key) {
69
+ if (!/^https?:\/\/\S+$/.test(value) || value.length > 2000) {
70
+ return false;
71
+ }
72
+ return IMAGE_EXT.test(new URL(value).pathname + new URL(value).search) || IMAGE_KEY.test(key);
73
+ }
74
+ function walk(value, key, visit) {
75
+ if (typeof value === "string") {
76
+ return isImageUrl(value, key) ? visit(value, key) : value;
77
+ }
78
+ if (Array.isArray(value)) {
79
+ return value.map((v) => walk(v, key, visit));
80
+ }
81
+ if (value && typeof value === "object") {
82
+ return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, walk(v, k, visit)]));
83
+ }
84
+ return value;
85
+ }
86
+ /**
87
+ * The pictures a widget's data points to on the web (a listing's photos, a product's cover),
88
+ * kept as assets; the data then carries asset://ID where the page expects the URL.
89
+ */
90
+ export async function snapshotDataImages(data, save, stepId, signal) {
91
+ const urls = [];
92
+ walk(data, "", (url) => {
93
+ urls.push(url);
94
+ return url;
95
+ });
96
+ if (!urls.length) {
97
+ return data;
98
+ }
99
+ const { refs } = await snapshot(urls, save, stepId, signal);
100
+ return walk(data, "", (url) => refs.get(url) ?? url);
101
+ }
@@ -8,6 +8,7 @@ import { managed } from "./manage.js";
8
8
  import { checkModelsAtStart, loadCatalog, loadLocalModels } from "./models.js";
9
9
  import { startJobs } from "./plugins/jobs.js";
10
10
  import { loadPlugins } from "./plugins/loader.js";
11
+ import { loadEntitlement, startProChecks } from "./plugins/pro.js";
11
12
  import { closeBrowser } from "./render/chromium.js";
12
13
  import { clearRunning, writeRunning } from "./running.js";
13
14
  import { syncMarketplace } from "./services/marketplace.js";
@@ -16,6 +17,8 @@ import { purgeUnusedStores } from "./store/index.js";
16
17
  import { LOCAL_TENANT } from "./tenants/tenant.js";
17
18
  import { onRestart } from "./update.js";
18
19
  await migrateControlDb();
20
+ // What the linked account's plan confirmed last: the Pro modules it includes load with the rest.
21
+ await loadEntitlement();
19
22
  // Before any tenant database opens: each gets the plugins' own tables as it does.
20
23
  await loadPlugins();
21
24
  startJobs();
@@ -31,6 +34,7 @@ else {
31
34
  await loadLocalModels();
32
35
  void loadCatalog();
33
36
  void checkModelsAtStart().catch(() => undefined);
37
+ startProChecks();
34
38
  }
35
39
  // The marketplace's starters and the starred entries, kept for offline use; checked hourly.
36
40
  void syncMarketplace();
@@ -47,6 +47,11 @@ function toClaims(payload) {
47
47
  * `audiences` are the resources this caller accepts; null skips the audience check.
48
48
  */
49
49
  export async function verifyToken(token, audiences, base = env.manage.url) {
50
+ const payload = await verifySigned(token, audiences, base);
51
+ return payload ? toClaims(payload) : null;
52
+ }
53
+ /** Any token the Manage-App at `base` signed (its JWKS, its issuer); null when it is not one. */
54
+ export async function verifySigned(token, audiences, base = env.manage.url) {
50
55
  try {
51
56
  const d = await discovery(base);
52
57
  let keys = keySets.get(d.jwks_uri);
@@ -58,7 +63,7 @@ export async function verifyToken(token, audiences, base = env.manage.url) {
58
63
  issuer: d.issuer,
59
64
  ...(audiences ? { audience: audiences } : {}),
60
65
  });
61
- return toClaims(payload);
66
+ return payload;
62
67
  }
63
68
  catch {
64
69
  return null;
@@ -10,12 +10,14 @@ import { pluginConnections } from "../connectors/plugin-connections.js";
10
10
  import { db, onTenantOpen } from "../db/client.js";
11
11
  import { parseDocument } from "../documents/parse.js";
12
12
  import { env } from "../env.js";
13
+ import { managed } from "../manage.js";
13
14
  import { ModelUnavailableError, textModel } from "../models.js";
14
15
  import { putIndexEntry, removeIndexEntries } from "../services/project-index.js";
15
16
  import { originData } from "../services/space-origin.js";
16
17
  import { safeFetch } from "../tools/net-guard.js";
17
18
  import { discoverPlugins } from "./discovery.js";
18
19
  import { migrateOpenTenants, migratePlugins } from "./migrations.js";
20
+ import { proModulesDir, proSources } from "./pro-state.js";
19
21
  import { publicUrlOf } from "./public.js";
20
22
  import { announceChange, compileRoute, dropPlugin, emptyRecord, loadedPlugin, loadedPlugins, setPlugin, } from "./registry.js";
21
23
  /**
@@ -231,11 +233,16 @@ export function pluginProblems() {
231
233
  }
232
234
  function discover() {
233
235
  const result = discoverPlugins(pluginDirs());
234
- problems = result.problems;
236
+ // Pro modules come last and only while the plan has them (pro-state.ts).
237
+ const pro = managed ? null : discoverPlugins([proModulesDir()]);
238
+ const allowed = pro
239
+ ? proSources(pro.found, new Set(result.found.map((s) => s.id)))
240
+ : { load: [], problems: [] };
241
+ problems = [...result.problems, ...(pro?.problems ?? []), ...allowed.problems];
235
242
  for (const problem of problems) {
236
243
  console.error(`[plugins] ${problem.path}: ${problem.message}`);
237
244
  }
238
- return result.found;
245
+ return [...result.found, ...allowed.load];
239
246
  }
240
247
  /** One load at a time: the watcher and a person may ask for a reload at the same moment. */
241
248
  let queue = Promise.resolve();
@@ -0,0 +1,34 @@
1
+ import { createPublicKey, verify } from "node:crypto";
2
+ import { env } from "../env.js";
3
+ /**
4
+ * The keys a Pro module's package must be signed with (docs/content/dev/plugins/pro-modules).
5
+ * Only the public halves are here; the private one signs in the build of the closed plugins and
6
+ * is kept nowhere else. A new key is added beside the old one, which goes a release later.
7
+ */
8
+ const SHIPPED = ["MCowBQYDK2VwAyEAqVQAptgVhkBtobNObBDHAZzKFdted0nH0I+LWAHGER8="];
9
+ let keys = null;
10
+ function trusted() {
11
+ keys ??= [...SHIPPED, ...env.plugins.proKeys].map((der) => createPublicKey({ key: Buffer.from(der, "base64"), format: "der", type: "spki" }));
12
+ return keys;
13
+ }
14
+ /** What the signature covers; the packer of the closed plugins writes the same text. */
15
+ const message = (p) => `engenty-module:v1\n${p.id}\n${p.version}\n${p.runtime}\n${p.sha256}`;
16
+ /** Whether one of the trusted keys signed this package for this release. */
17
+ export function signedByUs(p) {
18
+ let signature;
19
+ try {
20
+ signature = Buffer.from(p.signature, "base64");
21
+ }
22
+ catch {
23
+ return false;
24
+ }
25
+ const data = Buffer.from(message(p));
26
+ return trusted().some((key) => {
27
+ try {
28
+ return verify(null, data, key, signature);
29
+ }
30
+ catch {
31
+ return false;
32
+ }
33
+ });
34
+ }
@@ -0,0 +1,83 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { packageVersion } from "../cli/home.js";
4
+ import { env } from "../env.js";
5
+ import { managed } from "../manage.js";
6
+ /**
7
+ * Which Pro modules may load (docs/content/dev/plugins/pro-modules). A Pro module is a closed
8
+ * plugin the linked account's plan includes, installed by pro.ts into a folder of its own. It
9
+ * loads while the entitlement the account's Manage-App signed lists it and has not ended, and
10
+ * only in the release it was built for. Kept apart from pro.ts, so the loader needs no network.
11
+ */
12
+ /** Where Pro modules are installed: apart from the person's own plugins, not watched. */
13
+ export const proModulesDir = () => join(env.dataDir, "pro-modules");
14
+ /** Written beside an installed module: what was installed, for which release. */
15
+ export const INSTALLED_FILE = ".engenty-module.json";
16
+ export const ENTITLEMENT_SETTING = "pro.entitlement";
17
+ let entitlement = null;
18
+ export function currentEntitlement() {
19
+ return entitlement;
20
+ }
21
+ export function setEntitlement(value) {
22
+ entitlement = value;
23
+ }
24
+ /** `0.2.26` → `v0.2.26`: the release this runtime is. */
25
+ export const runtimeTag = () => `v${packageVersion()}`;
26
+ export function installedModule(root) {
27
+ try {
28
+ return JSON.parse(readFileSync(join(root, INSTALLED_FILE), "utf8"));
29
+ }
30
+ catch {
31
+ return null;
32
+ }
33
+ }
34
+ export function heldBack(source) {
35
+ if (managed) {
36
+ return "managed";
37
+ }
38
+ const installed = installedModule(source.root);
39
+ if (!installed || installed.id !== source.id) {
40
+ return "not_installed";
41
+ }
42
+ if (installed.runtime !== runtimeTag()) {
43
+ return "release";
44
+ }
45
+ if (!(entitlement && entitlement.expiresAt > Date.now())) {
46
+ return "unconfirmed";
47
+ }
48
+ if (!entitlement.modules.includes(source.id)) {
49
+ return "plan";
50
+ }
51
+ return null;
52
+ }
53
+ let held = new Map();
54
+ /** The Pro modules that were found and did not load, with the reason, as of the last look. */
55
+ export function heldBackModules() {
56
+ return held;
57
+ }
58
+ /**
59
+ * The Pro modules of a look at the folders that may load, marked as such. A Pro module never
60
+ * takes the id of a plugin found elsewhere.
61
+ */
62
+ export function proSources(found, taken) {
63
+ const load = [];
64
+ const problems = [];
65
+ held = new Map();
66
+ for (const source of found) {
67
+ if (taken.has(source.id)) {
68
+ problems.push({
69
+ path: source.root,
70
+ message: `Plugin "${source.id}" is already loaded from another folder.`,
71
+ });
72
+ continue;
73
+ }
74
+ const reason = heldBack(source);
75
+ if (reason) {
76
+ held.set(source.id, reason);
77
+ }
78
+ else {
79
+ load.push({ ...source, pro: true });
80
+ }
81
+ }
82
+ return { load, problems };
83
+ }