wizards 0.2.29 → 0.2.30
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.
- package/apps/runtime/dist/auth/account.js +8 -0
- package/apps/runtime/dist/auth/index.js +3 -0
- package/apps/runtime/dist/env.js +5 -0
- package/apps/runtime/dist/index.js +4 -0
- package/apps/runtime/dist/manage.js +6 -1
- package/apps/runtime/dist/plugins/loader.js +9 -2
- package/apps/runtime/dist/plugins/pro-keys.js +34 -0
- package/apps/runtime/dist/plugins/pro-state.js +83 -0
- package/apps/runtime/dist/plugins/pro.js +316 -0
- package/apps/runtime/dist/routes/plugins.js +49 -0
- package/apps/runtime/dist/routes/studio.js +3 -0
- package/apps/web/dist/assets/{Account-BaOouMw9.js → Account-9HRO4JWr.js} +1 -1
- package/apps/web/dist/assets/{ChangelogDialog-3t_EKUPH.js → ChangelogDialog-DXH1HcDL.js} +1 -1
- package/apps/web/dist/assets/{CreditsDialog-BW-RETl-.js → CreditsDialog-BtID6slV.js} +1 -1
- package/apps/web/dist/assets/{EditorPage-DrJhgEHO.js → EditorPage-DhAf6LjO.js} +1 -1
- package/apps/web/dist/assets/{FlowPreview-Wu5ovKrU.js → FlowPreview-D3ZFAeax.js} +1 -1
- package/apps/web/dist/assets/{Harness-Oh4MEjiK.js → Harness-mggD1ecl.js} +1 -1
- package/apps/web/dist/assets/{HomePage-eYqUUwzG.js → HomePage-CBhmQH5G.js} +1 -1
- package/apps/web/dist/assets/{ImportWizard-BSjp83g0.js → ImportWizard-BB8oJW0X.js} +1 -1
- package/apps/web/dist/assets/{LocalRuntime-CqBfKp8q.js → LocalRuntime-Dyvqh8HP.js} +1 -1
- package/apps/web/dist/assets/{NewWizardPage-Bpqk-BwW.js → NewWizardPage-CLFsjQgm.js} +2 -2
- package/apps/web/dist/assets/{ProjectPage-SK7e8kxV.js → ProjectPage-BwUhAgVx.js} +1 -1
- package/apps/web/dist/assets/{PublicRunner-Cu056QMy.js → PublicRunner-EfNAsmpU.js} +1 -1
- package/apps/web/dist/assets/{RunDrawer-BdcgNjPZ.js → RunDrawer-Cwtrh-Iz.js} +1 -1
- package/apps/web/dist/assets/{RunnerView-JOMD-e27.js → RunnerView-EQpbU4EU.js} +1 -1
- package/apps/web/dist/assets/SettingsPage-CASSFJS5.js +1 -0
- package/apps/web/dist/assets/{SetupPage-DdWIlUCF.js → SetupPage-BTjh2p6y.js} +1 -1
- package/apps/web/dist/assets/{SharePage-C5JlCg9u.js → SharePage-BSD9UL9f.js} +1 -1
- package/apps/web/dist/assets/arrow-right-CzoY677S.js +1 -0
- package/apps/web/dist/assets/changelog-j7qikrRS.js +1 -0
- package/apps/web/dist/assets/{circle-alert-BG9P3ewL.js → circle-alert-CN6dFS0d.js} +1 -1
- package/apps/web/dist/assets/{definition-CwX2olVF.js → definition-Cx9uG0KU.js} +1 -1
- package/apps/web/dist/assets/{esm-MDvpTvd_.js → esm-CKwbhTDV.js} +1 -1
- package/apps/web/dist/assets/{external-link-BwCtn1nW.js → external-link-DvkH63pi.js} +1 -1
- package/apps/web/dist/assets/{image-DG4q2P1H.js → image-BUbtvL1h.js} +1 -1
- package/apps/web/dist/assets/{index-knnf98Fv.js → index-C4KaV94L.js} +4 -4
- package/apps/web/dist/assets/{lock-DH_BntP_.js → lock-COAprdab.js} +1 -1
- package/apps/web/dist/assets/{meta-cFJejpIa.js → meta-vzJM2HEw.js} +1 -1
- package/apps/web/dist/assets/{pen-line-B1dsy3Sr.js → pen-line-JsP-r_aH.js} +1 -1
- package/apps/web/dist/assets/{refresh-cw-B9M1g3SH.js → refresh-cw-DLXaS3Ri.js} +1 -1
- package/apps/web/dist/assets/{settings-menu-DC825gd1.js → settings-menu-Ds_wegZm.js} +1 -1
- package/apps/web/dist/assets/{star-axQV_3EC.js → star-CpnGQZXD.js} +1 -1
- package/apps/web/dist/assets/{surface-D7bnQtqR.js → surface-BFwNWkx9.js} +1 -1
- package/apps/web/dist/assets/{trash-3S1_XpVa.js → trash-CQZuqdcu.js} +1 -1
- package/apps/web/dist/assets/{ui-BZ01zr3i.js → ui-BTkH_7CG.js} +1 -1
- package/apps/web/dist/assets/{upload-U0i8fAW3.js → upload-Dok4YeXQ.js} +1 -1
- package/apps/web/dist/assets/{video-Db4o719f.js → video-BjQoPIsn.js} +1 -1
- package/apps/web/dist/assets/{volume-2-6f7qN_LM.js → volume-2-D-Bn77h4.js} +1 -1
- package/apps/web/dist/assets/{wand-sparkles-MelvBtiK.js → wand-sparkles-D3sDsFFG.js} +1 -1
- package/apps/web/dist/index.html +3 -3
- package/apps/web/dist/mcp/flow.html +1 -1
- package/package.json +1 -1
- package/apps/web/dist/assets/SettingsPage-BSu-2gL5.js +0 -1
- package/apps/web/dist/assets/arrow-right-hy38IBRU.js +0 -1
- 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) {
|
package/apps/runtime/dist/env.js
CHANGED
|
@@ -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),
|
|
@@ -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
|
|
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
|
-
|
|
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
|
+
}
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { mkdir, readdir, rename, rm, writeFile } from "node:fs/promises";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { unzipSync } from "fflate";
|
|
6
|
+
import { accountToken, linkedAccount } from "../auth/account.js";
|
|
7
|
+
import { env } from "../env.js";
|
|
8
|
+
import { managed, verifySigned } from "../manage.js";
|
|
9
|
+
import { deleteSetting, readSetting, writeSetting } from "../settings.js";
|
|
10
|
+
import { MANIFEST_FILE } from "./discovery.js";
|
|
11
|
+
import { findNewPlugins, reloadPlugin } from "./loader.js";
|
|
12
|
+
import { signedByUs } from "./pro-keys.js";
|
|
13
|
+
import { currentEntitlement, ENTITLEMENT_SETTING, heldBackModules, INSTALLED_FILE, installedModule, proModulesDir, runtimeTag, setEntitlement, } from "./pro-state.js";
|
|
14
|
+
import { loadedPlugin } from "./registry.js";
|
|
15
|
+
/*
|
|
16
|
+
* Pro modules on a runtime that runs alone (docs/content/dev/plugins/pro-modules): the closed
|
|
17
|
+
* plugins the linked account's plan includes, fetched from its Manage-App.
|
|
18
|
+
*
|
|
19
|
+
* GET /v1/modules?runtime=<release> what is built for this release, the plan's
|
|
20
|
+
* part of it, and the signed entitlement
|
|
21
|
+
* GET /v1/modules/:id/package?runtime=<release> one module's zip
|
|
22
|
+
*
|
|
23
|
+
* A package is taken only when its SHA-256 and the signature over id, version, release and that
|
|
24
|
+
* hash check out against a key this runtime carries (pro-keys.ts): neither the Manage-App nor
|
|
25
|
+
* the way there can hand over code of their own.
|
|
26
|
+
*/
|
|
27
|
+
/** The audience of an entitlement, as the Manage-App signs it. */
|
|
28
|
+
const AUDIENCE = "urn:engenty:wizards:modules";
|
|
29
|
+
/** Larger packages are refused before they are read. */
|
|
30
|
+
const MAX_PACKAGE = 30 * 1024 * 1024;
|
|
31
|
+
/** What a package may hold, besides its manifest. */
|
|
32
|
+
const PARTS = ["dist/", "migrations/"];
|
|
33
|
+
const base = () => `${env.local.accountUrl}/v1/modules`;
|
|
34
|
+
/** Something the person can mend or retry; the message is for them. */
|
|
35
|
+
export class ProModuleError extends Error {
|
|
36
|
+
status;
|
|
37
|
+
code;
|
|
38
|
+
constructor(message, status = 400, code = "pro_module") {
|
|
39
|
+
super(message);
|
|
40
|
+
this.status = status;
|
|
41
|
+
this.code = code;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
let listing = null;
|
|
45
|
+
let lastError = null;
|
|
46
|
+
// --- the entitlement -------------------------------------------------------------------
|
|
47
|
+
/**
|
|
48
|
+
* At start, before the plugins load: what the last check said, while it lasts and belongs to the
|
|
49
|
+
* account that is linked now.
|
|
50
|
+
*/
|
|
51
|
+
export async function loadEntitlement() {
|
|
52
|
+
if (managed) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const [stored, account] = await Promise.all([
|
|
56
|
+
readSetting(ENTITLEMENT_SETTING),
|
|
57
|
+
linkedAccount(),
|
|
58
|
+
]);
|
|
59
|
+
setEntitlement(stored && account && stored.tenantId === account.tenantId ? stored : null);
|
|
60
|
+
}
|
|
61
|
+
async function keep(value) {
|
|
62
|
+
setEntitlement(value);
|
|
63
|
+
if (value) {
|
|
64
|
+
await writeSetting(ENTITLEMENT_SETTING, value);
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
await deleteSetting(ENTITLEMENT_SETTING);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/** Loads and unloads Pro modules after the entitlement changed. */
|
|
71
|
+
async function apply(before) {
|
|
72
|
+
const after = currentEntitlement();
|
|
73
|
+
const same = before?.modules.join() === after?.modules.join() &&
|
|
74
|
+
(before?.expiresAt ?? 0) > Date.now() === (after?.expiresAt ?? 0) > Date.now();
|
|
75
|
+
if (!same) {
|
|
76
|
+
await findNewPlugins();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
async function call(path, token) {
|
|
80
|
+
const url = new URL(`${base()}${path}`);
|
|
81
|
+
url.searchParams.set("runtime", runtimeTag());
|
|
82
|
+
const res = await fetch(url, {
|
|
83
|
+
headers: { authorization: `Bearer ${token}` },
|
|
84
|
+
signal: AbortSignal.timeout(60_000),
|
|
85
|
+
}).catch(() => null);
|
|
86
|
+
if (!res) {
|
|
87
|
+
throw new ProModuleError("Dein engenty-Konto ist gerade nicht erreichbar.", 502, "offline");
|
|
88
|
+
}
|
|
89
|
+
return res;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Asks the account what its plan includes now, keeps the signed answer and fetches what an
|
|
93
|
+
* installed module needs (a new release, a new build). Unreachable: what was confirmed lasts
|
|
94
|
+
* until it ends.
|
|
95
|
+
*/
|
|
96
|
+
export async function refreshPro() {
|
|
97
|
+
if (managed) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
const before = currentEntitlement();
|
|
101
|
+
const account = await linkedAccount();
|
|
102
|
+
if (!account) {
|
|
103
|
+
listing = null;
|
|
104
|
+
lastError = null;
|
|
105
|
+
await keep(null);
|
|
106
|
+
await apply(before);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
try {
|
|
110
|
+
const token = await accountToken();
|
|
111
|
+
if (!token) {
|
|
112
|
+
throw new ProModuleError("Dein engenty-Konto ist gerade nicht erreichbar.", 502, "offline");
|
|
113
|
+
}
|
|
114
|
+
const res = await call("", token);
|
|
115
|
+
if (res.status === 401) {
|
|
116
|
+
throw new ProModuleError("Das engenty-Konto nimmt diese Anmeldung nicht mehr an.", 403);
|
|
117
|
+
}
|
|
118
|
+
if (!res.ok) {
|
|
119
|
+
throw new ProModuleError(`Das Konto antwortet nicht wie erwartet (${res.status}).`, 502);
|
|
120
|
+
}
|
|
121
|
+
const answer = (await res.json());
|
|
122
|
+
const claims = await verifySigned(answer.entitlement?.token ?? "", [AUDIENCE], env.local.accountUrl);
|
|
123
|
+
if (!claims || claims.sub !== account.tenantId || !Array.isArray(claims.modules)) {
|
|
124
|
+
throw new ProModuleError("Die Bestätigung des Pakets ließ sich nicht prüfen.", 502);
|
|
125
|
+
}
|
|
126
|
+
listing = answer;
|
|
127
|
+
lastError = null;
|
|
128
|
+
await keep({
|
|
129
|
+
tenantId: account.tenantId,
|
|
130
|
+
modules: claims.modules.map(String),
|
|
131
|
+
plan: answer.plan ?? null,
|
|
132
|
+
expiresAt: (claims.exp ?? 0) * 1000,
|
|
133
|
+
checkedAt: Date.now(),
|
|
134
|
+
});
|
|
135
|
+
await apply(before);
|
|
136
|
+
await updateInstalled();
|
|
137
|
+
}
|
|
138
|
+
catch (err) {
|
|
139
|
+
lastError = err.message;
|
|
140
|
+
console.error("[pro modules]", lastError);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
let timer = null;
|
|
144
|
+
/** Checks at start, then once a day while the runtime runs; a check that failed, within the hour. */
|
|
145
|
+
export function startProChecks() {
|
|
146
|
+
if (managed || timer) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
void refreshPro();
|
|
150
|
+
timer = setInterval(() => {
|
|
151
|
+
const checkedAt = currentEntitlement()?.checkedAt ?? 0;
|
|
152
|
+
if (lastError || Date.now() - checkedAt > 23 * 3600_000) {
|
|
153
|
+
void refreshPro();
|
|
154
|
+
}
|
|
155
|
+
}, 3600_000);
|
|
156
|
+
timer.unref();
|
|
157
|
+
}
|
|
158
|
+
// --- installing ------------------------------------------------------------------------
|
|
159
|
+
async function installedIds() {
|
|
160
|
+
const entries = await readdir(proModulesDir(), { withFileTypes: true }).catch(() => []);
|
|
161
|
+
return entries.filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => e.name);
|
|
162
|
+
}
|
|
163
|
+
/** Installed modules built for another release or from another build are fetched anew. */
|
|
164
|
+
async function updateInstalled() {
|
|
165
|
+
for (const id of await installedIds()) {
|
|
166
|
+
const entry = listing?.modules.find((m) => m.id === id);
|
|
167
|
+
const installed = installedModule(join(proModulesDir(), id));
|
|
168
|
+
if (entry?.included && installed?.sha256 !== entry.sha256) {
|
|
169
|
+
await installPro(id).catch((err) => console.error(`[pro modules] ${id}: update failed:`, err.message));
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
/** The files of a zip, refusing any that would land outside the module's folder. */
|
|
174
|
+
function filesOf(zip) {
|
|
175
|
+
let files;
|
|
176
|
+
try {
|
|
177
|
+
files = unzipSync(zip);
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
throw new ProModuleError("Das Paket ist beschädigt.", 502);
|
|
181
|
+
}
|
|
182
|
+
const out = {};
|
|
183
|
+
for (const [name, data] of Object.entries(files)) {
|
|
184
|
+
if (name.endsWith("/")) {
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
const parts = name.split("/");
|
|
188
|
+
const safe = parts.every((p) => p && p !== "." && p !== ".." && !p.includes("\\"));
|
|
189
|
+
const allowed = name === MANIFEST_FILE || PARTS.some((part) => name.startsWith(part));
|
|
190
|
+
if (!(safe && allowed)) {
|
|
191
|
+
throw new ProModuleError(`Das Paket enthält eine unerlaubte Datei: ${name}`, 502);
|
|
192
|
+
}
|
|
193
|
+
out[name] = data;
|
|
194
|
+
}
|
|
195
|
+
return out;
|
|
196
|
+
}
|
|
197
|
+
/** Fetches one module of the plan, checks it and puts it in place; then it loads. */
|
|
198
|
+
export async function installPro(id) {
|
|
199
|
+
if (managed) {
|
|
200
|
+
throw new ProModuleError("Hier nicht verfügbar.", 404, "not_found");
|
|
201
|
+
}
|
|
202
|
+
const token = await accountToken();
|
|
203
|
+
if (!token) {
|
|
204
|
+
throw new ProModuleError("Verbinde zuerst dein engenty-Konto (Einstellungen → Konto).", 409);
|
|
205
|
+
}
|
|
206
|
+
if (!listing) {
|
|
207
|
+
await refreshPro();
|
|
208
|
+
}
|
|
209
|
+
const entry = listing?.modules.find((m) => m.id === id);
|
|
210
|
+
if (!entry) {
|
|
211
|
+
throw new ProModuleError(`Für ${runtimeTag()} gibt es dieses Modul nicht.`, 404, "not_found");
|
|
212
|
+
}
|
|
213
|
+
if (!entry.included) {
|
|
214
|
+
throw new ProModuleError("Dieses Modul ist nicht Teil deines Pakets.", 403, "plan");
|
|
215
|
+
}
|
|
216
|
+
const res = await call(`/${encodeURIComponent(id)}/package`, token);
|
|
217
|
+
if (!res.ok) {
|
|
218
|
+
throw new ProModuleError(`Das Modul ließ sich nicht laden (${res.status}).`, 502);
|
|
219
|
+
}
|
|
220
|
+
if (Number(res.headers.get("content-length") ?? 0) > MAX_PACKAGE) {
|
|
221
|
+
throw new ProModuleError("Das Paket ist zu groß.", 502);
|
|
222
|
+
}
|
|
223
|
+
const zip = new Uint8Array(await res.arrayBuffer());
|
|
224
|
+
const sha256 = createHash("sha256").update(zip).digest("hex");
|
|
225
|
+
const genuine = sha256 === entry.sha256 &&
|
|
226
|
+
signedByUs({
|
|
227
|
+
id,
|
|
228
|
+
version: entry.version,
|
|
229
|
+
runtime: runtimeTag(),
|
|
230
|
+
sha256,
|
|
231
|
+
signature: entry.signature,
|
|
232
|
+
});
|
|
233
|
+
if (zip.length > MAX_PACKAGE || !genuine) {
|
|
234
|
+
throw new ProModuleError("Das Paket ist nicht von engenty signiert und wurde verworfen.", 502);
|
|
235
|
+
}
|
|
236
|
+
const files = filesOf(zip);
|
|
237
|
+
const manifest = JSON.parse(new TextDecoder().decode(files[MANIFEST_FILE] ?? new Uint8Array()));
|
|
238
|
+
if (manifest?.id !== id) {
|
|
239
|
+
throw new ProModuleError("Das Paket passt nicht zu diesem Modul.", 502);
|
|
240
|
+
}
|
|
241
|
+
const dir = proModulesDir();
|
|
242
|
+
const target = join(dir, id);
|
|
243
|
+
const fresh = join(dir, `.new-${id}-${randomBytes(4).toString("hex")}`);
|
|
244
|
+
const old = join(dir, `.old-${id}-${randomBytes(4).toString("hex")}`);
|
|
245
|
+
await mkdir(fresh, { recursive: true });
|
|
246
|
+
try {
|
|
247
|
+
for (const [path, data] of Object.entries(files)) {
|
|
248
|
+
await mkdir(dirname(join(fresh, path)), { recursive: true });
|
|
249
|
+
await writeFile(join(fresh, path), data);
|
|
250
|
+
}
|
|
251
|
+
const installed = {
|
|
252
|
+
id,
|
|
253
|
+
version: entry.version,
|
|
254
|
+
runtime: runtimeTag(),
|
|
255
|
+
sha256,
|
|
256
|
+
installedAt: new Date().toISOString(),
|
|
257
|
+
};
|
|
258
|
+
await writeFile(join(fresh, INSTALLED_FILE), `${JSON.stringify(installed, null, 2)}\n`);
|
|
259
|
+
if (existsSync(target)) {
|
|
260
|
+
await rename(target, old);
|
|
261
|
+
}
|
|
262
|
+
await rename(fresh, target);
|
|
263
|
+
}
|
|
264
|
+
finally {
|
|
265
|
+
await rm(fresh, { recursive: true, force: true });
|
|
266
|
+
await rm(old, { recursive: true, force: true });
|
|
267
|
+
}
|
|
268
|
+
await reloadPlugin(id);
|
|
269
|
+
}
|
|
270
|
+
/** Removes an installed module; its tables and what is in them stay. */
|
|
271
|
+
export async function removePro(id) {
|
|
272
|
+
if (managed || !(await installedIds()).includes(id)) {
|
|
273
|
+
throw new ProModuleError("Dieses Modul ist nicht installiert.", 404, "not_found");
|
|
274
|
+
}
|
|
275
|
+
await rm(join(proModulesDir(), id), { recursive: true, force: true });
|
|
276
|
+
await findNewPlugins();
|
|
277
|
+
}
|
|
278
|
+
export async function proState() {
|
|
279
|
+
const entitlement = currentEntitlement();
|
|
280
|
+
const account = await linkedAccount();
|
|
281
|
+
const ids = new Set([...(listing?.modules.map((m) => m.id) ?? []), ...(await installedIds())]);
|
|
282
|
+
const modules = [...ids].sort().map((id) => {
|
|
283
|
+
const entry = listing?.modules.find((m) => m.id === id);
|
|
284
|
+
const installed = installedModule(join(proModulesDir(), id));
|
|
285
|
+
const plugin = loadedPlugin(id);
|
|
286
|
+
return {
|
|
287
|
+
id,
|
|
288
|
+
name: entry?.name ?? plugin?.source.manifest.name ?? id,
|
|
289
|
+
version: entry?.version ?? installed?.version ?? "",
|
|
290
|
+
description: entry?.description ?? plugin?.source.manifest.description ?? "",
|
|
291
|
+
size: entry?.size ?? null,
|
|
292
|
+
included: entitlement?.modules.includes(id) ?? false,
|
|
293
|
+
installed: installed ? { version: installed.version, runtime: installed.runtime } : null,
|
|
294
|
+
loaded: Boolean(plugin?.source.pro),
|
|
295
|
+
heldBack: heldBackModules().get(id) ?? null,
|
|
296
|
+
update: Boolean(entry?.included && installed && installed.sha256 !== entry.sha256),
|
|
297
|
+
};
|
|
298
|
+
});
|
|
299
|
+
return {
|
|
300
|
+
linked: Boolean(account),
|
|
301
|
+
/** Where the plans are: `<accountUrl>/billing`. */
|
|
302
|
+
accountUrl: env.local.accountUrl,
|
|
303
|
+
runtime: runtimeTag(),
|
|
304
|
+
plan: entitlement?.plan ?? listing?.plan ?? null,
|
|
305
|
+
expiresAt: entitlement ? new Date(entitlement.expiresAt).toISOString() : null,
|
|
306
|
+
checkedAt: entitlement ? new Date(entitlement.checkedAt).toISOString() : null,
|
|
307
|
+
error: lastError,
|
|
308
|
+
modules,
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
/** For tests: forget what the account said. */
|
|
312
|
+
export function resetProForTests() {
|
|
313
|
+
listing = null;
|
|
314
|
+
lastError = null;
|
|
315
|
+
setEntitlement(null);
|
|
316
|
+
}
|
|
@@ -3,6 +3,7 @@ import { Hono } from "hono";
|
|
|
3
3
|
import { streamSSE } from "hono/streaming";
|
|
4
4
|
import { managed } from "../manage.js";
|
|
5
5
|
import { pluginProblems, reloadPlugin, rescanPlugins } from "../plugins/loader.js";
|
|
6
|
+
import { installPro, proState, refreshPro, removePro } from "../plugins/pro.js";
|
|
6
7
|
import { loadedPlugin, onPluginChange, pluginAsset, pluginIdsOf, pluginsOf, } from "../plugins/registry.js";
|
|
7
8
|
const admin = (user) => user.role === "owner" || user.role === "admin";
|
|
8
9
|
/** Plugins change while the app runs only where it runs alone: a reload would hit every tenant. */
|
|
@@ -18,6 +19,8 @@ function view(plugin) {
|
|
|
18
19
|
version,
|
|
19
20
|
description: description ?? "",
|
|
20
21
|
generation: plugin.generation,
|
|
22
|
+
/** Installed from the linked account's plan (Settings → Plugins → Pro modules). */
|
|
23
|
+
pro: Boolean(plugin.source.pro),
|
|
21
24
|
/** Why the server half did not load; the plugin then adds nothing on the server. */
|
|
22
25
|
error: plugin.error,
|
|
23
26
|
script: script ? address("client.js", script.rev) : null,
|
|
@@ -155,6 +158,52 @@ export const pluginRoutes = new Hono()
|
|
|
155
158
|
return plugin
|
|
156
159
|
? c.json({ plugin: view(plugin) })
|
|
157
160
|
: c.json({ error: "not found", code: "not_found" }, 404);
|
|
161
|
+
})
|
|
162
|
+
// Pro modules: the closed plugins the linked account's plan includes (plugins/pro.ts).
|
|
163
|
+
.get("/-/pro", async (c) => {
|
|
164
|
+
if (!canReload(c.get("user"))) {
|
|
165
|
+
return c.json({ error: "not found", code: "not_found" }, 404);
|
|
166
|
+
}
|
|
167
|
+
return c.json(await proState());
|
|
168
|
+
})
|
|
169
|
+
.post("/-/pro/refresh", async (c) => {
|
|
170
|
+
if (!canReload(c.get("user"))) {
|
|
171
|
+
return c.json({ error: "forbidden", code: "forbidden" }, 403);
|
|
172
|
+
}
|
|
173
|
+
await refreshPro();
|
|
174
|
+
return c.json(await proState());
|
|
175
|
+
})
|
|
176
|
+
.post("/-/pro/:id", async (c) => {
|
|
177
|
+
if (!canReload(c.get("user"))) {
|
|
178
|
+
return c.json({ error: "forbidden", code: "forbidden" }, 403);
|
|
179
|
+
}
|
|
180
|
+
try {
|
|
181
|
+
await installPro(c.req.param("id"));
|
|
182
|
+
}
|
|
183
|
+
catch (err) {
|
|
184
|
+
const answer = answerOf(err);
|
|
185
|
+
if (answer) {
|
|
186
|
+
return c.json(answer.body, answer.status);
|
|
187
|
+
}
|
|
188
|
+
throw err;
|
|
189
|
+
}
|
|
190
|
+
return c.json(await proState());
|
|
191
|
+
})
|
|
192
|
+
.delete("/-/pro/:id", async (c) => {
|
|
193
|
+
if (!canReload(c.get("user"))) {
|
|
194
|
+
return c.json({ error: "forbidden", code: "forbidden" }, 403);
|
|
195
|
+
}
|
|
196
|
+
try {
|
|
197
|
+
await removePro(c.req.param("id"));
|
|
198
|
+
}
|
|
199
|
+
catch (err) {
|
|
200
|
+
const answer = answerOf(err);
|
|
201
|
+
if (answer) {
|
|
202
|
+
return c.json(answer.body, answer.status);
|
|
203
|
+
}
|
|
204
|
+
throw err;
|
|
205
|
+
}
|
|
206
|
+
return c.json(await proState());
|
|
158
207
|
})
|
|
159
208
|
.all("/:id", dispatch)
|
|
160
209
|
.all("/:id/*", dispatch);
|
|
@@ -27,6 +27,7 @@ import { seenApps } from "../mcp/seen.js";
|
|
|
27
27
|
import { generateImageMedia, generateSpeechMedia, generateVideoMedia } from "../media/generate.js";
|
|
28
28
|
import { transcribeAudio } from "../media/transcribe.js";
|
|
29
29
|
import { checkKey, classProblem, embeddingModel, hasTextModel, LOCAL_KEYS, localModelSettings, ModelUnavailableError, modelSettings, saveLocalModels, saveModels, storedKey, textModel, } from "../models.js";
|
|
30
|
+
import { refreshPro } from "../plugins/pro.js";
|
|
30
31
|
import { HTML_RESPONSE_CSP } from "../render/guard.js";
|
|
31
32
|
import { isAdminRole, mayBuild, mayCreate, projectWritable, requireWritable, } from "../services/access.js";
|
|
32
33
|
import { architectTurn } from "../services/architect.js";
|
|
@@ -1214,6 +1215,8 @@ export const studio = new Hono()
|
|
|
1214
1215
|
}
|
|
1215
1216
|
await unlink();
|
|
1216
1217
|
await forgetCloud();
|
|
1218
|
+
// Pro modules went with the account's plan; installed ones stay on disk.
|
|
1219
|
+
await refreshPro();
|
|
1217
1220
|
// Models that ran on the account's credits have nothing to run on now; another source stays.
|
|
1218
1221
|
if (localModelSettings().source === "account") {
|
|
1219
1222
|
await saveLocalModels({ source: "own" });
|