wizards 0.1.9
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/LICENSE +110 -0
- package/README.md +170 -0
- package/apps/runtime/dist/agents/architect.js +453 -0
- package/apps/runtime/dist/agents/project-assistant.js +354 -0
- package/apps/runtime/dist/agents/subscription.js +206 -0
- package/apps/runtime/dist/agents/thought.js +21 -0
- package/apps/runtime/dist/agents/website.js +197 -0
- package/apps/runtime/dist/app.js +153 -0
- package/apps/runtime/dist/auth/account.js +121 -0
- package/apps/runtime/dist/auth/index.js +329 -0
- package/apps/runtime/dist/auth/keys.js +55 -0
- package/apps/runtime/dist/auth/local-ticket.js +23 -0
- package/apps/runtime/dist/authoring/example.js +73 -0
- package/apps/runtime/dist/authoring/guide.js +259 -0
- package/apps/runtime/dist/authoring/ops.js +117 -0
- package/apps/runtime/dist/cli/autostart.js +134 -0
- package/apps/runtime/dist/cli/clients.js +44 -0
- package/apps/runtime/dist/cli/environment.js +81 -0
- package/apps/runtime/dist/cli/exec.js +45 -0
- package/apps/runtime/dist/cli/home.js +74 -0
- package/apps/runtime/dist/cli/machine.js +145 -0
- package/apps/runtime/dist/cli/main.js +125 -0
- package/apps/runtime/dist/cli/setup.js +273 -0
- package/apps/runtime/dist/cli/start.js +190 -0
- package/apps/runtime/dist/cli/status.js +120 -0
- package/apps/runtime/dist/cli/ui.js +12 -0
- package/apps/runtime/dist/cli/update.js +58 -0
- package/apps/runtime/dist/connectors/builtin.js +70 -0
- package/apps/runtime/dist/connectors/external.js +400 -0
- package/apps/runtime/dist/connectors/gmail.js +107 -0
- package/apps/runtime/dist/connectors/imap.js +217 -0
- package/apps/runtime/dist/connectors/index.js +266 -0
- package/apps/runtime/dist/connectors/mail.js +72 -0
- package/apps/runtime/dist/connectors/outlook.js +90 -0
- package/apps/runtime/dist/credits/credits.js +103 -0
- package/apps/runtime/dist/credits/estimate.js +132 -0
- package/apps/runtime/dist/db/adopt.js +149 -0
- package/apps/runtime/dist/db/client.js +146 -0
- package/apps/runtime/dist/db/control-schema.js +70 -0
- package/apps/runtime/dist/db/migrations/control/0000_last_absorbing_man.sql +47 -0
- package/apps/runtime/dist/db/migrations/control/0001_gorgeous_nicolaos.sql +41 -0
- package/apps/runtime/dist/db/migrations/control/0002_secret_penance.sql +1 -0
- package/apps/runtime/dist/db/migrations/control/0003_nasty_mordo.sql +3 -0
- package/apps/runtime/dist/db/migrations/control/0004_breezy_eddie_brock.sql +6 -0
- package/apps/runtime/dist/db/migrations/control/meta/0000_snapshot.json +311 -0
- package/apps/runtime/dist/db/migrations/control/meta/0001_snapshot.json +599 -0
- package/apps/runtime/dist/db/migrations/control/meta/0002_snapshot.json +607 -0
- package/apps/runtime/dist/db/migrations/control/meta/0003_snapshot.json +311 -0
- package/apps/runtime/dist/db/migrations/control/meta/0004_snapshot.json +350 -0
- package/apps/runtime/dist/db/migrations/control/meta/_journal.json +41 -0
- package/apps/runtime/dist/db/migrations/tenant/0000_eminent_freak.sql +188 -0
- package/apps/runtime/dist/db/migrations/tenant/0001_salty_master_chief.sql +1 -0
- package/apps/runtime/dist/db/migrations/tenant/0002_cloudy_firebird.sql +1 -0
- package/apps/runtime/dist/db/migrations/tenant/0003_brown_warbound.sql +52 -0
- package/apps/runtime/dist/db/migrations/tenant/0004_romantic_nocturne.sql +6 -0
- package/apps/runtime/dist/db/migrations/tenant/meta/0000_snapshot.json +1323 -0
- package/apps/runtime/dist/db/migrations/tenant/meta/0001_snapshot.json +1330 -0
- package/apps/runtime/dist/db/migrations/tenant/meta/0002_snapshot.json +1337 -0
- package/apps/runtime/dist/db/migrations/tenant/meta/0003_snapshot.json +1605 -0
- package/apps/runtime/dist/db/migrations/tenant/meta/0004_snapshot.json +1645 -0
- package/apps/runtime/dist/db/migrations/tenant/meta/_journal.json +41 -0
- package/apps/runtime/dist/db/schema.js +278 -0
- package/apps/runtime/dist/documents/parse.js +230 -0
- package/apps/runtime/dist/engenty/connections-external/build-connector.js +193 -0
- package/apps/runtime/dist/engenty/connections-external/errors.js +11 -0
- package/apps/runtime/dist/engenty/connections-external/import-service.js +270 -0
- package/apps/runtime/dist/engenty/connections-external/importer/classify.js +34 -0
- package/apps/runtime/dist/engenty/connections-external/importer/map-auth.js +185 -0
- package/apps/runtime/dist/engenty/connections-external/importer/normalize-mcp.js +63 -0
- package/apps/runtime/dist/engenty/connections-external/importer/normalize-openapi.js +130 -0
- package/apps/runtime/dist/engenty/connections-external/invoke/http-invoker.js +143 -0
- package/apps/runtime/dist/engenty/connections-external/invoke/mcp-client.js +143 -0
- package/apps/runtime/dist/engenty/connections-external/net/guarded-fetch.js +119 -0
- package/apps/runtime/dist/engenty/connections-external/oauth-dcr.js +88 -0
- package/apps/runtime/dist/engenty/connections-external/registry-client.js +336 -0
- package/apps/runtime/dist/engenty/connections-external/registry-source.js +82 -0
- package/apps/runtime/dist/engenty/connections-external/types.js +7 -0
- package/apps/runtime/dist/engenty/connections-github/connector.js +209 -0
- package/apps/runtime/dist/engenty/connections-google/connectors/calendar.js +359 -0
- package/apps/runtime/dist/engenty/connections-google/connectors/contacts.js +94 -0
- package/apps/runtime/dist/engenty/connections-google/connectors/drive.js +290 -0
- package/apps/runtime/dist/engenty/connections-google/connectors/gmail.js +567 -0
- package/apps/runtime/dist/engenty/connections-google/definitions.js +11 -0
- package/apps/runtime/dist/engenty/connections-google/shared.js +73 -0
- package/apps/runtime/dist/engenty/connections-hubspot/connector.js +140 -0
- package/apps/runtime/dist/engenty/connections-microsoft/action.js +17 -0
- package/apps/runtime/dist/engenty/connections-microsoft/graph.js +114 -0
- package/apps/runtime/dist/engenty/connections-microsoft/onedrive.js +286 -0
- package/apps/runtime/dist/engenty/connections-microsoft/outlook.js +464 -0
- package/apps/runtime/dist/engenty/connections-s3/s3.js +328 -0
- package/apps/runtime/dist/engenty/connections-sdk/files-capability.js +86 -0
- package/apps/runtime/dist/engenty/connections-sdk/oauth2.js +150 -0
- package/apps/runtime/dist/engenty/connections-sdk/registry.js +89 -0
- package/apps/runtime/dist/engenty/connections-sdk/storage-capability.js +84 -0
- package/apps/runtime/dist/engenty/connections-sdk/types.js +30 -0
- package/apps/runtime/dist/engenty/connections-slack/connector.js +360 -0
- package/apps/runtime/dist/engenty/csv-import/csv-matrix.js +178 -0
- package/apps/runtime/dist/engenty/csv-import/import-sources.js +87 -0
- package/apps/runtime/dist/engenty/csv-import/parse-csv.js +129 -0
- package/apps/runtime/dist/engenty/csv-import/serialize-csv.js +20 -0
- package/apps/runtime/dist/engenty/csv-import/types.js +1 -0
- package/apps/runtime/dist/engenty/csv-import/xlsx-workbook.js +159 -0
- package/apps/runtime/dist/engenty/doc-converter/interface.js +9 -0
- package/apps/runtime/dist/engenty/doc-converter/page-break.js +92 -0
- package/apps/runtime/dist/engenty/doc-converter/providers/local/index.js +72 -0
- package/apps/runtime/dist/engenty/document-scanner/schemas/classifier.js +30 -0
- package/apps/runtime/dist/engenty/document-scanner/schemas/index.js +4 -0
- package/apps/runtime/dist/engenty/document-scanner/schemas/invoice.js +73 -0
- package/apps/runtime/dist/engenty/document-scanner/schemas/receipt.js +63 -0
- package/apps/runtime/dist/engenty/document-scanner/schemas/shared.js +40 -0
- package/apps/runtime/dist/engenty/search-index/chunks.js +164 -0
- package/apps/runtime/dist/engenty/search-index/contracts.js +9 -0
- package/apps/runtime/dist/engenty/shims/connections-sdk.js +39 -0
- package/apps/runtime/dist/engenty/shims/plugin-sdk.js +1 -0
- package/apps/runtime/dist/engenty/shims/web-ingest.js +3 -0
- package/apps/runtime/dist/engenty/web-ingest/ssrf.js +277 -0
- package/apps/runtime/dist/engine/asks.js +60 -0
- package/apps/runtime/dist/engine/events.js +46 -0
- package/apps/runtime/dist/engine/input.js +170 -0
- package/apps/runtime/dist/engine/prepare.js +51 -0
- package/apps/runtime/dist/engine/requirements.js +124 -0
- package/apps/runtime/dist/engine/resources.js +89 -0
- package/apps/runtime/dist/engine/runner.js +560 -0
- package/apps/runtime/dist/engine/steps.js +598 -0
- package/apps/runtime/dist/engine/template.js +183 -0
- package/apps/runtime/dist/engine/types.js +3 -0
- package/apps/runtime/dist/env.js +224 -0
- package/apps/runtime/dist/files/blobs.js +22 -0
- package/apps/runtime/dist/files/objects.js +86 -0
- package/apps/runtime/dist/files/storage.js +95 -0
- package/apps/runtime/dist/geocode.js +58 -0
- package/apps/runtime/dist/harness/claude.js +138 -0
- package/apps/runtime/dist/harness/codex.js +228 -0
- package/apps/runtime/dist/harness/cursor.js +93 -0
- package/apps/runtime/dist/harness/env.js +124 -0
- package/apps/runtime/dist/harness/gemini.js +126 -0
- package/apps/runtime/dist/harness/index.js +99 -0
- package/apps/runtime/dist/harness/model.js +267 -0
- package/apps/runtime/dist/harness/prompt.js +121 -0
- package/apps/runtime/dist/harness/terminal.js +193 -0
- package/apps/runtime/dist/harness/types.js +7 -0
- package/apps/runtime/dist/index.js +76 -0
- package/apps/runtime/dist/limits.js +57 -0
- package/apps/runtime/dist/link-preview.js +59 -0
- package/apps/runtime/dist/manage.js +151 -0
- package/apps/runtime/dist/mcp/auth.js +102 -0
- package/apps/runtime/dist/mcp/bridge.js +59 -0
- package/apps/runtime/dist/mcp/handler.js +42 -0
- package/apps/runtime/dist/mcp/scopes.js +5 -0
- package/apps/runtime/dist/mcp/tools.js +401 -0
- package/apps/runtime/dist/media/ffmpeg.js +121 -0
- package/apps/runtime/dist/media/generate.js +120 -0
- package/apps/runtime/dist/media/marking.js +121 -0
- package/apps/runtime/dist/media/transcribe.js +30 -0
- package/apps/runtime/dist/model-errors.js +3 -0
- package/apps/runtime/dist/models.js +489 -0
- package/apps/runtime/dist/plugin.js +61 -0
- package/apps/runtime/dist/render/chromium.js +85 -0
- package/apps/runtime/dist/render/convert.js +161 -0
- package/apps/runtime/dist/render/downloads.js +277 -0
- package/apps/runtime/dist/render/guard.js +25 -0
- package/apps/runtime/dist/routes/api.js +74 -0
- package/apps/runtime/dist/routes/delivery.js +52 -0
- package/apps/runtime/dist/routes/internal.js +49 -0
- package/apps/runtime/dist/routes/marketplace.js +36 -0
- package/apps/runtime/dist/routes/runs.js +805 -0
- package/apps/runtime/dist/routes/studio.js +702 -0
- package/apps/runtime/dist/routes/wizard-stream.js +30 -0
- package/apps/runtime/dist/running.js +39 -0
- package/apps/runtime/dist/sandbox/agentos.js +191 -0
- package/apps/runtime/dist/sandbox/docker.js +77 -0
- package/apps/runtime/dist/sandbox/index.js +30 -0
- package/apps/runtime/dist/sandbox/types.js +1 -0
- package/apps/runtime/dist/secrets/crypto.js +28 -0
- package/apps/runtime/dist/secrets/signing.js +34 -0
- package/apps/runtime/dist/secrets/vault.js +59 -0
- package/apps/runtime/dist/services/architect.js +92 -0
- package/apps/runtime/dist/services/brand.js +20 -0
- package/apps/runtime/dist/services/cloud.js +52 -0
- package/apps/runtime/dist/services/draft-events.js +27 -0
- package/apps/runtime/dist/services/errors.js +24 -0
- package/apps/runtime/dist/services/files.js +129 -0
- package/apps/runtime/dist/services/links.js +16 -0
- package/apps/runtime/dist/services/marketplace.js +292 -0
- package/apps/runtime/dist/services/package.js +202 -0
- package/apps/runtime/dist/services/profile.js +23 -0
- package/apps/runtime/dist/services/project-files.js +266 -0
- package/apps/runtime/dist/services/project-index.js +109 -0
- package/apps/runtime/dist/services/projects.js +181 -0
- package/apps/runtime/dist/services/runs.js +224 -0
- package/apps/runtime/dist/services/shares.js +129 -0
- package/apps/runtime/dist/services/widgets.js +34 -0
- package/apps/runtime/dist/services/wizards.js +342 -0
- package/apps/runtime/dist/settings.js +16 -0
- package/apps/runtime/dist/store/index.js +279 -0
- package/apps/runtime/dist/tenants/control.js +106 -0
- package/apps/runtime/dist/tenants/tenant.js +26 -0
- package/apps/runtime/dist/tools/browser.js +380 -0
- package/apps/runtime/dist/tools/connector.js +204 -0
- package/apps/runtime/dist/tools/index.js +234 -0
- package/apps/runtime/dist/tools/mail.js +104 -0
- package/apps/runtime/dist/tools/net-guard.js +59 -0
- package/apps/runtime/dist/tools/project.js +53 -0
- package/apps/runtime/dist/tools/shared.js +42 -0
- package/apps/runtime/dist/tools/store.js +238 -0
- package/apps/runtime/dist/update.js +118 -0
- package/apps/runtime/dist/widgets/bundle.js +99 -0
- package/apps/runtime/dist/widgets/render.js +312 -0
- package/apps/runtime/dist/widgets/runtime.js +84 -0
- package/apps/runtime/dist/widgets/script.js +30 -0
- package/apps/runtime/dist/widgets/step.js +192 -0
- package/apps/web/dist/ai-labels/ai-black.svg +20 -0
- package/apps/web/dist/ai-labels/ai-generated-black.svg +31 -0
- package/apps/web/dist/ai-labels/ai-generated-white.svg +32 -0
- package/apps/web/dist/ai-labels/ai-modified-black.svg +30 -0
- package/apps/web/dist/ai-labels/ai-modified-white.svg +31 -0
- package/apps/web/dist/ai-labels/ai-white.svg +21 -0
- package/apps/web/dist/assets/ChangelogDialog-O00vEnbm.js +1 -0
- package/apps/web/dist/assets/ChatPanel-CH9ry0Hp.js +1 -0
- package/apps/web/dist/assets/CreditsDialog-DRzecwUw.js +1 -0
- package/apps/web/dist/assets/EditorPage-BRnBf02Q.js +2 -0
- package/apps/web/dist/assets/FlowPreview-DDOj8IAH.js +1 -0
- package/apps/web/dist/assets/HomePage-CAGTTG3N.js +1 -0
- package/apps/web/dist/assets/ImportWizard-0RRrAnX3.js +1 -0
- package/apps/web/dist/assets/LocalRuntime-BrP-ENHg.css +1 -0
- package/apps/web/dist/assets/LocalRuntime-C-TBYrzs.js +16 -0
- package/apps/web/dist/assets/NewWizardPage-kI90h3_9.js +2 -0
- package/apps/web/dist/assets/PublicRunner-CKRDeIVd.js +1 -0
- package/apps/web/dist/assets/RunnerView-Rc4H8wYH.js +2 -0
- package/apps/web/dist/assets/SettingsPage-CESMuZxq.js +2 -0
- package/apps/web/dist/assets/SetupPage-D1rrYU14.js +1 -0
- package/apps/web/dist/assets/SharePage-COTAmOgh.js +1 -0
- package/apps/web/dist/assets/api-BY28cjGY.js +10 -0
- package/apps/web/dist/assets/arrow-left-UpjOBfQp.js +1 -0
- package/apps/web/dist/assets/arrow-right-CucQycEy.js +1 -0
- package/apps/web/dist/assets/changelog-p3wsaDDS.js +1 -0
- package/apps/web/dist/assets/check-Bjx41xbB.js +1 -0
- package/apps/web/dist/assets/createLucideIcon-DBu2Ovxj.js +1 -0
- package/apps/web/dist/assets/definition-cy7uvY5L.js +55 -0
- package/apps/web/dist/assets/engenty-CWIFpgKq.js +1 -0
- package/apps/web/dist/assets/esm-D1OEhjTK.js +7 -0
- package/apps/web/dist/assets/external-link-CRg6DVdO.js +1 -0
- package/apps/web/dist/assets/fluffy-engenty-CJ3wCQgT.js +456 -0
- package/apps/web/dist/assets/index-CQ_q9iMO.css +1 -0
- package/apps/web/dist/assets/index-D0gxh90t.js +10 -0
- package/apps/web/dist/assets/jsx-runtime-Dk72oS4N.js +1 -0
- package/apps/web/dist/assets/meta-BbSdtTUa.js +1 -0
- package/apps/web/dist/assets/mic-CeqQ89Dj.js +1 -0
- package/apps/web/dist/assets/oss-credits-cPuZUFHD.js +1 -0
- package/apps/web/dist/assets/outputs-D6pxH6ve.js +69 -0
- package/apps/web/dist/assets/plus-HA210wXA.js +1 -0
- package/apps/web/dist/assets/preload-helper-CNEI7N0N.js +1 -0
- package/apps/web/dist/assets/qr-scanner-worker.min-CJaXo3yu.js +98 -0
- package/apps/web/dist/assets/qr-scanner.min-C_UI0gH-.js +1 -0
- package/apps/web/dist/assets/type-BzYgVWKL.js +1 -0
- package/apps/web/dist/assets/ui-Ddwl35A6.js +1 -0
- package/apps/web/dist/assets/upload-DsgLfQ3U.js +1 -0
- package/apps/web/dist/assets/video-Dk1fuTDl.js +1 -0
- package/apps/web/dist/assets/wand-sparkles-Bf97fs8T.js +1 -0
- package/apps/web/dist/embed.js +102 -0
- package/apps/web/dist/favicon.svg +1 -0
- package/apps/web/dist/icons/apple-touch-icon.png +0 -0
- package/apps/web/dist/icons/icon-192.png +0 -0
- package/apps/web/dist/icons/icon-512.png +0 -0
- package/apps/web/dist/icons/icon-maskable-512.png +0 -0
- package/apps/web/dist/index.html +46 -0
- package/apps/web/dist/install.sh +532 -0
- package/apps/web/dist/manifest.webmanifest +20 -0
- package/apps/web/dist/sw.js +15 -0
- package/bin/wizards.mjs +13 -0
- package/node_modules/@engenty-wizards/shared/dist/connectors.d.ts +77 -0
- package/node_modules/@engenty-wizards/shared/dist/connectors.js +2 -0
- package/node_modules/@engenty-wizards/shared/dist/definition.d.ts +1415 -0
- package/node_modules/@engenty-wizards/shared/dist/definition.js +762 -0
- package/node_modules/@engenty-wizards/shared/dist/engenty/data-tables/columns-format.d.ts +5 -0
- package/node_modules/@engenty-wizards/shared/dist/engenty/data-tables/columns-format.js +132 -0
- package/node_modules/@engenty-wizards/shared/dist/engenty/data-tables/columns-value.d.ts +11 -0
- package/node_modules/@engenty-wizards/shared/dist/engenty/data-tables/columns-value.js +209 -0
- package/node_modules/@engenty-wizards/shared/dist/engenty/data-tables/columns.d.ts +532 -0
- package/node_modules/@engenty-wizards/shared/dist/engenty/data-tables/columns.js +216 -0
- package/node_modules/@engenty-wizards/shared/dist/engenty/data-tables/index.d.ts +3 -0
- package/node_modules/@engenty-wizards/shared/dist/engenty/data-tables/index.js +3 -0
- package/node_modules/@engenty-wizards/shared/dist/engenty/search-index/catalog-lexical.d.ts +14 -0
- package/node_modules/@engenty-wizards/shared/dist/engenty/search-index/catalog-lexical.js +166 -0
- package/node_modules/@engenty-wizards/shared/dist/engenty/search-index/catalog-rank.d.ts +33 -0
- package/node_modules/@engenty-wizards/shared/dist/engenty/search-index/catalog-rank.js +99 -0
- package/node_modules/@engenty-wizards/shared/dist/marketplace-entry.d.ts +35 -0
- package/node_modules/@engenty-wizards/shared/dist/marketplace-entry.js +86 -0
- package/node_modules/@engenty-wizards/shared/dist/marketplace-search.d.ts +20 -0
- package/node_modules/@engenty-wizards/shared/dist/marketplace-search.js +133 -0
- package/node_modules/@engenty-wizards/shared/dist/marketplace.d.ts +298 -0
- package/node_modules/@engenty-wizards/shared/dist/marketplace.js +211 -0
- package/node_modules/@engenty-wizards/shared/dist/merge.d.ts +7 -0
- package/node_modules/@engenty-wizards/shared/dist/merge.js +40 -0
- package/node_modules/@engenty-wizards/shared/dist/projects.d.ts +74 -0
- package/node_modules/@engenty-wizards/shared/dist/projects.js +96 -0
- package/node_modules/@engenty-wizards/shared/dist/run.d.ts +219 -0
- package/node_modules/@engenty-wizards/shared/dist/run.js +1 -0
- package/node_modules/@engenty-wizards/shared/dist/store.d.ts +100 -0
- package/node_modules/@engenty-wizards/shared/dist/store.js +44 -0
- package/node_modules/@engenty-wizards/shared/dist/workspace.d.ts +16 -0
- package/node_modules/@engenty-wizards/shared/dist/workspace.js +51 -0
- package/node_modules/@engenty-wizards/shared/package.json +10 -0
- package/package.json +62 -0
- package/plugin/.claude-plugin/plugin.json +9 -0
- package/plugin/.mcp.json +8 -0
- package/plugin/commands/wizard.md +8 -0
- package/plugin/skills/build-wizard/SKILL.md +27 -0
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { accessSync, chmodSync, constants } from "node:fs";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { nanoid } from "nanoid";
|
|
5
|
+
import { ServiceError } from "../services/errors.js";
|
|
6
|
+
const BUFFER = 200_000;
|
|
7
|
+
/** A session nobody looks at is ended after this long. */
|
|
8
|
+
const IDLE_MS = 20 * 60_000;
|
|
9
|
+
/** An ended session is kept this long for a page to read its last lines. */
|
|
10
|
+
const LINGER_MS = 5 * 60_000;
|
|
11
|
+
const sessions = new Map();
|
|
12
|
+
/** The prebuilt helper node-pty forks must be executable; an install that skips scripts leaves it without the bit. */
|
|
13
|
+
function ensureHelper() {
|
|
14
|
+
try {
|
|
15
|
+
const entry = createRequire(import.meta.url).resolve("node-pty");
|
|
16
|
+
const helper = join(dirname(entry), "..", "prebuilds", `${process.platform}-${process.arch}`, "spawn-helper");
|
|
17
|
+
try {
|
|
18
|
+
accessSync(helper, constants.X_OK);
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
chmodSync(helper, 0o755);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
// Not on this platform, or not ours to change: spawning tells.
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
async function ptyModule() {
|
|
29
|
+
try {
|
|
30
|
+
ensureHelper();
|
|
31
|
+
const mod = (await import("node-pty"));
|
|
32
|
+
const spawn = mod.spawn ?? mod.default?.spawn;
|
|
33
|
+
if (!spawn) {
|
|
34
|
+
throw new Error("node-pty has no spawn");
|
|
35
|
+
}
|
|
36
|
+
return { spawn };
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
throw new ServiceError("refused", "Das eingebaute Terminal ist auf diesem Gerät nicht verfügbar. Führe den Befehl in deinem eigenen Terminal aus.");
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function emit(session, event) {
|
|
43
|
+
for (const listener of session.listeners) {
|
|
44
|
+
listener(event);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function forget(session) {
|
|
48
|
+
if (session.poll) {
|
|
49
|
+
clearInterval(session.poll);
|
|
50
|
+
}
|
|
51
|
+
if (session.reaper) {
|
|
52
|
+
clearTimeout(session.reaper);
|
|
53
|
+
}
|
|
54
|
+
sessions.delete(session.id);
|
|
55
|
+
}
|
|
56
|
+
function rearm(session) {
|
|
57
|
+
if (session.reaper) {
|
|
58
|
+
clearTimeout(session.reaper);
|
|
59
|
+
}
|
|
60
|
+
session.reaper = setTimeout(() => {
|
|
61
|
+
if (session.listeners.size) {
|
|
62
|
+
rearm(session);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (!session.exit) {
|
|
66
|
+
session.pty.kill();
|
|
67
|
+
}
|
|
68
|
+
forget(session);
|
|
69
|
+
}, session.exit ? LINGER_MS : IDLE_MS);
|
|
70
|
+
session.reaper.unref();
|
|
71
|
+
}
|
|
72
|
+
/** Starts a command in a new terminal; the id names it to the page. */
|
|
73
|
+
export async function openTerminal(opts) {
|
|
74
|
+
const { spawn } = await ptyModule();
|
|
75
|
+
const id = nanoid(16);
|
|
76
|
+
let pty;
|
|
77
|
+
try {
|
|
78
|
+
pty = spawn(opts.bin, opts.args, {
|
|
79
|
+
name: "xterm-256color",
|
|
80
|
+
cols: opts.cols ?? 100,
|
|
81
|
+
rows: opts.rows ?? 28,
|
|
82
|
+
cwd: opts.cwd,
|
|
83
|
+
env: opts.env,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
throw new ServiceError("refused", `Der Befehl konnte nicht starten: ${err.message}`);
|
|
88
|
+
}
|
|
89
|
+
const session = {
|
|
90
|
+
id,
|
|
91
|
+
pty,
|
|
92
|
+
buffer: "",
|
|
93
|
+
listeners: new Set(),
|
|
94
|
+
exit: null,
|
|
95
|
+
done: false,
|
|
96
|
+
poll: null,
|
|
97
|
+
reaper: null,
|
|
98
|
+
};
|
|
99
|
+
sessions.set(id, session);
|
|
100
|
+
pty.onData((data) => {
|
|
101
|
+
session.buffer = (session.buffer + data).slice(-BUFFER);
|
|
102
|
+
emit(session, { type: "data", data });
|
|
103
|
+
});
|
|
104
|
+
pty.onExit(({ exitCode }) => {
|
|
105
|
+
session.exit = { code: exitCode };
|
|
106
|
+
if (session.poll) {
|
|
107
|
+
clearInterval(session.poll);
|
|
108
|
+
session.poll = null;
|
|
109
|
+
}
|
|
110
|
+
emit(session, { type: "exit", code: exitCode });
|
|
111
|
+
rearm(session);
|
|
112
|
+
});
|
|
113
|
+
if (opts.until) {
|
|
114
|
+
const until = opts.until;
|
|
115
|
+
let asking = false;
|
|
116
|
+
session.poll = setInterval(() => {
|
|
117
|
+
if (asking || session.done) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
asking = true;
|
|
121
|
+
void until()
|
|
122
|
+
.then((ok) => {
|
|
123
|
+
if (ok && !session.done) {
|
|
124
|
+
session.done = true;
|
|
125
|
+
emit(session, { type: "done" });
|
|
126
|
+
opts.onDone?.();
|
|
127
|
+
if (!session.exit) {
|
|
128
|
+
// The command did its job; an app that stays open is ended for the person.
|
|
129
|
+
setTimeout(() => {
|
|
130
|
+
if (!session.exit) {
|
|
131
|
+
session.pty.kill();
|
|
132
|
+
}
|
|
133
|
+
}, 1500).unref();
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
})
|
|
137
|
+
.catch(() => undefined)
|
|
138
|
+
.finally(() => {
|
|
139
|
+
asking = false;
|
|
140
|
+
});
|
|
141
|
+
}, 3000);
|
|
142
|
+
}
|
|
143
|
+
rearm(session);
|
|
144
|
+
return id;
|
|
145
|
+
}
|
|
146
|
+
function sessionOf(id) {
|
|
147
|
+
const session = sessions.get(id);
|
|
148
|
+
if (!session) {
|
|
149
|
+
throw new ServiceError("not_found", "Dieses Terminal gibt es nicht mehr.");
|
|
150
|
+
}
|
|
151
|
+
return session;
|
|
152
|
+
}
|
|
153
|
+
/** Follows a terminal: what it printed so far, then everything new, then its end. */
|
|
154
|
+
export function subscribeTerminal(id, listener) {
|
|
155
|
+
const session = sessionOf(id);
|
|
156
|
+
if (session.buffer) {
|
|
157
|
+
listener({ type: "data", data: session.buffer });
|
|
158
|
+
}
|
|
159
|
+
if (session.done) {
|
|
160
|
+
listener({ type: "done" });
|
|
161
|
+
}
|
|
162
|
+
if (session.exit) {
|
|
163
|
+
listener({ type: "exit", code: session.exit.code });
|
|
164
|
+
}
|
|
165
|
+
session.listeners.add(listener);
|
|
166
|
+
rearm(session);
|
|
167
|
+
return () => {
|
|
168
|
+
session.listeners.delete(listener);
|
|
169
|
+
rearm(session);
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
export function writeTerminal(id, data) {
|
|
173
|
+
const session = sessionOf(id);
|
|
174
|
+
if (!session.exit) {
|
|
175
|
+
session.pty.write(data);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
export function resizeTerminal(id, cols, rows) {
|
|
179
|
+
const session = sessionOf(id);
|
|
180
|
+
if (!session.exit && cols > 0 && rows > 0) {
|
|
181
|
+
session.pty.resize(Math.min(cols, 500), Math.min(rows, 200));
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
export function closeTerminal(id) {
|
|
185
|
+
const session = sessions.get(id);
|
|
186
|
+
if (!session) {
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
if (!session.exit) {
|
|
190
|
+
session.pty.kill();
|
|
191
|
+
}
|
|
192
|
+
forget(session);
|
|
193
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A harness is an AI client installed on this machine — Claude Code, Codex, Gemini CLI, Cursor
|
|
3
|
+
* Agent — that answers on the person's own subscription or sign-in. The runtime runs it headless
|
|
4
|
+
* as a model (./model.ts) and signs it in through the inline terminal (./terminal.ts). Nothing of
|
|
5
|
+
* this process reaches it: no key of ours, no session of a parent client.
|
|
6
|
+
*/
|
|
7
|
+
export const HARNESS_IDS = ["claude", "codex", "gemini", "cursor"];
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { serve } from "@hono/node-server";
|
|
2
|
+
import { localEntryUrl, purgeSessions } from "./auth/index.js";
|
|
3
|
+
import { adoptLegacyData } from "./db/adopt.js";
|
|
4
|
+
import { knownTenants, migrateAllTenants, migrateControlDb, openTenantDb, withTenant, } from "./db/client.js";
|
|
5
|
+
import { resumeInterruptedRuns } from "./engine/runner.js";
|
|
6
|
+
import { basePath, env } from "./env.js";
|
|
7
|
+
import { managed } from "./manage.js";
|
|
8
|
+
import { loadCatalog, loadLocalModels } from "./models.js";
|
|
9
|
+
import { closeBrowser } from "./render/chromium.js";
|
|
10
|
+
import { clearRunning, writeRunning } from "./running.js";
|
|
11
|
+
import { syncMarketplace } from "./services/marketplace.js";
|
|
12
|
+
import { purgeExpiredRuns } from "./services/shares.js";
|
|
13
|
+
import { purgeUnusedStores } from "./store/index.js";
|
|
14
|
+
import { LOCAL_TENANT } from "./tenants/tenant.js";
|
|
15
|
+
import { onRestart } from "./update.js";
|
|
16
|
+
await migrateControlDb();
|
|
17
|
+
if (managed) {
|
|
18
|
+
// Every tenant database is brought to this release's schema; a new one migrates when first opened.
|
|
19
|
+
void migrateAllTenants().then(({ migrated, failed }) => {
|
|
20
|
+
console.log(`tenant databases: ${migrated} current${failed.length ? `, failed: ${failed}` : ""}`);
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
await adoptLegacyData();
|
|
25
|
+
await openTenantDb(LOCAL_TENANT);
|
|
26
|
+
await loadLocalModels();
|
|
27
|
+
void loadCatalog();
|
|
28
|
+
}
|
|
29
|
+
// The marketplace's starters and the starred entries, kept for offline use; checked hourly.
|
|
30
|
+
void syncMarketplace();
|
|
31
|
+
setInterval(() => void syncMarketplace(), 3600_000).unref();
|
|
32
|
+
const { default: app } = await import("./app.js");
|
|
33
|
+
/** Behind a path (APP_URL ends in /w) the proxy may leave the prefix on; the routes know none. */
|
|
34
|
+
function withoutBasePath(request) {
|
|
35
|
+
const url = new URL(request.url);
|
|
36
|
+
if (basePath && (url.pathname === basePath || url.pathname.startsWith(`${basePath}/`))) {
|
|
37
|
+
url.pathname = url.pathname.slice(basePath.length) || "/";
|
|
38
|
+
return app.fetch(new Request(url, request));
|
|
39
|
+
}
|
|
40
|
+
return app.fetch(request);
|
|
41
|
+
}
|
|
42
|
+
const server = serve({ fetch: withoutBasePath, port: env.port, hostname: env.host }, (info) => {
|
|
43
|
+
console.log(`engenty wizards on :${info.port} — ${env.appUrl}`);
|
|
44
|
+
const entry = localEntryUrl();
|
|
45
|
+
if (entry && !env.local.accessKey) {
|
|
46
|
+
console.log(`Open the studio: ${entry}`);
|
|
47
|
+
}
|
|
48
|
+
if (!managed) {
|
|
49
|
+
writeRunning(env.dataDir, {
|
|
50
|
+
pid: process.pid,
|
|
51
|
+
port: info.port,
|
|
52
|
+
url: env.appUrl,
|
|
53
|
+
startedAt: new Date().toISOString(),
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
await resumeInterruptedRuns();
|
|
58
|
+
// End-user results are kept RESULT_TTL_DAYS; expired runs and their files go once an hour.
|
|
59
|
+
// What a wizard keeps for a person goes once nobody used it for STORE_TTL_DAYS.
|
|
60
|
+
async function purge() {
|
|
61
|
+
await purgeSessions();
|
|
62
|
+
for (const id of await knownTenants()) {
|
|
63
|
+
await withTenant(id, () => Promise.all([purgeExpiredRuns(), purgeUnusedStores()])).catch((err) => console.error(`[retention ${id}]`, err));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
void purge().catch((err) => console.error("[retention]", err));
|
|
67
|
+
setInterval(() => void purge().catch((err) => console.error("[retention]", err)), 3600_000).unref();
|
|
68
|
+
async function shutdown(code = 0) {
|
|
69
|
+
clearRunning(env.dataDir);
|
|
70
|
+
server.close();
|
|
71
|
+
await closeBrowser();
|
|
72
|
+
process.exit(code);
|
|
73
|
+
}
|
|
74
|
+
process.on("SIGINT", () => shutdown());
|
|
75
|
+
process.on("SIGTERM", () => shutdown());
|
|
76
|
+
onRestart((code) => void shutdown(code));
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { and, count, eq, gte } from "drizzle-orm";
|
|
3
|
+
import { canSpend } from "./credits/credits.js";
|
|
4
|
+
import { db, schema } from "./db/client.js";
|
|
5
|
+
import { env } from "./env.js";
|
|
6
|
+
export function hashIp(ip) {
|
|
7
|
+
if (!ip) {
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
return createHash("sha256").update(`${env.authSecret}:${ip}`).digest("hex").slice(0, 32);
|
|
11
|
+
}
|
|
12
|
+
function startOfDay() {
|
|
13
|
+
const d = new Date();
|
|
14
|
+
d.setUTCHours(0, 0, 0, 0);
|
|
15
|
+
return d;
|
|
16
|
+
}
|
|
17
|
+
/** Why a shared wizard cannot start a run right now, or null when it can. */
|
|
18
|
+
export async function wizardUnavailable(w) {
|
|
19
|
+
if (!w.shareEnabled || w.publishedVersion === null) {
|
|
20
|
+
return "Dieser Wizard ist gerade nicht verfügbar.";
|
|
21
|
+
}
|
|
22
|
+
const [{ n }] = await db
|
|
23
|
+
.select({ n: count() })
|
|
24
|
+
.from(schema.run)
|
|
25
|
+
.where(and(eq(schema.run.wizardId, w.id), eq(schema.run.mode, "live"), gte(schema.run.createdAt, startOfDay())));
|
|
26
|
+
if (n >= w.dailyRunLimit) {
|
|
27
|
+
return "Für heute ist das Limit dieses Wizards erreicht. Bitte morgen wieder versuchen.";
|
|
28
|
+
}
|
|
29
|
+
if (!(await canSpend())) {
|
|
30
|
+
return "Dieser Wizard ist gerade nicht verfügbar.";
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
export async function verifyTurnstile(token, ip) {
|
|
35
|
+
if (!env.turnstile.secret) {
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
if (!token) {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
const body = new URLSearchParams({ secret: env.turnstile.secret, response: token });
|
|
42
|
+
if (ip) {
|
|
43
|
+
body.set("remoteip", ip);
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
const res = await fetch("https://challenges.cloudflare.com/turnstile/v0/siteverify", {
|
|
47
|
+
method: "POST",
|
|
48
|
+
body,
|
|
49
|
+
signal: AbortSignal.timeout(8000),
|
|
50
|
+
});
|
|
51
|
+
const data = (await res.json());
|
|
52
|
+
return Boolean(data.success);
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { and, eq } from "drizzle-orm";
|
|
2
|
+
import { db, schema } from "./db/client.js";
|
|
3
|
+
import { env } from "./env.js";
|
|
4
|
+
import { escapeHtml } from "./render/convert.js";
|
|
5
|
+
import { sharedRun, shareView } from "./services/shares.js";
|
|
6
|
+
function tags(p) {
|
|
7
|
+
const t = [
|
|
8
|
+
`<meta property="og:title" content="${escapeHtml(p.title)}">`,
|
|
9
|
+
`<meta property="og:description" content="${escapeHtml(p.description)}">`,
|
|
10
|
+
`<meta property="og:url" content="${escapeHtml(p.url)}">`,
|
|
11
|
+
`<meta property="og:type" content="website">`,
|
|
12
|
+
`<meta name="twitter:card" content="${p.image ? "summary_large_image" : "summary"}">`,
|
|
13
|
+
`<meta name="twitter:title" content="${escapeHtml(p.title)}">`,
|
|
14
|
+
`<meta name="twitter:description" content="${escapeHtml(p.description)}">`,
|
|
15
|
+
];
|
|
16
|
+
if (p.image) {
|
|
17
|
+
t.push(`<meta property="og:image" content="${escapeHtml(p.image)}">`);
|
|
18
|
+
t.push(`<meta name="twitter:image" content="${escapeHtml(p.image)}">`);
|
|
19
|
+
}
|
|
20
|
+
return `<title>${escapeHtml(p.title)}</title>\n${t.join("\n")}`;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Open Graph tags for shared links, so X, LinkedIn, WhatsApp and Slack show a card: a shared
|
|
24
|
+
* result (`/s/<token>`) with its picture, a public wizard (`/w/<token>`) with its title.
|
|
25
|
+
*/
|
|
26
|
+
export async function linkPreview(path) {
|
|
27
|
+
const share = path.match(/^\/s\/([A-Za-z0-9_-]+)\/?$/);
|
|
28
|
+
if (share) {
|
|
29
|
+
const run = await sharedRun(share[1]);
|
|
30
|
+
if (!run) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
const view = await shareView(run);
|
|
34
|
+
return tags({
|
|
35
|
+
title: view.title === view.wizard.title ? view.title : `${view.title} · ${view.wizard.title}`,
|
|
36
|
+
description: view.message ?? view.wizard.description,
|
|
37
|
+
url: `${env.appUrl}/s/${share[1]}`,
|
|
38
|
+
image: `${env.appUrl}/api/shares/${share[1]}/image`,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
const wizard = path.match(/^\/w\/([A-Za-z0-9_-]+)\/?$/);
|
|
42
|
+
if (wizard) {
|
|
43
|
+
const w = await db.query.wizard.findFirst({ where: eq(schema.wizard.shareToken, wizard[1]) });
|
|
44
|
+
if (!w || w.publishedVersion === null || !w.shareEnabled) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
const v = await db.query.wizardVersion.findFirst({
|
|
48
|
+
where: and(eq(schema.wizardVersion.wizardId, w.id), eq(schema.wizardVersion.version, w.publishedVersion)),
|
|
49
|
+
});
|
|
50
|
+
return v
|
|
51
|
+
? tags({
|
|
52
|
+
title: v.definition.title,
|
|
53
|
+
description: v.definition.description,
|
|
54
|
+
url: `${env.appUrl}/w/${wizard[1]}`,
|
|
55
|
+
})
|
|
56
|
+
: null;
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { createRemoteJWKSet, jwtVerify } from "jose";
|
|
2
|
+
import { env } from "./env.js";
|
|
3
|
+
/**
|
|
4
|
+
* The Manage-App as this runtime sees it (docs/manage-contract.md): it issues the tokens,
|
|
5
|
+
* knows the tenants and their credits. Nothing here runs when `MANAGE_URL` is empty.
|
|
6
|
+
*/
|
|
7
|
+
export const managed = Boolean(env.manage.url);
|
|
8
|
+
const discoveries = new Map();
|
|
9
|
+
/** OIDC discovery of a Manage-App, cached for an hour. */
|
|
10
|
+
export async function discovery(base = env.manage.url) {
|
|
11
|
+
const hit = discoveries.get(base);
|
|
12
|
+
if (hit && Date.now() - hit.at < 3600_000) {
|
|
13
|
+
return hit.value;
|
|
14
|
+
}
|
|
15
|
+
const res = await fetch(`${base}/.well-known/openid-configuration`, {
|
|
16
|
+
signal: AbortSignal.timeout(10_000),
|
|
17
|
+
});
|
|
18
|
+
if (!res.ok) {
|
|
19
|
+
throw new Error(`Manage-App discovery failed: ${res.status}`);
|
|
20
|
+
}
|
|
21
|
+
const value = (await res.json());
|
|
22
|
+
discoveries.set(base, { at: Date.now(), value });
|
|
23
|
+
return value;
|
|
24
|
+
}
|
|
25
|
+
const keySets = new Map();
|
|
26
|
+
function toClaims(payload) {
|
|
27
|
+
const p = payload;
|
|
28
|
+
if (typeof p.sub !== "string" || typeof p.tenant !== "string") {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
const role = p.role === "owner" || p.role === "admin" ? p.role : "member";
|
|
32
|
+
return {
|
|
33
|
+
userId: p.sub,
|
|
34
|
+
tenantId: p.tenant,
|
|
35
|
+
role,
|
|
36
|
+
name: typeof p.name === "string" ? p.name : "",
|
|
37
|
+
email: typeof p.email === "string" ? p.email : "",
|
|
38
|
+
clientId: typeof p.azp === "string" ? p.azp : String(p.client_id ?? ""),
|
|
39
|
+
scopes: String(p.scope ?? "")
|
|
40
|
+
.split(" ")
|
|
41
|
+
.filter(Boolean),
|
|
42
|
+
expiresAt: (p.exp ?? 0) * 1000,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Verifies an access token of the Manage-App against its JWKS — no round trip per request.
|
|
47
|
+
* `audiences` are the resources this caller accepts; null skips the audience check.
|
|
48
|
+
*/
|
|
49
|
+
export async function verifyToken(token, audiences, base = env.manage.url) {
|
|
50
|
+
try {
|
|
51
|
+
const d = await discovery(base);
|
|
52
|
+
let keys = keySets.get(d.jwks_uri);
|
|
53
|
+
if (!keys) {
|
|
54
|
+
keys = createRemoteJWKSet(new URL(d.jwks_uri));
|
|
55
|
+
keySets.set(d.jwks_uri, keys);
|
|
56
|
+
}
|
|
57
|
+
const { payload } = await jwtVerify(token, keys, {
|
|
58
|
+
issuer: d.issuer,
|
|
59
|
+
...(audiences ? { audience: audiences } : {}),
|
|
60
|
+
});
|
|
61
|
+
return toClaims(payload);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/** Reads a token's claims without verifying it — only for tokens the token endpoint just handed us. */
|
|
68
|
+
export function unverifiedClaims(token) {
|
|
69
|
+
try {
|
|
70
|
+
const payload = JSON.parse(Buffer.from(token.split(".")[1], "base64url").toString("utf8"));
|
|
71
|
+
return toClaims(payload);
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
// --- service calls -----------------------------------------------------------
|
|
78
|
+
export class ManageError extends Error {
|
|
79
|
+
status;
|
|
80
|
+
code;
|
|
81
|
+
constructor(status, code, message) {
|
|
82
|
+
super(message);
|
|
83
|
+
this.status = status;
|
|
84
|
+
this.code = code;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
async function call(method, path, body) {
|
|
88
|
+
const res = await fetch(`${env.manage.url}${path}`, {
|
|
89
|
+
method,
|
|
90
|
+
headers: {
|
|
91
|
+
authorization: `Bearer ${env.manage.serviceKey}`,
|
|
92
|
+
...(body === undefined ? {} : { "content-type": "application/json" }),
|
|
93
|
+
},
|
|
94
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
95
|
+
signal: AbortSignal.timeout(15_000),
|
|
96
|
+
});
|
|
97
|
+
const data = (await res.json().catch(() => ({})));
|
|
98
|
+
if (!res.ok) {
|
|
99
|
+
throw new ManageError(res.status, data.code ?? "error", data.error ?? `Manage-App ${res.status}`);
|
|
100
|
+
}
|
|
101
|
+
return data;
|
|
102
|
+
}
|
|
103
|
+
const tenants = new Map();
|
|
104
|
+
/** Plan, limits and balance of a tenant, cached for a minute. */
|
|
105
|
+
export async function tenantInfo(id, fresh = false) {
|
|
106
|
+
const hit = tenants.get(id);
|
|
107
|
+
if (!fresh && hit && Date.now() - hit.at < 60_000) {
|
|
108
|
+
return hit.value;
|
|
109
|
+
}
|
|
110
|
+
const value = await call("GET", `/v1/tenants/${encodeURIComponent(id)}`);
|
|
111
|
+
tenants.set(id, { at: Date.now(), value });
|
|
112
|
+
return value;
|
|
113
|
+
}
|
|
114
|
+
export function forgetTenant(id) {
|
|
115
|
+
tenants.delete(id);
|
|
116
|
+
}
|
|
117
|
+
export function verifyKey(key) {
|
|
118
|
+
return call("POST", "/v1/keys/verify", { key });
|
|
119
|
+
}
|
|
120
|
+
export function reserve(tenantId, runId, credits) {
|
|
121
|
+
return call("POST", "/v1/reservations", { tenantId, runId, credits });
|
|
122
|
+
}
|
|
123
|
+
export function release(runId) {
|
|
124
|
+
return call("POST", "/v1/reservations/release", { runId });
|
|
125
|
+
}
|
|
126
|
+
export function runUsage(tenantId, runId) {
|
|
127
|
+
return call("GET", `/v1/runs/${encodeURIComponent(runId)}/usage?tenantId=${encodeURIComponent(tenantId)}`);
|
|
128
|
+
}
|
|
129
|
+
export function bookUsage(input) {
|
|
130
|
+
return call("POST", "/v1/usage", input);
|
|
131
|
+
}
|
|
132
|
+
/** Code or refresh token → tokens. A confidential client sends its secret, a public one only its id. */
|
|
133
|
+
export async function tokenRequest(params, client, base = env.manage.url) {
|
|
134
|
+
const d = await discovery(base);
|
|
135
|
+
const body = new URLSearchParams({ ...params, client_id: client.id });
|
|
136
|
+
const headers = { "content-type": "application/x-www-form-urlencoded" };
|
|
137
|
+
if (client.secret) {
|
|
138
|
+
headers.authorization = `Basic ${Buffer.from(`${encodeURIComponent(client.id)}:${encodeURIComponent(client.secret)}`).toString("base64")}`;
|
|
139
|
+
}
|
|
140
|
+
const res = await fetch(d.token_endpoint, {
|
|
141
|
+
method: "POST",
|
|
142
|
+
headers,
|
|
143
|
+
body,
|
|
144
|
+
signal: AbortSignal.timeout(15_000),
|
|
145
|
+
});
|
|
146
|
+
const data = (await res.json().catch(() => ({})));
|
|
147
|
+
if (!res.ok || !data.access_token) {
|
|
148
|
+
throw new ManageError(res.status, data.error ?? "token_error", "Token request failed");
|
|
149
|
+
}
|
|
150
|
+
return data;
|
|
151
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { API_KEY_PREFIX, verifyLocalKey } from "../auth/keys.js";
|
|
2
|
+
import { env } from "../env.js";
|
|
3
|
+
import { managed, verifyKey, verifyToken } from "../manage.js";
|
|
4
|
+
import { LOCAL_TENANT } from "../tenants/tenant.js";
|
|
5
|
+
import { MCP_RESOURCE, SCOPES } from "./scopes.js";
|
|
6
|
+
export function principalOf(info) {
|
|
7
|
+
return info.extra;
|
|
8
|
+
}
|
|
9
|
+
function authInfoFor(who, token, clientId) {
|
|
10
|
+
return { token, clientId, scopes: who.scopes, extra: { ...who } };
|
|
11
|
+
}
|
|
12
|
+
function bearer(request) {
|
|
13
|
+
return (request.headers
|
|
14
|
+
.get("authorization")
|
|
15
|
+
?.match(/^Bearer\s+(.+)$/i)?.[1]
|
|
16
|
+
?.trim() || null);
|
|
17
|
+
}
|
|
18
|
+
const resourceMetadataUrl = (() => {
|
|
19
|
+
const url = new URL(MCP_RESOURCE);
|
|
20
|
+
return `${url.origin}/.well-known/oauth-protected-resource${url.pathname}`;
|
|
21
|
+
})();
|
|
22
|
+
/** RFC 6750 / RFC 9728 challenge: tells an MCP client where to start OAuth. */
|
|
23
|
+
function challenge(status, error, description) {
|
|
24
|
+
return new Response(JSON.stringify({ error, error_description: description }), {
|
|
25
|
+
status,
|
|
26
|
+
headers: {
|
|
27
|
+
"content-type": "application/json",
|
|
28
|
+
...(status === 401
|
|
29
|
+
? {
|
|
30
|
+
"www-authenticate": managed
|
|
31
|
+
? `Bearer error="${error}", error_description="${description}", resource_metadata="${resourceMetadataUrl}", scope="${SCOPES.join(" ")}"`
|
|
32
|
+
: `Bearer error="${error}", error_description="${description}"`,
|
|
33
|
+
}
|
|
34
|
+
: {}),
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
const keyCache = new Map();
|
|
39
|
+
/** An API key acts as its owner, in the key's tenant, with every scope. */
|
|
40
|
+
async function apiKeyAuth(key) {
|
|
41
|
+
if (!managed) {
|
|
42
|
+
const found = await verifyLocalKey(key);
|
|
43
|
+
if (!found) {
|
|
44
|
+
return challenge(401, "invalid_token", "This API key is unknown or revoked.");
|
|
45
|
+
}
|
|
46
|
+
const who = {
|
|
47
|
+
userId: "local",
|
|
48
|
+
tenantId: LOCAL_TENANT,
|
|
49
|
+
client: found.name || "MCP",
|
|
50
|
+
scopes: [...SCOPES],
|
|
51
|
+
};
|
|
52
|
+
return authInfoFor(who, key, found.id);
|
|
53
|
+
}
|
|
54
|
+
const hit = keyCache.get(key);
|
|
55
|
+
if (hit && Date.now() - hit.at < 60_000) {
|
|
56
|
+
return authInfoFor(hit.who, key, hit.keyId);
|
|
57
|
+
}
|
|
58
|
+
const result = await verifyKey(key).catch(() => null);
|
|
59
|
+
if (!result?.valid || !result.userId || !result.tenantId) {
|
|
60
|
+
keyCache.delete(key);
|
|
61
|
+
return challenge(401, "invalid_token", "This API key is unknown or revoked.");
|
|
62
|
+
}
|
|
63
|
+
const who = {
|
|
64
|
+
userId: result.userId,
|
|
65
|
+
tenantId: result.tenantId,
|
|
66
|
+
client: result.name || "MCP",
|
|
67
|
+
scopes: [...SCOPES],
|
|
68
|
+
};
|
|
69
|
+
keyCache.set(key, { at: Date.now(), who, keyId: result.keyId ?? "" });
|
|
70
|
+
return authInfoFor(who, key, result.keyId ?? "");
|
|
71
|
+
}
|
|
72
|
+
/** An access token of the Manage-App, issued for this MCP endpoint, acts within its scopes. */
|
|
73
|
+
async function tokenAuth(token) {
|
|
74
|
+
if (!managed) {
|
|
75
|
+
return challenge(401, "invalid_token", "Sign in with an API key from the settings.");
|
|
76
|
+
}
|
|
77
|
+
const claims = await verifyToken(token, [MCP_RESOURCE, env.appUrl]);
|
|
78
|
+
if (!claims) {
|
|
79
|
+
return challenge(401, "invalid_token", "The access token is invalid or expired.");
|
|
80
|
+
}
|
|
81
|
+
const granted = new Set(claims.scopes);
|
|
82
|
+
const who = {
|
|
83
|
+
userId: claims.userId,
|
|
84
|
+
tenantId: claims.tenantId,
|
|
85
|
+
client: claims.clientId || "MCP",
|
|
86
|
+
scopes: SCOPES.filter((s) => granted.has(s)),
|
|
87
|
+
};
|
|
88
|
+
return authInfoFor(who, token, claims.clientId);
|
|
89
|
+
}
|
|
90
|
+
/** Who an MCP or API request acts for: an API key (`x-api-key` or a `wz_…` bearer) or an OAuth access token. */
|
|
91
|
+
export async function authenticate(request) {
|
|
92
|
+
const header = request.headers.get("x-api-key")?.trim();
|
|
93
|
+
const token = bearer(request);
|
|
94
|
+
const key = header || (token?.startsWith(API_KEY_PREFIX) ? token : null);
|
|
95
|
+
if (key) {
|
|
96
|
+
return apiKeyAuth(key);
|
|
97
|
+
}
|
|
98
|
+
if (!token) {
|
|
99
|
+
return challenge(401, "invalid_token", "Sign in to use this endpoint.");
|
|
100
|
+
}
|
|
101
|
+
return tokenAuth(token);
|
|
102
|
+
}
|