wizards 0.4.7 → 0.5.1
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/README.md +45 -42
- package/apps/runtime/dist/agents/studio-assistant.js +8 -8
- package/apps/runtime/dist/app.js +7 -4
- package/apps/runtime/dist/assistant/primer.js +1 -1
- package/apps/runtime/dist/auth/index.js +2 -2
- package/apps/runtime/dist/authoring/guide.js +8 -8
- package/apps/runtime/dist/cli/account.js +31 -28
- package/apps/runtime/dist/cli/autostart.js +5 -3
- package/apps/runtime/dist/cli/clients.js +1 -1
- package/apps/runtime/dist/cli/connect.js +24 -24
- package/apps/runtime/dist/cli/features.js +6 -6
- package/apps/runtime/dist/cli/main.js +51 -39
- package/apps/runtime/dist/cli/mcp.js +5 -4
- package/apps/runtime/dist/cli/menu.js +32 -30
- package/apps/runtime/dist/cli/report.js +7 -9
- package/apps/runtime/dist/cli/setup.js +42 -38
- package/apps/runtime/dist/cli/start.js +16 -15
- package/apps/runtime/dist/cli/status.js +20 -20
- package/apps/runtime/dist/cli/ui.js +9 -2
- package/apps/runtime/dist/cli/update.js +6 -6
- package/apps/runtime/dist/cli/validate.js +1 -1
- package/apps/runtime/dist/connectors/external.js +3 -2
- package/apps/runtime/dist/connectors/index.js +5 -5
- package/apps/runtime/dist/connectors/plugin-connections.js +2 -2
- package/apps/runtime/dist/connectors/via-account.js +5 -5
- package/apps/runtime/dist/db/control-schema.js +14 -0
- package/apps/runtime/dist/db/migrations/control/0009_plugin_claim.sql +9 -0
- package/apps/runtime/dist/db/migrations/control/meta/0009_snapshot.json +561 -0
- package/apps/runtime/dist/db/migrations/control/meta/_journal.json +7 -0
- package/apps/runtime/dist/db/schema.js +9 -9
- package/apps/runtime/dist/docs/dev/index.md +16 -14
- package/apps/runtime/dist/docs/dev/plugins/connections.md +27 -24
- package/apps/runtime/dist/docs/dev/plugins/events.md +19 -17
- package/apps/runtime/dist/docs/dev/plugins/examples.md +13 -12
- package/apps/runtime/dist/docs/dev/plugins/index.md +29 -24
- package/apps/runtime/dist/docs/dev/plugins/layout.md +20 -19
- package/apps/runtime/dist/docs/dev/plugins/quick-start.md +16 -15
- package/apps/runtime/dist/docs/dev/plugins/reference.md +52 -40
- package/apps/runtime/dist/docs/dev/plugins/reloading.md +24 -23
- package/apps/runtime/dist/docs/dev/plugins/routes.md +26 -22
- package/apps/runtime/dist/docs/dev/plugins/runners.md +49 -44
- package/apps/runtime/dist/docs/dev/plugins/server.md +95 -43
- package/apps/runtime/dist/docs/dev/plugins/shipping.md +19 -16
- package/apps/runtime/dist/docs/dev/plugins/space.md +75 -69
- package/apps/runtime/dist/docs/dev/plugins/starters.md +22 -17
- package/apps/runtime/dist/docs/dev/plugins/studio.md +80 -37
- package/apps/runtime/dist/docs/dev/plugins/styling.md +44 -41
- package/apps/runtime/dist/docs/dev/plugins/tables.md +11 -11
- package/apps/runtime/dist/docs/dev/plugins/tools.md +26 -25
- package/apps/runtime/dist/docs/dev/wizard-format.md +35 -23
- package/apps/runtime/dist/docs/user/ai-apps.md +46 -43
- package/apps/runtime/dist/docs/user/assistant.md +41 -36
- package/apps/runtime/dist/docs/user/build-a-wizard.md +64 -39
- package/apps/runtime/dist/docs/user/command-line.md +47 -44
- package/apps/runtime/dist/docs/user/connectors.md +40 -35
- package/apps/runtime/dist/docs/user/create-account.md +27 -22
- package/apps/runtime/dist/docs/user/data.md +29 -29
- package/apps/runtime/dist/docs/user/first-start.md +26 -28
- package/apps/runtime/dist/docs/user/index.md +16 -17
- package/apps/runtime/dist/docs/user/install.md +28 -27
- package/apps/runtime/dist/docs/user/local-only.md +21 -17
- package/apps/runtime/dist/docs/user/models.md +92 -84
- package/apps/runtime/dist/docs/user/plugins.md +28 -23
- package/apps/runtime/dist/docs/user/run.md +51 -50
- package/apps/runtime/dist/docs/user/server.md +9 -9
- package/apps/runtime/dist/docs/user/space.md +87 -65
- package/apps/runtime/dist/docs/user/steps.md +73 -69
- package/apps/runtime/dist/docs/user/test-and-share.md +127 -105
- package/apps/runtime/dist/docs/user/troubleshooting.md +40 -38
- package/apps/runtime/dist/docs/user/wordpress.md +62 -54
- package/apps/runtime/dist/documents/parse.js +2 -2
- package/apps/runtime/dist/engenty/shims/web-ingest.js +17 -1
- package/apps/runtime/dist/engine/input.js +4 -4
- package/apps/runtime/dist/engine/push.js +2 -2
- package/apps/runtime/dist/engine/runner.js +8 -8
- package/apps/runtime/dist/engine/steps.js +4 -4
- package/apps/runtime/dist/engine/surface.js +1 -1
- package/apps/runtime/dist/film/step.js +3 -3
- package/apps/runtime/dist/harness/claude.js +1 -1
- package/apps/runtime/dist/harness/codex.js +1 -1
- package/apps/runtime/dist/harness/model.js +2 -2
- package/apps/runtime/dist/harness/terminal.js +3 -3
- package/apps/runtime/dist/mcp/handler.js +2 -2
- package/apps/runtime/dist/mcp/tools.js +9 -9
- package/apps/runtime/dist/models.js +15 -15
- package/apps/runtime/dist/plugins/claims.js +56 -0
- package/apps/runtime/dist/plugins/loader.js +2 -0
- package/apps/runtime/dist/plugins/public.js +2 -3
- package/apps/runtime/dist/plugins/runs.js +13 -8
- package/apps/runtime/dist/render/chromium.js +24 -11
- package/apps/runtime/dist/routes/api.js +16 -10
- package/apps/runtime/dist/routes/debug.js +1 -1
- package/apps/runtime/dist/routes/delivery.js +46 -0
- package/apps/runtime/dist/routes/lang.js +7 -0
- package/apps/runtime/dist/routes/marketplace.js +2 -1
- package/apps/runtime/dist/routes/plugin-public.js +2 -1
- package/apps/runtime/dist/routes/plugins.js +13 -14
- package/apps/runtime/dist/routes/runs.js +59 -49
- package/apps/runtime/dist/routes/studio.js +58 -28
- package/apps/runtime/dist/services/access.js +3 -3
- package/apps/runtime/dist/services/assistant-chats.js +1 -1
- package/apps/runtime/dist/services/brand.js +2 -2
- package/apps/runtime/dist/services/cloud.js +5 -4
- package/apps/runtime/dist/services/errors.js +2 -1
- package/apps/runtime/dist/services/knowledge-model.js +10 -10
- package/apps/runtime/dist/services/knowledge.js +19 -19
- package/apps/runtime/dist/services/marketplace.js +2 -2
- package/apps/runtime/dist/services/package-file.js +12 -9
- package/apps/runtime/dist/services/package.js +3 -3
- package/apps/runtime/dist/services/project-files.js +15 -17
- package/apps/runtime/dist/services/project-index.js +10 -10
- package/apps/runtime/dist/services/projects.js +13 -8
- package/apps/runtime/dist/services/relevance.js +3 -3
- package/apps/runtime/dist/services/runners.js +4 -3
- package/apps/runtime/dist/services/runs.js +3 -3
- package/apps/runtime/dist/services/site.js +10 -10
- package/apps/runtime/dist/services/space-categories.js +22 -22
- package/apps/runtime/dist/services/space-data.js +16 -16
- package/apps/runtime/dist/services/space-origin.js +4 -4
- package/apps/runtime/dist/services/spaces.js +10 -10
- package/apps/runtime/dist/services/wizards.js +5 -5
- package/apps/runtime/dist/tools/index.js +1 -1
- package/apps/runtime/dist/tools/net-guard.js +135 -24
- package/apps/runtime/dist/tools/pages.js +4 -4
- package/apps/runtime/dist/tools/plugin.js +1 -1
- package/apps/runtime/dist/tools/project.js +10 -10
- package/apps/runtime/dist/tools/store.js +1 -1
- package/apps/runtime/dist/widgets/step.js +1 -1
- package/apps/web/dist/assets/AboutDialog-DaYwk84X.js +1 -0
- package/apps/web/dist/assets/{ChangelogDialog-Cm25slr3.js → ChangelogDialog-CsYbEayb.js} +1 -1
- package/apps/web/dist/assets/{ChatView-BXtXWnqK.js → ChatView-BRD3fKZU.js} +1 -1
- package/apps/web/dist/assets/EditorPage-ocPlZD3E.js +3 -0
- package/apps/web/dist/assets/{FlowPreview-BsfjDVv8.js → FlowPreview-Bm7sv746.js} +1 -1
- package/apps/web/dist/assets/{Harness-B0hD5Pp7.js → Harness-DKE-YNA5.js} +1 -1
- package/apps/web/dist/assets/{NewWizardChannels-DMDp9Ao5.js → NewWizardChannels-CnWckqjO.js} +1 -1
- package/apps/web/dist/assets/{NewWizardPage-aOisxyk2.js → NewWizardPage-BXTpbHVM.js} +1 -1
- package/apps/web/dist/assets/{OnboardingPage-Czb0pOyy.js → OnboardingPage-3Rz5uOsa.js} +1 -1
- package/apps/web/dist/assets/{ProjectPage-DPCtb42K.js → ProjectPage-QdSJwm3k.js} +1 -1
- package/apps/web/dist/assets/{PublicRunner-CvyA-gkf.js → PublicRunner-DEpBer5g.js} +1 -1
- package/apps/web/dist/assets/ReadOnly-rr_RJD-a.js +1 -0
- package/apps/web/dist/assets/{RunDrawer-BCiPEz9g.js → RunDrawer-BsZt0tL9.js} +1 -1
- package/apps/web/dist/assets/{RunnerView-4z6DMzE6.js → RunnerView-BbRukEdE.js} +1 -1
- package/apps/web/dist/assets/SettingsPage-qkWsQIMN.js +1 -0
- package/apps/web/dist/assets/{SetupPage-YFoU_nXK.js → SetupPage-PzvPPLfM.js} +1 -1
- package/apps/web/dist/assets/{SharePage-BBHiJ34e.js → SharePage-7vdDWG1n.js} +1 -1
- package/apps/web/dist/assets/changelog-BFdAYD28.js +1 -0
- package/apps/web/dist/assets/{esm-hSq-jeOG.js → esm-g0kaGp8t.js} +1 -1
- package/apps/web/dist/assets/{index-CSeBGwy0.css → index-Bi2x2g7n.css} +1 -1
- package/apps/web/dist/assets/{index-DB_iwB35.js → index-BzPid1oQ.js} +4 -4
- package/apps/web/dist/assets/lock-BK9RYIft.js +1 -0
- package/apps/web/dist/assets/oss-credits-BJrR7BR1.js +1 -0
- package/apps/web/dist/assets/{phone-CeqyvwK-.js → phone-DtkY0NfU.js} +1 -1
- package/apps/web/dist/assets/{settings-menu-BK8WpcGv.js → settings-menu-7EJQRFOi.js} +1 -1
- package/apps/web/dist/assets/{ui-BbIh6SOJ.js → ui-B6S5hYPd.js} +3 -3
- package/apps/web/dist/assets/{upload-BQrAlHIG.js → upload-rYCAy7ox.js} +1 -1
- package/apps/web/dist/index.html +3 -3
- package/apps/web/dist/mcp/flow.html +4 -4
- package/bin/wizards.mjs +1 -1
- package/node_modules/@engenty-wizards/plugin-sdk/dist/index.d.ts +33 -14
- package/node_modules/@engenty-wizards/plugin-sdk/dist/studio.d.ts +3 -3
- package/node_modules/@engenty-wizards/shared/dist/definition.d.ts +690 -690
- package/node_modules/@engenty-wizards/shared/dist/definition.js +1 -1
- package/node_modules/@engenty-wizards/shared/dist/engenty/data-tables/columns.d.ts +76 -76
- package/node_modules/@engenty-wizards/shared/dist/feature-flags.d.ts +3 -3
- package/node_modules/@engenty-wizards/shared/dist/feature-flags.js +3 -3
- package/node_modules/@engenty-wizards/shared/dist/knowledge.d.ts +13 -13
- package/node_modules/@engenty-wizards/shared/dist/knowledge.js +4 -4
- package/node_modules/@engenty-wizards/shared/dist/marketplace.d.ts +4 -4
- package/node_modules/@engenty-wizards/shared/dist/projects.js +1 -1
- package/node_modules/@engenty-wizards/shared/dist/run.d.ts +48 -48
- package/node_modules/@engenty-wizards/shared/dist/run.js +48 -48
- package/node_modules/@engenty-wizards/shared/dist/space-data.d.ts +2 -2
- package/node_modules/@engenty-wizards/shared/dist/surface.d.ts +20 -20
- package/node_modules/@engenty-wizards/shared/dist/wizard-package.js +1 -1
- package/package.json +5 -4
- package/apps/web/dist/assets/AboutDialog-BtAuA44m.js +0 -1
- package/apps/web/dist/assets/EditorPage-B4jX_Ckc.js +0 -3
- package/apps/web/dist/assets/ReadOnly-DFysMEMF.js +0 -1
- package/apps/web/dist/assets/SettingsPage-BKkOfz6P.js +0 -1
- package/apps/web/dist/assets/changelog-D8WjOq7d.js +0 -1
- package/apps/web/dist/assets/circle-alert-BVj6idzu.js +0 -1
- package/apps/web/dist/assets/lock-BoWS-JhN.js +0 -1
- package/apps/web/dist/assets/oss-credits-BQQX0kvk.js +0 -1
|
@@ -247,12 +247,12 @@ const MAKES = {
|
|
|
247
247
|
function missingText(cfg, cls) {
|
|
248
248
|
const bound = cfg.settings.bindings[cls];
|
|
249
249
|
if (isCreditsRef(bound)) {
|
|
250
|
-
return bilingual("Dafür
|
|
250
|
+
return bilingual("Dafür sind Credits gewählt, aber du bist mit keinem Konto angemeldet.", "This is set to run on credits, but you’re not signed in to an account.");
|
|
251
251
|
}
|
|
252
252
|
const makes = MAKES[cls];
|
|
253
253
|
const provider = bound ? providerInfo(providerOfRef(bound) ?? "") : null;
|
|
254
254
|
if (provider) {
|
|
255
|
-
return bilingual(`Für ${makes.de} ist ${provider.name} gewählt, aber kein API
|
|
255
|
+
return bilingual(`Für ${makes.de} ist ${provider.name} gewählt, aber kein API-Key hinterlegt.`, `${provider.name} is chosen for ${makes.en}, but no API key is set.`);
|
|
256
256
|
}
|
|
257
257
|
return bilingual(`Für ${makes.de} ist kein Modell eingerichtet.`, `No model is set up for ${makes.en}.`);
|
|
258
258
|
}
|
|
@@ -274,7 +274,7 @@ async function creditAccess(cfg, chosen) {
|
|
|
274
274
|
const token = await accountToken();
|
|
275
275
|
if (!token) {
|
|
276
276
|
if (chosen) {
|
|
277
|
-
throw new ModelUnavailableError(bilingual("Das Konto ist nicht mehr angemeldet.
|
|
277
|
+
throw new ModelUnavailableError(bilingual("Das Konto ist nicht mehr angemeldet. Melde dich unter Einstellungen → Konto neu an.", "The account is no longer signed in. Sign in again under Settings → Account."));
|
|
278
278
|
}
|
|
279
279
|
return null;
|
|
280
280
|
}
|
|
@@ -301,7 +301,7 @@ async function routeOf(cls) {
|
|
|
301
301
|
function need(cfg, name) {
|
|
302
302
|
const key = cfg.keys[name];
|
|
303
303
|
if (!key) {
|
|
304
|
-
throw new ModelUnavailableError(bilingual(`Für ${providerInfo(name)?.name ?? name} ist kein API
|
|
304
|
+
throw new ModelUnavailableError(bilingual(`Für ${providerInfo(name)?.name ?? name} ist kein API-Key hinterlegt.`, `No API key is set for ${providerInfo(name)?.name ?? name}.`));
|
|
305
305
|
}
|
|
306
306
|
return key;
|
|
307
307
|
}
|
|
@@ -412,7 +412,7 @@ async function ownLanguageModel(cfg, ref, meta) {
|
|
|
412
412
|
};
|
|
413
413
|
}
|
|
414
414
|
}
|
|
415
|
-
throw new ModelUnavailableError(bilingual(`
|
|
415
|
+
throw new ModelUnavailableError(bilingual(`Das Modell „${ref}“ lässt sich noch nicht nutzen. Hinterleg unter Einstellungen → Modelle einen API-Key oder melde dich mit einem Konto an.`, `The model “${ref}” can’t be used yet. Add an API key under Settings → Models, or sign in to an account.`));
|
|
416
416
|
}
|
|
417
417
|
/**
|
|
418
418
|
* The language model a text class runs on. A run watched from a device that offers the class
|
|
@@ -711,7 +711,7 @@ export function freeTierRefusal(err) {
|
|
|
711
711
|
}
|
|
712
712
|
/** What the person reads when Vercel's free tier leaves a model out, and how to fix it. */
|
|
713
713
|
export function freeTierText(model) {
|
|
714
|
-
return bilingual(`Dein AI
|
|
714
|
+
return bilingual(`Dein AI-Gateway-Key nutzt bei Vercel die kostenlose Stufe, und die schließt ${model ? `„${model}“` : "dieses Modell"} aus. Lade bei Vercel Guthaben auf (${GATEWAY_TOP_UP}) oder wähle unter Einstellungen → Modelle ein anderes Modell.`, `Your AI Gateway key is on Vercel’s free tier, which doesn’t include ${model ? `“${model}”` : "this model"}. Top up at Vercel (${GATEWAY_TOP_UP}) or choose another model under Settings → Models.`);
|
|
715
715
|
}
|
|
716
716
|
/** What a key was answered for a model: refusals are asked again soon, as a top-up lifts them. */
|
|
717
717
|
const gatewayProbes = new Map();
|
|
@@ -765,7 +765,7 @@ const CONVERSATION_MODEL = env.conversationModel || "gpt-realtime";
|
|
|
765
765
|
export async function conversationAccess() {
|
|
766
766
|
const cfg = await config();
|
|
767
767
|
if (!cfg.keys.openai) {
|
|
768
|
-
throw new ModelUnavailableError(bilingual("Für Sprachgespräche ist kein OpenAI
|
|
768
|
+
throw new ModelUnavailableError(bilingual("Für Sprachgespräche ist kein OpenAI-API-Key hinterlegt (Einstellungen → Modelle).", "No OpenAI API key is set for voice conversations (Settings → Models)."));
|
|
769
769
|
}
|
|
770
770
|
return { apiKey: cfg.keys.openai, model: CONVERSATION_MODEL };
|
|
771
771
|
}
|
|
@@ -802,11 +802,11 @@ export async function classProblem(cls) {
|
|
|
802
802
|
const named = creditsModelOf((await config()).settings.bindings[cls]);
|
|
803
803
|
if (named) {
|
|
804
804
|
return catalog?.models && !catalog.models.some((m) => m.id === named)
|
|
805
|
-
? bilingual("Dieses Modell gibt es
|
|
805
|
+
? bilingual("Dieses Modell gibt es mit deinem Konto nicht.", "This model isn’t available with your account.")
|
|
806
806
|
: null;
|
|
807
807
|
}
|
|
808
808
|
return catalog?.classes && !catalog.classes[cls]
|
|
809
|
-
? bilingual("Dafür ist
|
|
809
|
+
? bilingual("Dafür ist bei deinem Konto kein Modell eingerichtet.", "Your account has no model set up for this.")
|
|
810
810
|
: null;
|
|
811
811
|
}
|
|
812
812
|
const client = harness(resolved.vendor);
|
|
@@ -833,7 +833,7 @@ export async function filmClient(cls) {
|
|
|
833
833
|
const client = managed ? null : harness(cfg.settings.source);
|
|
834
834
|
if (!client || !FILM_CLIENTS.has(client.id)) {
|
|
835
835
|
return {
|
|
836
|
-
problem: bilingual("Filme macht Claude Code auf diesem Computer mit deinem Abo. Richte es unter
|
|
836
|
+
problem: bilingual("Filme macht Claude Code auf diesem Computer mit deinem Abo. Richte es unter Einstellungen → Modelle als KI-Abo ein.", "Claude Code makes films on this computer, using your subscription. Set it up as an AI subscription under Settings → Models."),
|
|
837
837
|
};
|
|
838
838
|
}
|
|
839
839
|
const status = await detectHarness(client.id);
|
|
@@ -920,7 +920,7 @@ async function appleProblem(cls) {
|
|
|
920
920
|
}
|
|
921
921
|
return (await hasFfmpeg())
|
|
922
922
|
? null
|
|
923
|
-
: bilingual("Aufnahmen aus Chrome oder Firefox (WebM, Ogg) schreibt dieser Mac erst mit ffmpeg auf
|
|
923
|
+
: bilingual("Aufnahmen aus Chrome oder Firefox (WebM, Ogg) schreibt dieser Mac erst mit ffmpeg auf. Safari und die App brauchen es nicht. Installiere ffmpeg, z. B. mit „brew install ffmpeg“.", "This Mac needs ffmpeg to transcribe recordings from Chrome or Firefox (WebM, Ogg). Safari and the app don’t. Install ffmpeg, e.g. with “brew install ffmpeg”.");
|
|
924
924
|
}
|
|
925
925
|
/** At start the ways are asked once, so a model the key cannot reach shows before a run meets it. */
|
|
926
926
|
export async function checkModelsAtStart() {
|
|
@@ -992,11 +992,11 @@ export async function checkKey(name, key, lang = "en") {
|
|
|
992
992
|
ok: false,
|
|
993
993
|
message: res.status === 401
|
|
994
994
|
? de
|
|
995
|
-
? "Der Anbieter kennt diesen Key nicht."
|
|
996
|
-
: "The provider
|
|
995
|
+
? "Der Anbieter kennt diesen API-Key nicht."
|
|
996
|
+
: "The provider doesn’t recognise this key."
|
|
997
997
|
: de
|
|
998
|
-
? `Der Anbieter antwortet mit ${res.status}.`
|
|
999
|
-
: `The provider
|
|
998
|
+
? `Der Anbieter antwortet mit Fehler ${res.status}.`
|
|
999
|
+
: `The provider answered with error ${res.status}.`,
|
|
1000
1000
|
};
|
|
1001
1001
|
}
|
|
1002
1002
|
catch {
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { and, eq } from "drizzle-orm";
|
|
2
|
+
import { control, controlDb } from "../db/client.js";
|
|
3
|
+
import { currentTenant } from "../tenants/tenant.js";
|
|
4
|
+
import { publicRefOf } from "./public.js";
|
|
5
|
+
/**
|
|
6
|
+
* `server.claims`: keys a plugin holds across the runtime's tenants, each for one tenant at a
|
|
7
|
+
* time (control `plugin_claim`). A key is the plugin's own; another plugin's of the same name is
|
|
8
|
+
* another key.
|
|
9
|
+
*/
|
|
10
|
+
const MAX_KEY = 200;
|
|
11
|
+
function checked(key) {
|
|
12
|
+
if (!key || key.length > MAX_KEY) {
|
|
13
|
+
throw Object.assign(new Error(`A key is 1 to ${MAX_KEY} characters.`), {
|
|
14
|
+
status: 400,
|
|
15
|
+
code: "invalid",
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
return key;
|
|
19
|
+
}
|
|
20
|
+
export function claimsApiFor(plugin) {
|
|
21
|
+
const where = (key) => and(eq(control.pluginClaim.plugin, plugin), eq(control.pluginClaim.key, checked(key)));
|
|
22
|
+
const holderOf = async (key) => (await controlDb.query.pluginClaim.findFirst({ where: where(key) }))?.tenantId ?? null;
|
|
23
|
+
return {
|
|
24
|
+
async take(key) {
|
|
25
|
+
const tenantId = currentTenant();
|
|
26
|
+
await controlDb
|
|
27
|
+
.insert(control.pluginClaim)
|
|
28
|
+
.values({ plugin, key: checked(key), tenantId, updatedAt: new Date() })
|
|
29
|
+
.onConflictDoNothing();
|
|
30
|
+
return (await holderOf(key)) === tenantId;
|
|
31
|
+
},
|
|
32
|
+
async put(key) {
|
|
33
|
+
const tenantId = currentTenant();
|
|
34
|
+
const updatedAt = new Date();
|
|
35
|
+
await controlDb
|
|
36
|
+
.insert(control.pluginClaim)
|
|
37
|
+
.values({ plugin, key: checked(key), tenantId, updatedAt })
|
|
38
|
+
.onConflictDoUpdate({
|
|
39
|
+
target: [control.pluginClaim.plugin, control.pluginClaim.key],
|
|
40
|
+
set: { tenantId, updatedAt },
|
|
41
|
+
});
|
|
42
|
+
},
|
|
43
|
+
async drop(key) {
|
|
44
|
+
await controlDb
|
|
45
|
+
.delete(control.pluginClaim)
|
|
46
|
+
.where(and(where(key), eq(control.pluginClaim.tenantId, currentTenant())));
|
|
47
|
+
},
|
|
48
|
+
async holder(key) {
|
|
49
|
+
const tenantId = await holderOf(key);
|
|
50
|
+
return tenantId ? publicRefOf(plugin, tenantId) : null;
|
|
51
|
+
},
|
|
52
|
+
async mine(key) {
|
|
53
|
+
return (await holderOf(key)) === currentTenant();
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
@@ -21,6 +21,7 @@ import { putIndexEntry, removeIndexEntries } from "../services/project-index.js"
|
|
|
21
21
|
import { originData } from "../services/space-origin.js";
|
|
22
22
|
import { safeFetch } from "../tools/net-guard.js";
|
|
23
23
|
import { channelsApi } from "./channels.js";
|
|
24
|
+
import { claimsApiFor } from "./claims.js";
|
|
24
25
|
import { discoverPlugins } from "./discovery.js";
|
|
25
26
|
import { migrateOpenTenants, migratePlugins } from "./migrations.js";
|
|
26
27
|
import { proModulesDir, proSources } from "./pro-state.js";
|
|
@@ -166,6 +167,7 @@ function apiFor(record) {
|
|
|
166
167
|
},
|
|
167
168
|
sharedUrl: (path) => sharedUrlOf(source.id, path),
|
|
168
169
|
channels: channelsApi,
|
|
170
|
+
claims: claimsApiFor(source.id),
|
|
169
171
|
runs: runsApiFor(source.id),
|
|
170
172
|
speech: {
|
|
171
173
|
async conversation() {
|
|
@@ -22,9 +22,8 @@ export async function publicUrlOf(plugin, path) {
|
|
|
22
22
|
export function sharedUrlOf(plugin, path) {
|
|
23
23
|
return `${env.publicUrl}/api/public/plugins/${plugin}/${SHARED}${rest(path)}`;
|
|
24
24
|
}
|
|
25
|
-
/** The ref of
|
|
26
|
-
export async function publicRefOf(plugin) {
|
|
27
|
-
const tenantId = currentTenant();
|
|
25
|
+
/** The ref of a tenant's public address for a plugin (the current tenant's by default), made the first time. */
|
|
26
|
+
export async function publicRefOf(plugin, tenantId = currentTenant()) {
|
|
28
27
|
const where = and(eq(control.pluginAddress.tenantId, tenantId), eq(control.pluginAddress.plugin, plugin));
|
|
29
28
|
let row = await controlDb.query.pluginAddress.findFirst({ where });
|
|
30
29
|
if (!row) {
|
|
@@ -11,6 +11,7 @@ import { saveAsset } from "../files/storage.js";
|
|
|
11
11
|
import { wizardUnavailable } from "../limits.js";
|
|
12
12
|
import { transcribeAudio } from "../media/transcribe.js";
|
|
13
13
|
import { errorText, ModelUnavailableError } from "../model-errors.js";
|
|
14
|
+
import { uploadMime } from "../routes/delivery.js";
|
|
14
15
|
import { runTicket } from "../secrets/signing.js";
|
|
15
16
|
import { notFound, ServiceError } from "../services/errors.js";
|
|
16
17
|
import { runnerSettingsOf, runnersOf } from "../services/runners.js";
|
|
@@ -51,7 +52,7 @@ export function runsApiFor(pluginId) {
|
|
|
51
52
|
}
|
|
52
53
|
const visitorId = visitorOf(input.person);
|
|
53
54
|
if (await visitorOverLimit(visitorId, null)) {
|
|
54
|
-
throw new ServiceError("refused",
|
|
55
|
+
throw new ServiceError("refused", noteText({ code: "tooManyRuns" }, wizardLang(version.definition)));
|
|
55
56
|
}
|
|
56
57
|
const runId = await createRun({
|
|
57
58
|
wizardId: w.id,
|
|
@@ -66,7 +67,9 @@ export function runsApiFor(pluginId) {
|
|
|
66
67
|
throw new ServiceError("refused", errorText(err, wizardLang(version.definition)));
|
|
67
68
|
}
|
|
68
69
|
if (err instanceof NoCreditsError) {
|
|
69
|
-
throw new ServiceError("no_credits",
|
|
70
|
+
throw new ServiceError("no_credits", wizardLang(version.definition) === "de"
|
|
71
|
+
? "Dieser Wizard hat gerade kein Guthaben mehr. Versuch es später noch einmal."
|
|
72
|
+
: "This wizard has run out of credits for now. Try again later.");
|
|
70
73
|
}
|
|
71
74
|
throw err;
|
|
72
75
|
});
|
|
@@ -126,9 +129,9 @@ export function runsApiFor(pluginId) {
|
|
|
126
129
|
await reviewStep(runId, stepId, action).catch(refusedInput);
|
|
127
130
|
},
|
|
128
131
|
async answerAsk(runId, askId, answer) {
|
|
129
|
-
await pluginRun(runId);
|
|
132
|
+
const run = await pluginRun(runId);
|
|
130
133
|
if (!(await answerAsk(runId, askId, answer === "allow" ? { type: "done" } : { type: "skip" }))) {
|
|
131
|
-
throw new ServiceError("refused",
|
|
134
|
+
throw new ServiceError("refused", noteText({ code: "askClosed" }, wizardLang(run.definition)));
|
|
132
135
|
}
|
|
133
136
|
},
|
|
134
137
|
async control(runId, action) {
|
|
@@ -173,10 +176,12 @@ export function runsApiFor(pluginId) {
|
|
|
173
176
|
},
|
|
174
177
|
async upload(runId, file) {
|
|
175
178
|
await pluginRun(runId);
|
|
179
|
+
// The type comes from the channel's sender: kept only when it is one a wizard takes.
|
|
180
|
+
const mime = uploadMime(file.mime);
|
|
176
181
|
const asset = await saveAsset({
|
|
177
182
|
runId,
|
|
178
|
-
kind:
|
|
179
|
-
mime
|
|
183
|
+
kind: mime.startsWith("image/") ? "image" : "file",
|
|
184
|
+
mime,
|
|
180
185
|
name: file.name,
|
|
181
186
|
data: file.data instanceof Uint8Array ? file.data : new Uint8Array(file.data),
|
|
182
187
|
});
|
|
@@ -185,11 +190,11 @@ export function runsApiFor(pluginId) {
|
|
|
185
190
|
async transcribe(runId, file) {
|
|
186
191
|
const run = await pluginRun(runId);
|
|
187
192
|
if (run.status === "done" || run.status === "cancelled" || run.status === "failed") {
|
|
188
|
-
throw new ServiceError("refused", "
|
|
193
|
+
throw new ServiceError("refused", noteText({ code: "runOver" }, wizardLang(run.definition)));
|
|
189
194
|
}
|
|
190
195
|
const bytes = file.data instanceof Uint8Array ? file.data : new Uint8Array(file.data);
|
|
191
196
|
if (!bytes.length || bytes.length > 10_000_000) {
|
|
192
|
-
throw new ServiceError("invalid",
|
|
197
|
+
throw new ServiceError("invalid", noteText({ code: "recordingTooLarge" }, wizardLang(run.definition)));
|
|
193
198
|
}
|
|
194
199
|
const { text } = await transcribeAudio({
|
|
195
200
|
bytes,
|
|
@@ -4,7 +4,8 @@ import { assertPublicUrl } from "../tools/net-guard.js";
|
|
|
4
4
|
/**
|
|
5
5
|
* One Chromium for the whole server: the engenty-browser container over CDP when
|
|
6
6
|
* `BROWSER_CDP_URL` is set, else a local headless Chrome. Every user of it opens
|
|
7
|
-
* its own context, so runs never share cookies or storage.
|
|
7
|
+
* its own context, so runs never share cookies or storage. Service workers are blocked: their
|
|
8
|
+
* requests would not pass a context's route guard.
|
|
8
9
|
*/
|
|
9
10
|
let browserPromise = null;
|
|
10
11
|
async function connect() {
|
|
@@ -31,8 +32,17 @@ export function getBrowser() {
|
|
|
31
32
|
}
|
|
32
33
|
export async function newContext(options = {}) {
|
|
33
34
|
const browser = await getBrowser();
|
|
34
|
-
return browser.newContext({
|
|
35
|
+
return browser.newContext({
|
|
36
|
+
viewport: { width: 1280, height: 900 },
|
|
37
|
+
serviceWorkers: "block",
|
|
38
|
+
...options,
|
|
39
|
+
});
|
|
35
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* Verdicts are kept only briefly: a name's addresses can change, and a page that rebinds its own
|
|
43
|
+
* name to a private address must not ride on an old "public".
|
|
44
|
+
*/
|
|
45
|
+
const HOST_CHECK_TTL_MS = 30_000;
|
|
36
46
|
const hostChecks = new Map();
|
|
37
47
|
function publicHost(url) {
|
|
38
48
|
let host;
|
|
@@ -42,25 +52,28 @@ function publicHost(url) {
|
|
|
42
52
|
catch {
|
|
43
53
|
return Promise.resolve(false);
|
|
44
54
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
55
|
+
const now = Date.now();
|
|
56
|
+
const cached = hostChecks.get(host);
|
|
57
|
+
if (cached && now - cached.at < HOST_CHECK_TTL_MS) {
|
|
58
|
+
return cached.check;
|
|
59
|
+
}
|
|
60
|
+
const check = assertPublicUrl(url).then(() => true, () => false);
|
|
61
|
+
hostChecks.set(host, { at: now, check });
|
|
62
|
+
if (hostChecks.size > 2000) {
|
|
63
|
+
hostChecks.clear();
|
|
52
64
|
}
|
|
53
65
|
return check;
|
|
54
66
|
}
|
|
55
67
|
/**
|
|
56
68
|
* A context for browsing the web on a person's behalf: every request, whoever started it (the
|
|
57
|
-
* agent, a redirect, the person clicking), may only reach public hosts.
|
|
69
|
+
* agent, a redirect, the person clicking), may only reach public hosts over http(s). Pages may
|
|
70
|
+
* still use their own data: and blob: URLs; every other scheme (file:, ftp:, ...) is refused.
|
|
58
71
|
*/
|
|
59
72
|
export async function webContext(options = {}) {
|
|
60
73
|
const context = await newContext({ acceptDownloads: true, ...options });
|
|
61
74
|
await context.route("**/*", async (route) => {
|
|
62
75
|
const url = route.request().url();
|
|
63
|
-
if (
|
|
76
|
+
if (/^(data|blob):/i.test(url) || (/^https?:/i.test(url) && (await publicHost(url)))) {
|
|
64
77
|
await route.continue();
|
|
65
78
|
}
|
|
66
79
|
else {
|
|
@@ -20,7 +20,7 @@ import { siteJob, siteSchema, startSiteJob } from "../services/site.js";
|
|
|
20
20
|
import { checkOnServer, checkSchema, listSyncedSpaces, patchSyncedWizard, removeSyncedSpace, removeSyncedWizard, rotateSyncedLink, syncDataSchema, syncSettingsSchema, syncSpaceData, syncWizard, syncWizardSchema, } from "../services/spaces.js";
|
|
21
21
|
import { createWizard, ownedWizard, publishWizard, writeDraft } from "../services/wizards.js";
|
|
22
22
|
import { codeOf, tenantStatus } from "../tenants/control.js";
|
|
23
|
-
import { inLang } from "./lang.js";
|
|
23
|
+
import { inLang, notFoundAnswer } from "./lang.js";
|
|
24
24
|
import { dispatchAs } from "./plugins.js";
|
|
25
25
|
const importSchema = z.object({
|
|
26
26
|
/** An earlier import's wizard: it is updated instead of a new one being made. */
|
|
@@ -44,7 +44,7 @@ const PRO_PLUGINS = ["contacts", "appointments"];
|
|
|
44
44
|
const WIZARD_BYTES = 48 * 1024 * 1024;
|
|
45
45
|
const tooLarge = bodyLimit({
|
|
46
46
|
maxSize: WIZARD_BYTES,
|
|
47
|
-
onError: (c) => c.json({ error: "
|
|
47
|
+
onError: (c) => c.json({ error: "The request is too large.", code: "too_large" }, 413),
|
|
48
48
|
});
|
|
49
49
|
/**
|
|
50
50
|
* The caller of an API call, signed in like an MCP client (access token or API key), with the
|
|
@@ -57,10 +57,13 @@ async function caller(c, scopes) {
|
|
|
57
57
|
}
|
|
58
58
|
const who = principalOf(authInfo);
|
|
59
59
|
if (scopes.some((scope) => !who.scopes.includes(scope))) {
|
|
60
|
-
return c.json({ error: "This token
|
|
60
|
+
return c.json({ error: "This token isn’t allowed to do that.", code: "insufficient_scope" }, 403);
|
|
61
61
|
}
|
|
62
62
|
if ((await tenantStatus(who.tenantId)) === "suspended") {
|
|
63
|
-
return c.json({
|
|
63
|
+
return c.json({
|
|
64
|
+
error: inLang(c, "Dieses Konto ist gesperrt.", "This account is suspended."),
|
|
65
|
+
code: "tenant_suspended",
|
|
66
|
+
}, 403);
|
|
64
67
|
}
|
|
65
68
|
return who;
|
|
66
69
|
}
|
|
@@ -69,7 +72,7 @@ function withinSyncLimit(c, who) {
|
|
|
69
72
|
return allowed(`sync:${who.tenantId}`, 240, HOUR)
|
|
70
73
|
? null
|
|
71
74
|
: c.json({
|
|
72
|
-
error: inLang(c, "Zu viele Übertragungen.
|
|
75
|
+
error: inLang(c, "Zu viele Übertragungen. Versuch es später noch einmal.", "Too many transfers. Try again later."),
|
|
73
76
|
code: "rate_limited",
|
|
74
77
|
}, 429);
|
|
75
78
|
}
|
|
@@ -91,7 +94,7 @@ async function pluginCall(c) {
|
|
|
91
94
|
code: "plugin",
|
|
92
95
|
plugin: id,
|
|
93
96
|
}, 403)
|
|
94
|
-
: c.json(
|
|
97
|
+
: c.json(notFoundAnswer(c), 404);
|
|
95
98
|
}
|
|
96
99
|
return inTenantAs(who, () => dispatchAs(c, {
|
|
97
100
|
id: who.userId,
|
|
@@ -271,7 +274,7 @@ export const apiRoutes = new Hono()
|
|
|
271
274
|
}
|
|
272
275
|
return inTenantAs(who, async () => c.json(await apiRun(who.userId, c.req.param("id"))));
|
|
273
276
|
})
|
|
274
|
-
// The space's own details: title, about, colours, facts, logo, how much
|
|
277
|
+
// The space's own details: title, about, colours, facts, logo, how much Knowledge it holds.
|
|
275
278
|
.get("/space", async (c) => {
|
|
276
279
|
const who = await caller(c, ["wizards:read"]);
|
|
277
280
|
if (who instanceof Response) {
|
|
@@ -287,7 +290,7 @@ export const apiRoutes = new Hono()
|
|
|
287
290
|
}
|
|
288
291
|
if (!allowed(`site:${who.tenantId}`, 20, HOUR)) {
|
|
289
292
|
return c.json({
|
|
290
|
-
error: inLang(c, "Zu oft hintereinander.
|
|
293
|
+
error: inLang(c, "Zu oft hintereinander. Versuch es später noch einmal.", "Too many times in a row. Try again later."),
|
|
291
294
|
code: "rate_limited",
|
|
292
295
|
}, 429);
|
|
293
296
|
}
|
|
@@ -389,10 +392,13 @@ export const apiRoutes = new Hono()
|
|
|
389
392
|
const body = importSchema.parse(await c.req.json());
|
|
390
393
|
if (!who.scopes.includes("wizards:write") ||
|
|
391
394
|
(body.publish && !who.scopes.includes("wizards:publish"))) {
|
|
392
|
-
return c.json({ error: "This token
|
|
395
|
+
return c.json({ error: "This token isn’t allowed to do that.", code: "insufficient_scope" }, 403);
|
|
393
396
|
}
|
|
394
397
|
if ((await tenantStatus(who.tenantId)) === "suspended") {
|
|
395
|
-
return c.json({
|
|
398
|
+
return c.json({
|
|
399
|
+
error: inLang(c, "Dieses Konto ist gesperrt.", "This account is suspended."),
|
|
400
|
+
code: "tenant_suspended",
|
|
401
|
+
}, 403);
|
|
396
402
|
}
|
|
397
403
|
return inTenantAs(who, async () => {
|
|
398
404
|
const writer = { source: "mcp", client: who.client };
|
|
@@ -15,7 +15,7 @@ export const localDebugRoutes = new Hono()
|
|
|
15
15
|
.use(async (c, next) => {
|
|
16
16
|
const ticket = c.req.header(LOCAL_MCP_HEADER) ?? "";
|
|
17
17
|
if (managed || !localTicketExpiry(env.authSecret, ticket, Date.now(), "local-debug")) {
|
|
18
|
-
return c.json({ error: "unauthorized" }, 401);
|
|
18
|
+
return c.json({ error: "This ticket isn’t valid.", code: "unauthorized" }, 401);
|
|
19
19
|
}
|
|
20
20
|
await next();
|
|
21
21
|
})
|
|
@@ -3,6 +3,52 @@ import { basePath } from "../env.js";
|
|
|
3
3
|
export function uploadLimit(mime) {
|
|
4
4
|
return mime.startsWith("video/") ? 40_000_000 : 15_000_000;
|
|
5
5
|
}
|
|
6
|
+
/** `type/subtype` and nothing else: no commas, spaces or quotes a browser could read another type from. */
|
|
7
|
+
const MIME = /^[a-z0-9][a-z0-9!#$&^_.+-]*\/[a-z0-9][a-z0-9!#$&^_.+-]*$/;
|
|
8
|
+
/** The bare type of a declared one ("audio/webm;codecs=opus" → "audio/webm"), or null if it is not one. */
|
|
9
|
+
export function bareMime(declared) {
|
|
10
|
+
const mime = declared.split(";")[0].trim().toLowerCase();
|
|
11
|
+
return MIME.test(mime) ? mime : null;
|
|
12
|
+
}
|
|
13
|
+
/** What a person hands a wizard besides photos, recordings and clips: documents and tables. */
|
|
14
|
+
const DOCUMENT_TYPES = new Set([
|
|
15
|
+
"application/pdf",
|
|
16
|
+
"application/msword",
|
|
17
|
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
18
|
+
"application/vnd.ms-excel",
|
|
19
|
+
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
20
|
+
"application/vnd.ms-powerpoint",
|
|
21
|
+
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
22
|
+
"application/vnd.oasis.opendocument.text",
|
|
23
|
+
"application/vnd.oasis.opendocument.spreadsheet",
|
|
24
|
+
"application/vnd.oasis.opendocument.presentation",
|
|
25
|
+
"application/rtf",
|
|
26
|
+
"application/json",
|
|
27
|
+
"application/zip",
|
|
28
|
+
"message/rfc822",
|
|
29
|
+
"text/plain",
|
|
30
|
+
"text/csv",
|
|
31
|
+
"text/tab-separated-values",
|
|
32
|
+
"text/markdown",
|
|
33
|
+
"text/calendar",
|
|
34
|
+
"text/vcard",
|
|
35
|
+
]);
|
|
36
|
+
/**
|
|
37
|
+
* The type an upload is kept as: the browser's word for it, when it is a picture, a recording, a
|
|
38
|
+
* clip or a document. Anything else (an HTML page, a script) is kept as plain bytes; reading it
|
|
39
|
+
* goes by its name (documents/parse.ts).
|
|
40
|
+
*/
|
|
41
|
+
export function uploadMime(declared) {
|
|
42
|
+
const mime = bareMime(declared);
|
|
43
|
+
return mime && (/^(image|audio|video)\//.test(mime) || DOCUMENT_TYPES.has(mime))
|
|
44
|
+
? mime
|
|
45
|
+
: "application/octet-stream";
|
|
46
|
+
}
|
|
47
|
+
/** What a browser shows in place without running anything of the file's: photos, sound, film, PDFs. */
|
|
48
|
+
export function inlineSafe(mime) {
|
|
49
|
+
return ((/^(image|audio|video)\//.test(mime) && !mime.startsWith("image/svg")) ||
|
|
50
|
+
mime === "application/pdf");
|
|
51
|
+
}
|
|
6
52
|
/** `version`: the published one, so a new engenty or name reaches icons a phone has kept. */
|
|
7
53
|
export function wizardManifest(token, title, description, version = 0) {
|
|
8
54
|
const start = `${basePath}/w/${token}`;
|
|
@@ -11,3 +11,10 @@ export function requestLang(c) {
|
|
|
11
11
|
export function inLang(c, de, en) {
|
|
12
12
|
return requestLang(c) === "de" ? de : en;
|
|
13
13
|
}
|
|
14
|
+
/** A 404 the person may read: a sentence in their language, the machine code beside it. */
|
|
15
|
+
export function notFoundAnswer(c) {
|
|
16
|
+
return {
|
|
17
|
+
error: inLang(c, "Das gibt es nicht oder nicht mehr.", "This doesn’t exist or was deleted."),
|
|
18
|
+
code: "not_found",
|
|
19
|
+
};
|
|
20
|
+
}
|
|
@@ -2,6 +2,7 @@ import { CAPABILITY_IDS, INDUSTRY_IDS, ITEM_FORMAT_IDS, MARKETPLACE_PLAN_IDS, US
|
|
|
2
2
|
import { Hono } from "hono";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { asLang, marketplaceDetail, searchMarketplace, starEntry, } from "../services/marketplace.js";
|
|
5
|
+
import { notFoundAnswer } from "./lang.js";
|
|
5
6
|
const searchSchema = z.object({
|
|
6
7
|
q: z.string().trim().max(200).default(""),
|
|
7
8
|
lang: z.string().optional(),
|
|
@@ -25,7 +26,7 @@ export const marketplaceStudio = new Hono()
|
|
|
25
26
|
})
|
|
26
27
|
.get("/:id", async (c) => {
|
|
27
28
|
const detail = await marketplaceDetail(c.req.param("id"), asLang(c.req.query("lang")));
|
|
28
|
-
return detail ? c.json(detail) : c.json(
|
|
29
|
+
return detail ? c.json(detail) : c.json(notFoundAnswer(c), 404);
|
|
29
30
|
})
|
|
30
31
|
.put("/:id/star", async (c) => {
|
|
31
32
|
await starEntry(c.get("user").tenantId, c.req.param("id"), true);
|
|
@@ -3,6 +3,7 @@ import { withTenant } from "../db/client.js";
|
|
|
3
3
|
import { SHARED, tenantOfAddress } from "../plugins/public.js";
|
|
4
4
|
import { loadedPlugin, pluginIdsOf } from "../plugins/registry.js";
|
|
5
5
|
import { tenantStatus } from "../tenants/control.js";
|
|
6
|
+
import { notFoundAnswer } from "./lang.js";
|
|
6
7
|
import { answerOf } from "./plugins.js";
|
|
7
8
|
/**
|
|
8
9
|
* Plugins' public routes (`server.registerPublicRoute`), below `/api/public/plugins/<id>/<ref>`.
|
|
@@ -10,7 +11,7 @@ import { answerOf } from "./plugins.js";
|
|
|
10
11
|
* Shared routes (`server.registerSharedRoute`) are below `/api/public/plugins/<id>/shared`, for
|
|
11
12
|
* every tenant; their handler enters a tenant by the ref the call carries.
|
|
12
13
|
*/
|
|
13
|
-
const notFound = (c) => c.json(
|
|
14
|
+
const notFound = (c) => c.json(notFoundAnswer(c), 404);
|
|
14
15
|
/** What a handler threw, as the answer: its status and code, or a 500. */
|
|
15
16
|
function failed(c, id, what, err) {
|
|
16
17
|
const answer = answerOf(err);
|
|
@@ -5,6 +5,7 @@ import { managed } from "../manage.js";
|
|
|
5
5
|
import { pluginProblems, reloadPlugin, rescanPlugins } from "../plugins/loader.js";
|
|
6
6
|
import { installPro, proState, refreshPro, removePro } from "../plugins/pro.js";
|
|
7
7
|
import { loadedPlugin, onPluginChange, pluginAsset, pluginIdsOf, pluginsOf, } from "../plugins/registry.js";
|
|
8
|
+
import { notFoundAnswer } from "./lang.js";
|
|
8
9
|
const admin = (user) => user.role === "owner" || user.role === "admin";
|
|
9
10
|
/** Plugins change while the app runs only where it runs alone: a reload would hit every tenant. */
|
|
10
11
|
const canReload = (user) => !managed && admin(user);
|
|
@@ -49,7 +50,7 @@ export async function dispatchAs(c, user) {
|
|
|
49
50
|
const id = c.req.param("id") ?? "";
|
|
50
51
|
const plugin = (await pluginIdsOf(user.tenantId)).has(id) ? loadedPlugin(id) : undefined;
|
|
51
52
|
if (!plugin) {
|
|
52
|
-
return c.json(
|
|
53
|
+
return c.json(notFoundAnswer(c), 404);
|
|
53
54
|
}
|
|
54
55
|
const url = new URL(c.req.url);
|
|
55
56
|
const path = url.pathname.slice(url.pathname.indexOf(`/plugins/${id}`) + `/plugins/${id}`.length);
|
|
@@ -59,7 +60,7 @@ export async function dispatchAs(c, user) {
|
|
|
59
60
|
continue;
|
|
60
61
|
}
|
|
61
62
|
if (route.role === "admin" && !admin(user)) {
|
|
62
|
-
return c.json({ error: "
|
|
63
|
+
return c.json({ error: "This isn’t allowed here.", code: "forbidden" }, 403);
|
|
63
64
|
}
|
|
64
65
|
const request = {
|
|
65
66
|
request: c.req.raw,
|
|
@@ -91,7 +92,7 @@ export async function dispatchAs(c, user) {
|
|
|
91
92
|
return c.json({ error: "The plugin failed.", code: "plugin_failed" }, 500);
|
|
92
93
|
}
|
|
93
94
|
}
|
|
94
|
-
return c.json(
|
|
95
|
+
return c.json(notFoundAnswer(c), 404);
|
|
95
96
|
}
|
|
96
97
|
const dispatch = (c) => dispatchAs(c, c.get("user"));
|
|
97
98
|
const TYPES = {
|
|
@@ -117,7 +118,7 @@ export const pluginRoutes = new Hono()
|
|
|
117
118
|
const plugin = (await pluginIdsOf(user.tenantId)).has(id) ? loadedPlugin(id) : undefined;
|
|
118
119
|
const asset = plugin && kind ? pluginAsset(plugin, kind.kind) : null;
|
|
119
120
|
if (!asset || !kind) {
|
|
120
|
-
return c.json(
|
|
121
|
+
return c.json(notFoundAnswer(c), 404);
|
|
121
122
|
}
|
|
122
123
|
return c.body(new Uint8Array(await readFile(asset.file)), 200, {
|
|
123
124
|
"content-type": kind.type,
|
|
@@ -127,7 +128,7 @@ export const pluginRoutes = new Hono()
|
|
|
127
128
|
// Says when a plugin loaded again, so an open studio takes the new one without a page load.
|
|
128
129
|
.get("/-/events", (c) => {
|
|
129
130
|
if (managed) {
|
|
130
|
-
return c.json(
|
|
131
|
+
return c.json(notFoundAnswer(c), 404);
|
|
131
132
|
}
|
|
132
133
|
return streamSSE(c, async (stream) => {
|
|
133
134
|
let closed = false;
|
|
@@ -149,37 +150,35 @@ export const pluginRoutes = new Hono()
|
|
|
149
150
|
})
|
|
150
151
|
.post("/-/reload", async (c) => {
|
|
151
152
|
if (!canReload(c.get("user"))) {
|
|
152
|
-
return c.json({ error: "
|
|
153
|
+
return c.json({ error: "This isn’t allowed here.", code: "forbidden" }, 403);
|
|
153
154
|
}
|
|
154
155
|
await rescanPlugins();
|
|
155
156
|
return c.json({ plugins: (await pluginsOf(c.get("user").tenantId)).map(view) });
|
|
156
157
|
})
|
|
157
158
|
.post("/-/reload/:id", async (c) => {
|
|
158
159
|
if (!canReload(c.get("user"))) {
|
|
159
|
-
return c.json({ error: "
|
|
160
|
+
return c.json({ error: "This isn’t allowed here.", code: "forbidden" }, 403);
|
|
160
161
|
}
|
|
161
162
|
const plugin = await reloadPlugin(c.req.param("id"));
|
|
162
|
-
return plugin
|
|
163
|
-
? c.json({ plugin: view(plugin) })
|
|
164
|
-
: c.json({ error: "not found", code: "not_found" }, 404);
|
|
163
|
+
return plugin ? c.json({ plugin: view(plugin) }) : c.json(notFoundAnswer(c), 404);
|
|
165
164
|
})
|
|
166
165
|
// Pro modules: the closed plugins the linked account's plan includes (plugins/pro.ts).
|
|
167
166
|
.get("/-/pro", async (c) => {
|
|
168
167
|
if (!canReload(c.get("user"))) {
|
|
169
|
-
return c.json(
|
|
168
|
+
return c.json(notFoundAnswer(c), 404);
|
|
170
169
|
}
|
|
171
170
|
return c.json(await proState());
|
|
172
171
|
})
|
|
173
172
|
.post("/-/pro/refresh", async (c) => {
|
|
174
173
|
if (!canReload(c.get("user"))) {
|
|
175
|
-
return c.json({ error: "
|
|
174
|
+
return c.json({ error: "This isn’t allowed here.", code: "forbidden" }, 403);
|
|
176
175
|
}
|
|
177
176
|
await refreshPro();
|
|
178
177
|
return c.json(await proState());
|
|
179
178
|
})
|
|
180
179
|
.post("/-/pro/:id", async (c) => {
|
|
181
180
|
if (!canReload(c.get("user"))) {
|
|
182
|
-
return c.json({ error: "
|
|
181
|
+
return c.json({ error: "This isn’t allowed here.", code: "forbidden" }, 403);
|
|
183
182
|
}
|
|
184
183
|
try {
|
|
185
184
|
await installPro(c.req.param("id"));
|
|
@@ -195,7 +194,7 @@ export const pluginRoutes = new Hono()
|
|
|
195
194
|
})
|
|
196
195
|
.delete("/-/pro/:id", async (c) => {
|
|
197
196
|
if (!canReload(c.get("user"))) {
|
|
198
|
-
return c.json({ error: "
|
|
197
|
+
return c.json({ error: "This isn’t allowed here.", code: "forbidden" }, 403);
|
|
199
198
|
}
|
|
200
199
|
try {
|
|
201
200
|
await removePro(c.req.param("id"));
|