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,103 @@
|
|
|
1
|
+
import { sql } from "drizzle-orm";
|
|
2
|
+
import { db, schema } from "../db/client.js";
|
|
3
|
+
import { ManageError, managed, release, reserve, runUsage, tenantInfo } from "../manage.js";
|
|
4
|
+
import { currentTenant } from "../tenants/tenant.js";
|
|
5
|
+
/**
|
|
6
|
+
* Credits live in the Manage-App: its gateway books every model call. The runtime only asks
|
|
7
|
+
* whether a tenant may start, reserves before a run and reads what a run cost. A runtime that
|
|
8
|
+
* runs alone has no credits; it keeps the provider cost of a run for display (1 credit = 1 cent).
|
|
9
|
+
*/
|
|
10
|
+
export const MICROS_PER_CREDIT = 1_000_000;
|
|
11
|
+
export function usdToMicros(usd) {
|
|
12
|
+
return Math.ceil(usd * 100 * MICROS_PER_CREDIT);
|
|
13
|
+
}
|
|
14
|
+
export function microsToCredits(micros) {
|
|
15
|
+
return Math.round(micros / MICROS_PER_CREDIT);
|
|
16
|
+
}
|
|
17
|
+
/** At least one credit left — enough to start something. */
|
|
18
|
+
export async function canSpend() {
|
|
19
|
+
if (!managed) {
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
const t = await tenantInfo(currentTenant());
|
|
24
|
+
return t.status === "active" && t.balanceCredits >= 1;
|
|
25
|
+
}
|
|
26
|
+
catch (err) {
|
|
27
|
+
console.error("[credits]", err);
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export async function balanceCredits() {
|
|
32
|
+
if (!managed) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
return (await tenantInfo(currentTenant(), true).catch(() => null))?.balanceCredits ?? null;
|
|
36
|
+
}
|
|
37
|
+
/** Holds credits for a run before it starts. False = the free balance does not cover it. */
|
|
38
|
+
export async function reserveForRun(runId, credits) {
|
|
39
|
+
if (!managed || credits <= 0) {
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
await reserve(currentTenant(), runId, Math.ceil(credits));
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
catch (err) {
|
|
47
|
+
if (err instanceof ManageError && err.status === 402) {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
throw err;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
export async function releaseRun(runId) {
|
|
54
|
+
if (managed) {
|
|
55
|
+
await release(runId).catch((err) => console.error("[credits] release", err));
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
async function setStepCost(runId, stepId, micros) {
|
|
59
|
+
await db
|
|
60
|
+
.insert(schema.runCost)
|
|
61
|
+
.values({ runId, stepId, micros })
|
|
62
|
+
.onConflictDoUpdate({
|
|
63
|
+
target: [schema.runCost.runId, schema.runCost.stepId],
|
|
64
|
+
set: { micros },
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
/** What the ledger booked for the run so far, written to the run and its steps. */
|
|
68
|
+
export async function syncRunCost(runId) {
|
|
69
|
+
if (!managed) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
try {
|
|
73
|
+
const usage = await runUsage(currentTenant(), runId);
|
|
74
|
+
await db
|
|
75
|
+
.update(schema.run)
|
|
76
|
+
.set({ costMicros: Math.round(usage.credits * MICROS_PER_CREDIT) })
|
|
77
|
+
.where(sql `${schema.run.id} = ${runId}`);
|
|
78
|
+
for (const [stepId, credits] of Object.entries(usage.steps)) {
|
|
79
|
+
await setStepCost(runId, stepId, Math.round(credits * MICROS_PER_CREDIT));
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
catch (err) {
|
|
83
|
+
console.error("[credits] usage", err);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/** A runtime that runs alone adds up the provider cost itself. */
|
|
87
|
+
export async function addLocalCost(runId, stepId, usd) {
|
|
88
|
+
const micros = usdToMicros(usd);
|
|
89
|
+
if (managed || micros <= 0) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
await db
|
|
93
|
+
.update(schema.run)
|
|
94
|
+
.set({ costMicros: sql `${schema.run.costMicros} + ${micros}` })
|
|
95
|
+
.where(sql `${schema.run.id} = ${runId}`);
|
|
96
|
+
await db
|
|
97
|
+
.insert(schema.runCost)
|
|
98
|
+
.values({ runId, stepId, micros })
|
|
99
|
+
.onConflictDoUpdate({
|
|
100
|
+
target: [schema.runCost.runId, schema.runCost.stepId],
|
|
101
|
+
set: { micros: sql `${schema.runCost.micros} + ${micros}` },
|
|
102
|
+
});
|
|
103
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { isDecisionStep, } from "@engenty-wizards/shared/definition";
|
|
2
|
+
import { and, desc, eq, inArray, isNull } from "drizzle-orm";
|
|
3
|
+
import { db, schema } from "../db/client.js";
|
|
4
|
+
import { classCatalog } from "../models.js";
|
|
5
|
+
import { MICROS_PER_CREDIT } from "./credits.js";
|
|
6
|
+
/**
|
|
7
|
+
* What a run will cost, in credits, before it starts. A step that has run five times on this
|
|
8
|
+
* version is estimated from its measurements (median, and the 90th percentile as the amount to
|
|
9
|
+
* hold); until then a formula per step type and model class stands in.
|
|
10
|
+
*/
|
|
11
|
+
const MIN_SAMPLES = 5;
|
|
12
|
+
const SAMPLE_RUNS = 40;
|
|
13
|
+
/** Tokens a text call of each shape takes, as [input, output]. */
|
|
14
|
+
const TOKENS = {
|
|
15
|
+
plain: [8_000, 1_500],
|
|
16
|
+
tools: [60_000, 4_000],
|
|
17
|
+
browser: [250_000, 8_000],
|
|
18
|
+
document: [6_000, 8_000],
|
|
19
|
+
prompt: [2_000, 300],
|
|
20
|
+
structure: [4_000, 1_000],
|
|
21
|
+
decision: [1_500, 100],
|
|
22
|
+
};
|
|
23
|
+
function textCredits(price, [input, output]) {
|
|
24
|
+
if (!price) {
|
|
25
|
+
return 0;
|
|
26
|
+
}
|
|
27
|
+
return ((input / 1e6) * (price.inputCreditsPerMTok ?? 0) +
|
|
28
|
+
(output / 1e6) * (price.outputCreditsPerMTok ?? 0));
|
|
29
|
+
}
|
|
30
|
+
function formula(step, catalog) {
|
|
31
|
+
const cls = (c) => catalog.classes[c];
|
|
32
|
+
if (step.type === "agent") {
|
|
33
|
+
// A decision is one small call to the classifier class.
|
|
34
|
+
if (isDecisionStep(step)) {
|
|
35
|
+
return textCredits(cls("classifier"), TOKENS.decision);
|
|
36
|
+
}
|
|
37
|
+
const shape = step.tools.includes("browser")
|
|
38
|
+
? TOKENS.browser
|
|
39
|
+
: step.tools.length || step.mcp?.length || step.connections?.length
|
|
40
|
+
? TOKENS.tools
|
|
41
|
+
: TOKENS.plain;
|
|
42
|
+
let credits = textCredits(cls(step.model ?? "high"), shape);
|
|
43
|
+
if (step.tools.includes("web_search")) {
|
|
44
|
+
credits += 4 * catalog.webSearchCredits;
|
|
45
|
+
}
|
|
46
|
+
if (step.tools.includes("image")) {
|
|
47
|
+
credits += catalog.classes.image?.creditsPerImage ?? 0;
|
|
48
|
+
}
|
|
49
|
+
if (step.output.format === "json") {
|
|
50
|
+
credits += textCredits(cls("classifier"), TOKENS.structure);
|
|
51
|
+
}
|
|
52
|
+
return credits;
|
|
53
|
+
}
|
|
54
|
+
if (step.type === "generate") {
|
|
55
|
+
if (step.asset === "image") {
|
|
56
|
+
return ((catalog.classes.image?.creditsPerImage ?? 0) + textCredits(cls("standard"), TOKENS.prompt));
|
|
57
|
+
}
|
|
58
|
+
if (step.asset === "video") {
|
|
59
|
+
const seconds = Math.min(Math.max(Math.round(step.options?.duration ?? 8), 4), 10);
|
|
60
|
+
return (seconds * (catalog.classes.video?.creditsPerSecond ?? 0) +
|
|
61
|
+
textCredits(cls("standard"), TOKENS.prompt));
|
|
62
|
+
}
|
|
63
|
+
return textCredits(cls(step.model ?? "high"), TOKENS.document);
|
|
64
|
+
}
|
|
65
|
+
return 0;
|
|
66
|
+
}
|
|
67
|
+
/** What a run of a definition costs by the formula alone — before anyone has run it. */
|
|
68
|
+
export function formulaEstimate(definition, catalog) {
|
|
69
|
+
const credits = definition.steps.reduce((sum, step) => sum + formula(step, catalog), 0);
|
|
70
|
+
return { credits: round(credits), high: Math.ceil(credits * 2) };
|
|
71
|
+
}
|
|
72
|
+
function percentile(sorted, p) {
|
|
73
|
+
return sorted[Math.min(sorted.length - 1, Math.floor(p * sorted.length))];
|
|
74
|
+
}
|
|
75
|
+
/** Credits each step cost in the latest finished runs of this wizard version. */
|
|
76
|
+
async function measurements(wizardId, version) {
|
|
77
|
+
const runs = await db
|
|
78
|
+
.select({ id: schema.run.id })
|
|
79
|
+
.from(schema.run)
|
|
80
|
+
.where(and(eq(schema.run.wizardId, wizardId), eq(schema.run.status, "done"), version === null ? isNull(schema.run.version) : eq(schema.run.version, version)))
|
|
81
|
+
.orderBy(desc(schema.run.createdAt))
|
|
82
|
+
.limit(SAMPLE_RUNS);
|
|
83
|
+
const out = new Map();
|
|
84
|
+
if (!runs.length) {
|
|
85
|
+
return out;
|
|
86
|
+
}
|
|
87
|
+
const costs = await db
|
|
88
|
+
.select()
|
|
89
|
+
.from(schema.runCost)
|
|
90
|
+
.where(inArray(schema.runCost.runId, runs.map((r) => r.id)));
|
|
91
|
+
for (const c of costs) {
|
|
92
|
+
const list = out.get(c.stepId) ?? [];
|
|
93
|
+
list.push(c.micros / MICROS_PER_CREDIT);
|
|
94
|
+
out.set(c.stepId, list);
|
|
95
|
+
}
|
|
96
|
+
return out;
|
|
97
|
+
}
|
|
98
|
+
const round = (credits) => Math.round(credits * 10) / 10;
|
|
99
|
+
export async function estimateRun(wizardId, version, definition) {
|
|
100
|
+
const catalog = await classCatalog();
|
|
101
|
+
if (!catalog) {
|
|
102
|
+
// A runtime that resolves models itself has no credits to estimate in.
|
|
103
|
+
return { available: false, credits: 0, reserve: 0, steps: {} };
|
|
104
|
+
}
|
|
105
|
+
const measured = await measurements(wizardId, version);
|
|
106
|
+
const steps = {};
|
|
107
|
+
let credits = 0;
|
|
108
|
+
let reserve = 0;
|
|
109
|
+
for (const step of definition.steps) {
|
|
110
|
+
const samples = (measured.get(step.id) ?? []).sort((a, b) => a - b);
|
|
111
|
+
let estimate;
|
|
112
|
+
if (samples.length >= MIN_SAMPLES) {
|
|
113
|
+
estimate = {
|
|
114
|
+
credits: round(percentile(samples, 0.5)),
|
|
115
|
+
high: round(percentile(samples, 0.9)),
|
|
116
|
+
measured: true,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
const value = formula(step, catalog);
|
|
121
|
+
if (value <= 0) {
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
// A formula knows little: hold twice as much as it says.
|
|
125
|
+
estimate = { credits: round(value), high: round(value * 2), measured: false };
|
|
126
|
+
}
|
|
127
|
+
steps[step.id] = estimate;
|
|
128
|
+
credits += estimate.credits;
|
|
129
|
+
reserve += estimate.high;
|
|
130
|
+
}
|
|
131
|
+
return { available: true, credits: round(credits), reserve: Math.ceil(reserve), steps };
|
|
132
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { cp } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { createClient } from "@libsql/client";
|
|
5
|
+
import { env } from "../env.js";
|
|
6
|
+
import { readSetting, writeSetting } from "../settings.js";
|
|
7
|
+
import { LOCAL_TENANT } from "../tenants/tenant.js";
|
|
8
|
+
import { control, controlDb, openTenantDb } from "./client.js";
|
|
9
|
+
/**
|
|
10
|
+
* A data folder from before tenants had their own database holds everything in `wizards.db`,
|
|
11
|
+
* owned by a user. Its wizards, runs and files become the local tenant's, once. The old file
|
|
12
|
+
* and folders stay where they are.
|
|
13
|
+
*/
|
|
14
|
+
const TABLES = [
|
|
15
|
+
"project",
|
|
16
|
+
"wizard",
|
|
17
|
+
"wizard_version",
|
|
18
|
+
"wizard_file",
|
|
19
|
+
"wizard_message",
|
|
20
|
+
"run",
|
|
21
|
+
"run_event",
|
|
22
|
+
"asset",
|
|
23
|
+
"project_connector",
|
|
24
|
+
"store_holder",
|
|
25
|
+
"store_row",
|
|
26
|
+
"store_file",
|
|
27
|
+
"store_secret",
|
|
28
|
+
];
|
|
29
|
+
const CLASS_OF = { fast: "standard", smart: "high" };
|
|
30
|
+
/** Steps named a model speed before they named a model class. */
|
|
31
|
+
function withClasses(json) {
|
|
32
|
+
if (typeof json !== "string") {
|
|
33
|
+
return json;
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
const def = JSON.parse(json);
|
|
37
|
+
for (const step of def.steps ?? []) {
|
|
38
|
+
if (step.model && CLASS_OF[step.model]) {
|
|
39
|
+
step.model = CLASS_OF[step.model];
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return JSON.stringify(def);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return json;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const personOf = (holder) => typeof holder === "string" && holder.startsWith("u:") ? "u:local" : holder;
|
|
49
|
+
export async function adoptLegacyData() {
|
|
50
|
+
const legacyFile = join(env.dataDir, "wizards.db");
|
|
51
|
+
const targetFile = join(env.dataDir, "tenants", `${LOCAL_TENANT}.db`);
|
|
52
|
+
if (!existsSync(legacyFile) || existsSync(targetFile) || (await readSetting("adopted"))) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const legacy = createClient({ url: `file:${legacyFile}` });
|
|
56
|
+
const hasOwner = (await legacy.execute("PRAGMA table_info(project)")).rows.some((r) => r.name === "owner_id");
|
|
57
|
+
if (!hasOwner) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
await openTenantDb(LOCAL_TENANT);
|
|
61
|
+
const target = createClient({ url: `file:${targetFile}` });
|
|
62
|
+
const statements = [];
|
|
63
|
+
const links = [];
|
|
64
|
+
const runs = [];
|
|
65
|
+
for (const table of TABLES) {
|
|
66
|
+
const targetColumns = new Set((await target.execute(`PRAGMA table_info(${table})`)).rows.map((r) => String(r.name)));
|
|
67
|
+
const rows = (await legacy.execute(`SELECT * FROM ${table}`).catch(() => ({ rows: [] }))).rows;
|
|
68
|
+
for (const row of rows) {
|
|
69
|
+
const values = {};
|
|
70
|
+
for (const [column, value] of Object.entries(row)) {
|
|
71
|
+
const name = column === "owner_id" ? "tenant_id" : column;
|
|
72
|
+
if (targetColumns.has(name)) {
|
|
73
|
+
values[name] = value;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if ("tenant_id" in values) {
|
|
77
|
+
values.tenant_id = LOCAL_TENANT;
|
|
78
|
+
}
|
|
79
|
+
if ("holder" in values) {
|
|
80
|
+
values.holder = personOf(values.holder);
|
|
81
|
+
}
|
|
82
|
+
if (table === "wizard") {
|
|
83
|
+
values.draft = withClasses(values.draft);
|
|
84
|
+
links.push({
|
|
85
|
+
token: String(values.share_token),
|
|
86
|
+
tenantId: LOCAL_TENANT,
|
|
87
|
+
kind: "wizard",
|
|
88
|
+
ref: String(values.id),
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
if (table === "wizard_version" || table === "run") {
|
|
92
|
+
values.definition = withClasses(values.definition);
|
|
93
|
+
}
|
|
94
|
+
if (table === "run") {
|
|
95
|
+
values.user_id = values.user_id ? "local" : null;
|
|
96
|
+
runs.push({
|
|
97
|
+
runId: String(values.id),
|
|
98
|
+
tenantId: LOCAL_TENANT,
|
|
99
|
+
visitorId: values.visitor_id ?? null,
|
|
100
|
+
ipHash: values.ip_hash ?? null,
|
|
101
|
+
active: values.status === "running",
|
|
102
|
+
createdAt: new Date(Number(values.created_at)),
|
|
103
|
+
});
|
|
104
|
+
if (values.share_token) {
|
|
105
|
+
links.push({
|
|
106
|
+
token: String(values.share_token),
|
|
107
|
+
tenantId: LOCAL_TENANT,
|
|
108
|
+
kind: "result",
|
|
109
|
+
ref: String(values.id),
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (table === "asset" && values.kind === "logo") {
|
|
114
|
+
links.push({
|
|
115
|
+
token: String(values.id),
|
|
116
|
+
tenantId: LOCAL_TENANT,
|
|
117
|
+
kind: "logo",
|
|
118
|
+
ref: String(values.id),
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
const columns = Object.keys(values);
|
|
122
|
+
statements.push({
|
|
123
|
+
sql: `INSERT INTO ${table} (${columns.join(", ")}) VALUES (${columns.map(() => "?").join(", ")})`,
|
|
124
|
+
args: columns.map((c) => values[c]),
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
await target.batch(statements, "write");
|
|
129
|
+
for (let i = 0; i < links.length; i += 100) {
|
|
130
|
+
await controlDb
|
|
131
|
+
.insert(control.link)
|
|
132
|
+
.values(links.slice(i, i + 100))
|
|
133
|
+
.onConflictDoNothing();
|
|
134
|
+
}
|
|
135
|
+
for (let i = 0; i < runs.length; i += 100) {
|
|
136
|
+
await controlDb
|
|
137
|
+
.insert(control.runIndex)
|
|
138
|
+
.values(runs.slice(i, i + 100))
|
|
139
|
+
.onConflictDoNothing();
|
|
140
|
+
}
|
|
141
|
+
for (const folder of ["assets", "blobs"]) {
|
|
142
|
+
const from = join(env.dataDir, folder);
|
|
143
|
+
if (existsSync(from)) {
|
|
144
|
+
await cp(from, join(env.dataDir, "objects", "t", LOCAL_TENANT, folder), { recursive: true });
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
await writeSetting("adopted", new Date().toISOString());
|
|
148
|
+
console.log(`Adopted ${statements.length} rows from ${legacyFile} into the local tenant.`);
|
|
149
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { mkdirSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { createClient } from "@libsql/client";
|
|
5
|
+
import { eq } from "drizzle-orm";
|
|
6
|
+
import { drizzle } from "drizzle-orm/libsql";
|
|
7
|
+
import { migrate } from "drizzle-orm/libsql/migrator";
|
|
8
|
+
import { env } from "../env.js";
|
|
9
|
+
import { currentTenant, inTenant } from "../tenants/tenant.js";
|
|
10
|
+
import * as control from "./control-schema.js";
|
|
11
|
+
import * as schema from "./schema.js";
|
|
12
|
+
mkdirSync(join(env.dataDir, "tenants"), { recursive: true });
|
|
13
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
14
|
+
/** Migrations are SQL files next to this module; the build copies them next to the built one. */
|
|
15
|
+
function migrationsFolder(name) {
|
|
16
|
+
return join(here, "migrations", name);
|
|
17
|
+
}
|
|
18
|
+
function open(url, authToken) {
|
|
19
|
+
return createClient({ url, ...(authToken ? { authToken } : {}) });
|
|
20
|
+
}
|
|
21
|
+
async function prepare(client, url) {
|
|
22
|
+
if (url.startsWith("file:")) {
|
|
23
|
+
await client.execute("PRAGMA journal_mode = WAL");
|
|
24
|
+
await client.execute("PRAGMA busy_timeout = 5000");
|
|
25
|
+
}
|
|
26
|
+
await client.execute("PRAGMA foreign_keys = ON");
|
|
27
|
+
}
|
|
28
|
+
// --- control database --------------------------------------------------------
|
|
29
|
+
const controlUrl = env.databaseUrl || `file:${join(env.dataDir, "control.db")}`;
|
|
30
|
+
const controlClient = open(controlUrl, env.databaseAuthToken);
|
|
31
|
+
/** Links, the run index, sessions and settings: what is known before a tenant's database opens. */
|
|
32
|
+
export const controlDb = drizzle(controlClient, { schema: control });
|
|
33
|
+
export { control };
|
|
34
|
+
export async function migrateControlDb() {
|
|
35
|
+
await prepare(controlClient, controlUrl);
|
|
36
|
+
await migrate(controlDb, { migrationsFolder: migrationsFolder("control") });
|
|
37
|
+
}
|
|
38
|
+
// --- tenant databases --------------------------------------------------------
|
|
39
|
+
function makeTenantDb(client) {
|
|
40
|
+
return drizzle(client, { schema });
|
|
41
|
+
}
|
|
42
|
+
const opened = new Map();
|
|
43
|
+
const clients = new Map();
|
|
44
|
+
/** Where a tenant's database lives: the URL the Manage-App handed out, else a file in the data folder. */
|
|
45
|
+
function tenantUrl(id, dbUrl) {
|
|
46
|
+
if (dbUrl) {
|
|
47
|
+
return dbUrl;
|
|
48
|
+
}
|
|
49
|
+
if (env.tenantDbUrlTemplate) {
|
|
50
|
+
return env.tenantDbUrlTemplate.replaceAll("{tenant}", id.toLowerCase());
|
|
51
|
+
}
|
|
52
|
+
return `file:${join(env.dataDir, "tenants", `${id}.db`)}`;
|
|
53
|
+
}
|
|
54
|
+
async function openTenant(id) {
|
|
55
|
+
let row = await controlDb.query.tenant.findFirst({ where: eq(control.tenant.id, id) });
|
|
56
|
+
if (!row) {
|
|
57
|
+
await controlDb.insert(control.tenant).values({ id }).onConflictDoNothing();
|
|
58
|
+
row = (await controlDb.query.tenant.findFirst({ where: eq(control.tenant.id, id) }));
|
|
59
|
+
}
|
|
60
|
+
const url = tenantUrl(id, row.dbUrl);
|
|
61
|
+
const client = open(url, url.startsWith("file:") ? "" : env.tenantDbAuthToken);
|
|
62
|
+
clients.set(id, client);
|
|
63
|
+
await prepare(client, url);
|
|
64
|
+
const db = makeTenantDb(client);
|
|
65
|
+
// Every database is brought to the current schema when it is first opened.
|
|
66
|
+
await migrate(db, { migrationsFolder: migrationsFolder("tenant") });
|
|
67
|
+
return db;
|
|
68
|
+
}
|
|
69
|
+
/** The only way to a tenant's tables. Opens the database once per process and migrates it then. */
|
|
70
|
+
export function tenantDb(id) {
|
|
71
|
+
let db = opened.get(id);
|
|
72
|
+
if (!db) {
|
|
73
|
+
db = openTenant(id).catch((err) => {
|
|
74
|
+
opened.delete(id);
|
|
75
|
+
throw err;
|
|
76
|
+
});
|
|
77
|
+
opened.set(id, db);
|
|
78
|
+
}
|
|
79
|
+
return db;
|
|
80
|
+
}
|
|
81
|
+
/** Records where the Manage-App created the tenant's database, before it is first opened. */
|
|
82
|
+
export async function registerTenant(id, dbUrl) {
|
|
83
|
+
await controlDb
|
|
84
|
+
.insert(control.tenant)
|
|
85
|
+
.values({ id, dbUrl })
|
|
86
|
+
.onConflictDoUpdate({ target: control.tenant.id, set: { dbUrl } });
|
|
87
|
+
}
|
|
88
|
+
const ready = new Map();
|
|
89
|
+
/** Closes and forgets an open tenant database (the tenant was deleted or moved). */
|
|
90
|
+
export function closeTenantDb(id) {
|
|
91
|
+
opened.delete(id);
|
|
92
|
+
ready.delete(id);
|
|
93
|
+
clients.get(id)?.close();
|
|
94
|
+
clients.delete(id);
|
|
95
|
+
}
|
|
96
|
+
/** Opens the current tenant's database; `db` works synchronously afterwards. */
|
|
97
|
+
export async function openTenantDb(id) {
|
|
98
|
+
if (!ready.has(id)) {
|
|
99
|
+
ready.set(id, await tenantDb(id));
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/** Runs `fn` for one tenant: opens its database, then sets the context `db` resolves from. */
|
|
103
|
+
export async function withTenant(id, fn) {
|
|
104
|
+
await openTenantDb(id);
|
|
105
|
+
return inTenant(id, fn);
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* The current tenant's database, resolved from the context `inTenant()` set. Code that reads or
|
|
109
|
+
* writes wizards, runs or files never names a tenant itself.
|
|
110
|
+
*/
|
|
111
|
+
export const db = new Proxy({}, {
|
|
112
|
+
get(_target, prop) {
|
|
113
|
+
const id = currentTenant();
|
|
114
|
+
const target = ready.get(id);
|
|
115
|
+
if (!target) {
|
|
116
|
+
throw new Error(`Tenant database "${id}" is not open: enter it with withTenant().`);
|
|
117
|
+
}
|
|
118
|
+
const value = Reflect.get(target, prop);
|
|
119
|
+
return typeof value === "function" ? value.bind(target) : value;
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
export { schema };
|
|
123
|
+
/** Brings every known tenant database to the current schema (after a release). */
|
|
124
|
+
export async function migrateAllTenants() {
|
|
125
|
+
const tenants = await controlDb.select({ id: control.tenant.id }).from(control.tenant);
|
|
126
|
+
const failed = [];
|
|
127
|
+
let migrated = 0;
|
|
128
|
+
const queue = [...tenants];
|
|
129
|
+
await Promise.all(Array.from({ length: 4 }, async () => {
|
|
130
|
+
for (let t = queue.shift(); t; t = queue.shift()) {
|
|
131
|
+
try {
|
|
132
|
+
await openTenantDb(t.id);
|
|
133
|
+
migrated++;
|
|
134
|
+
}
|
|
135
|
+
catch (err) {
|
|
136
|
+
console.error(`[migrate ${t.id}]`, err);
|
|
137
|
+
failed.push(t.id);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}));
|
|
141
|
+
return { migrated, failed };
|
|
142
|
+
}
|
|
143
|
+
export async function knownTenants() {
|
|
144
|
+
const rows = await controlDb.select({ id: control.tenant.id }).from(control.tenant);
|
|
145
|
+
return rows.map((r) => r.id);
|
|
146
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { sql } from "drizzle-orm";
|
|
2
|
+
import { index, integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
|
|
3
|
+
const now = sql `(unixepoch() * 1000)`;
|
|
4
|
+
const createdAt = () => integer("created_at", { mode: "timestamp_ms" }).notNull().default(now);
|
|
5
|
+
// The control database: what the runtime must know before it can open a tenant's database,
|
|
6
|
+
// and what it counts across tenants. No wizard, run content or file lives here.
|
|
7
|
+
/** Every tenant this runtime has opened. `dbUrl` null = a libSQL file in the data folder. */
|
|
8
|
+
export const tenant = sqliteTable("tenant", {
|
|
9
|
+
id: text("id").primaryKey(),
|
|
10
|
+
status: text("status", { enum: ["active", "suspended"] })
|
|
11
|
+
.notNull()
|
|
12
|
+
.default("active"),
|
|
13
|
+
dbUrl: text("db_url"),
|
|
14
|
+
createdAt: createdAt(),
|
|
15
|
+
});
|
|
16
|
+
/** Public tokens that arrive without a session: which tenant, and which row there. */
|
|
17
|
+
export const link = sqliteTable("link", {
|
|
18
|
+
token: text("token").primaryKey(),
|
|
19
|
+
tenantId: text("tenant_id").notNull(),
|
|
20
|
+
/** `wizard` = a share link `/w/<token>`, `result` = a shared result `/s/<token>`, `logo` = a brand logo. */
|
|
21
|
+
kind: text("kind", { enum: ["wizard", "result", "logo"] }).notNull(),
|
|
22
|
+
ref: text("ref").notNull(),
|
|
23
|
+
createdAt: createdAt(),
|
|
24
|
+
}, (t) => [index("link_tenant").on(t.tenantId)]);
|
|
25
|
+
/**
|
|
26
|
+
* One row per run: finds the run's tenant, counts a visitor's runs across all wizards, and
|
|
27
|
+
* marks the runs that are working (resumed after a restart, counted against the tenant's limit).
|
|
28
|
+
*/
|
|
29
|
+
export const runIndex = sqliteTable("run_index", {
|
|
30
|
+
runId: text("run_id").primaryKey(),
|
|
31
|
+
tenantId: text("tenant_id").notNull(),
|
|
32
|
+
visitorId: text("visitor_id"),
|
|
33
|
+
ipHash: text("ip_hash"),
|
|
34
|
+
active: integer("active", { mode: "boolean" }).notNull().default(false),
|
|
35
|
+
createdAt: createdAt(),
|
|
36
|
+
}, (t) => [
|
|
37
|
+
index("run_index_visitor").on(t.visitorId, t.createdAt),
|
|
38
|
+
index("run_index_ip").on(t.ipHash, t.createdAt),
|
|
39
|
+
index("run_index_active").on(t.active, t.tenantId),
|
|
40
|
+
]);
|
|
41
|
+
/** A studio session of someone signed in at the Manage-App. The refresh token is encrypted. */
|
|
42
|
+
export const session = sqliteTable("session", {
|
|
43
|
+
id: text("id").primaryKey(),
|
|
44
|
+
userId: text("user_id").notNull(),
|
|
45
|
+
tenantId: text("tenant_id").notNull(),
|
|
46
|
+
role: text("role").notNull(),
|
|
47
|
+
name: text("name").notNull(),
|
|
48
|
+
email: text("email").notNull(),
|
|
49
|
+
image: text("image"),
|
|
50
|
+
refreshToken: text("refresh_token"),
|
|
51
|
+
/** When the access token's claims (tenant, role) must be renewed. */
|
|
52
|
+
claimsExpireAt: integer("claims_expire_at", { mode: "timestamp_ms" }).notNull(),
|
|
53
|
+
expiresAt: integer("expires_at", { mode: "timestamp_ms" }).notNull(),
|
|
54
|
+
createdAt: createdAt(),
|
|
55
|
+
}, (t) => [index("session_expires").on(t.expiresAt)]);
|
|
56
|
+
/** Settings of a runtime that runs alone: model bindings, the linked account. Never secrets. */
|
|
57
|
+
export const setting = sqliteTable("setting", {
|
|
58
|
+
key: text("key").primaryKey(),
|
|
59
|
+
value: text("value", { mode: "json" }).$type().notNull(),
|
|
60
|
+
});
|
|
61
|
+
// --- marketplace -----------------------------------------------------------------
|
|
62
|
+
// The marketplace is an app of its own (docs/marketplace-contract.md). What this runtime keeps
|
|
63
|
+
// of it for offline use: its starters and what people starred, as the marketplace exports them.
|
|
64
|
+
export const marketplaceCache = sqliteTable("marketplace_cache", {
|
|
65
|
+
id: text("id").primaryKey(),
|
|
66
|
+
/** The marketplace's hash of the entry: a different one in its list means it changed. */
|
|
67
|
+
hash: text("hash").notNull(),
|
|
68
|
+
entry: text("entry", { mode: "json" }).$type().notNull(),
|
|
69
|
+
fetchedAt: integer("fetched_at", { mode: "timestamp_ms" }).notNull().default(now),
|
|
70
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
CREATE TABLE `link` (
|
|
2
|
+
`token` text PRIMARY KEY NOT NULL,
|
|
3
|
+
`tenant_id` text NOT NULL,
|
|
4
|
+
`kind` text NOT NULL,
|
|
5
|
+
`ref` text NOT NULL,
|
|
6
|
+
`created_at` integer DEFAULT (unixepoch() * 1000) NOT NULL
|
|
7
|
+
);
|
|
8
|
+
--> statement-breakpoint
|
|
9
|
+
CREATE INDEX `link_tenant` ON `link` (`tenant_id`);--> statement-breakpoint
|
|
10
|
+
CREATE TABLE `run_index` (
|
|
11
|
+
`run_id` text PRIMARY KEY NOT NULL,
|
|
12
|
+
`tenant_id` text NOT NULL,
|
|
13
|
+
`visitor_id` text,
|
|
14
|
+
`ip_hash` text,
|
|
15
|
+
`active` integer DEFAULT false NOT NULL,
|
|
16
|
+
`created_at` integer DEFAULT (unixepoch() * 1000) NOT NULL
|
|
17
|
+
);
|
|
18
|
+
--> statement-breakpoint
|
|
19
|
+
CREATE INDEX `run_index_visitor` ON `run_index` (`visitor_id`,`created_at`);--> statement-breakpoint
|
|
20
|
+
CREATE INDEX `run_index_ip` ON `run_index` (`ip_hash`,`created_at`);--> statement-breakpoint
|
|
21
|
+
CREATE INDEX `run_index_active` ON `run_index` (`active`,`tenant_id`);--> statement-breakpoint
|
|
22
|
+
CREATE TABLE `session` (
|
|
23
|
+
`id` text PRIMARY KEY NOT NULL,
|
|
24
|
+
`user_id` text NOT NULL,
|
|
25
|
+
`tenant_id` text NOT NULL,
|
|
26
|
+
`role` text NOT NULL,
|
|
27
|
+
`name` text NOT NULL,
|
|
28
|
+
`email` text NOT NULL,
|
|
29
|
+
`image` text,
|
|
30
|
+
`refresh_token` text,
|
|
31
|
+
`claims_expire_at` integer NOT NULL,
|
|
32
|
+
`expires_at` integer NOT NULL,
|
|
33
|
+
`created_at` integer DEFAULT (unixepoch() * 1000) NOT NULL
|
|
34
|
+
);
|
|
35
|
+
--> statement-breakpoint
|
|
36
|
+
CREATE INDEX `session_expires` ON `session` (`expires_at`);--> statement-breakpoint
|
|
37
|
+
CREATE TABLE `setting` (
|
|
38
|
+
`key` text PRIMARY KEY NOT NULL,
|
|
39
|
+
`value` text NOT NULL
|
|
40
|
+
);
|
|
41
|
+
--> statement-breakpoint
|
|
42
|
+
CREATE TABLE `tenant` (
|
|
43
|
+
`id` text PRIMARY KEY NOT NULL,
|
|
44
|
+
`status` text DEFAULT 'active' NOT NULL,
|
|
45
|
+
`db_url` text,
|
|
46
|
+
`created_at` integer DEFAULT (unixepoch() * 1000) NOT NULL
|
|
47
|
+
);
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
CREATE TABLE `marketplace_file` (
|
|
2
|
+
`item_id` text NOT NULL,
|
|
3
|
+
`path` text NOT NULL,
|
|
4
|
+
`data` blob NOT NULL,
|
|
5
|
+
PRIMARY KEY(`item_id`, `path`),
|
|
6
|
+
FOREIGN KEY (`item_id`) REFERENCES `marketplace_item`(`id`) ON UPDATE no action ON DELETE cascade
|
|
7
|
+
);
|
|
8
|
+
--> statement-breakpoint
|
|
9
|
+
CREATE TABLE `marketplace_item` (
|
|
10
|
+
`id` text PRIMARY KEY NOT NULL,
|
|
11
|
+
`status` text DEFAULT 'draft' NOT NULL,
|
|
12
|
+
`revision` integer DEFAULT 1 NOT NULL,
|
|
13
|
+
`origin` text DEFAULT 'admin' NOT NULL,
|
|
14
|
+
`base_revision` integer,
|
|
15
|
+
`language` text DEFAULT 'de' NOT NULL,
|
|
16
|
+
`avatar` text DEFAULT 'round' NOT NULL,
|
|
17
|
+
`formats` text NOT NULL,
|
|
18
|
+
`industries` text NOT NULL,
|
|
19
|
+
`use_cases` text NOT NULL,
|
|
20
|
+
`capabilities` text NOT NULL,
|
|
21
|
+
`credits` integer,
|
|
22
|
+
`credits_high` integer,
|
|
23
|
+
`position` integer DEFAULT 1000 NOT NULL,
|
|
24
|
+
`installs` integer DEFAULT 0 NOT NULL,
|
|
25
|
+
`updated_by` text,
|
|
26
|
+
`created_at` integer DEFAULT (unixepoch() * 1000) NOT NULL,
|
|
27
|
+
`updated_at` integer DEFAULT (unixepoch() * 1000) NOT NULL
|
|
28
|
+
);
|
|
29
|
+
--> statement-breakpoint
|
|
30
|
+
CREATE INDEX `marketplace_item_updated` ON `marketplace_item` (`updated_at`);--> statement-breakpoint
|
|
31
|
+
CREATE TABLE `marketplace_text` (
|
|
32
|
+
`item_id` text NOT NULL,
|
|
33
|
+
`language` text NOT NULL,
|
|
34
|
+
`revision` integer NOT NULL,
|
|
35
|
+
`title` text NOT NULL,
|
|
36
|
+
`pitch` text NOT NULL,
|
|
37
|
+
`definition` text NOT NULL,
|
|
38
|
+
`machine` integer DEFAULT false NOT NULL,
|
|
39
|
+
PRIMARY KEY(`item_id`, `language`),
|
|
40
|
+
FOREIGN KEY (`item_id`) REFERENCES `marketplace_item`(`id`) ON UPDATE no action ON DELETE cascade
|
|
41
|
+
);
|