@engenty/wizards-runtime 0.0.0-stage → 0.7.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (523) hide show
  1. package/LICENSE +110 -0
  2. package/README.md +214 -2
  3. package/apps/runtime/dist/agents/architect.js +444 -0
  4. package/apps/runtime/dist/agents/studio-assistant.js +883 -0
  5. package/apps/runtime/dist/agents/subscription.js +207 -0
  6. package/apps/runtime/dist/agents/thought.js +21 -0
  7. package/apps/runtime/dist/agents/website.js +480 -0
  8. package/apps/runtime/dist/app.js +359 -0
  9. package/apps/runtime/dist/apple/wizards-apple +0 -0
  10. package/apps/runtime/dist/assistant/browser.js +26 -0
  11. package/apps/runtime/dist/assistant/docs.js +158 -0
  12. package/apps/runtime/dist/assistant/knows.js +353 -0
  13. package/apps/runtime/dist/assistant/primer.js +26 -0
  14. package/apps/runtime/dist/assistant/runs.js +24 -0
  15. package/apps/runtime/dist/auth/account.js +175 -0
  16. package/apps/runtime/dist/auth/device.js +112 -0
  17. package/apps/runtime/dist/auth/index.js +358 -0
  18. package/apps/runtime/dist/auth/keys.js +55 -0
  19. package/apps/runtime/dist/auth/local-ticket.js +26 -0
  20. package/apps/runtime/dist/authoring/example.js +74 -0
  21. package/apps/runtime/dist/authoring/guide.js +433 -0
  22. package/apps/runtime/dist/authoring/ops.js +117 -0
  23. package/apps/runtime/dist/browser/fast/action-space.js +160 -0
  24. package/apps/runtime/dist/browser/fast/classifier.js +91 -0
  25. package/apps/runtime/dist/browser/fast/decide.js +171 -0
  26. package/apps/runtime/dist/browser/fast/driver.js +141 -0
  27. package/apps/runtime/dist/browser/fast/instructions.js +33 -0
  28. package/apps/runtime/dist/browser/fast/run.js +249 -0
  29. package/apps/runtime/dist/browser/fast/span-picker.js +153 -0
  30. package/apps/runtime/dist/browser/fast/text-helper.js +178 -0
  31. package/apps/runtime/dist/browser/snapshot.js +335 -0
  32. package/apps/runtime/dist/cli/account.js +385 -0
  33. package/apps/runtime/dist/cli/autostart.js +182 -0
  34. package/apps/runtime/dist/cli/autoupdate.js +221 -0
  35. package/apps/runtime/dist/cli/clients.js +44 -0
  36. package/apps/runtime/dist/cli/connect.js +540 -0
  37. package/apps/runtime/dist/cli/environment.js +95 -0
  38. package/apps/runtime/dist/cli/exec.js +45 -0
  39. package/apps/runtime/dist/cli/features.js +50 -0
  40. package/apps/runtime/dist/cli/home.js +88 -0
  41. package/apps/runtime/dist/cli/install-env.js +33 -0
  42. package/apps/runtime/dist/cli/machine.js +107 -0
  43. package/apps/runtime/dist/cli/main.js +231 -0
  44. package/apps/runtime/dist/cli/mcp.js +136 -0
  45. package/apps/runtime/dist/cli/menu.js +202 -0
  46. package/apps/runtime/dist/cli/processes.js +72 -0
  47. package/apps/runtime/dist/cli/report.js +66 -0
  48. package/apps/runtime/dist/cli/sandbox.js +77 -0
  49. package/apps/runtime/dist/cli/setup.js +335 -0
  50. package/apps/runtime/dist/cli/start.js +276 -0
  51. package/apps/runtime/dist/cli/status.js +212 -0
  52. package/apps/runtime/dist/cli/ui.js +59 -0
  53. package/apps/runtime/dist/cli/uninstall-main.js +20 -0
  54. package/apps/runtime/dist/cli/uninstall.js +231 -0
  55. package/apps/runtime/dist/cli/update.js +86 -0
  56. package/apps/runtime/dist/cli/validate.js +75 -0
  57. package/apps/runtime/dist/cli/versions.js +123 -0
  58. package/apps/runtime/dist/connectors/builtin.js +86 -0
  59. package/apps/runtime/dist/connectors/external.js +409 -0
  60. package/apps/runtime/dist/connectors/gmail.js +107 -0
  61. package/apps/runtime/dist/connectors/imap.js +261 -0
  62. package/apps/runtime/dist/connectors/index.js +325 -0
  63. package/apps/runtime/dist/connectors/mail.js +72 -0
  64. package/apps/runtime/dist/connectors/outlook.js +90 -0
  65. package/apps/runtime/dist/connectors/plugin-connections.js +343 -0
  66. package/apps/runtime/dist/connectors/via-account.js +65 -0
  67. package/apps/runtime/dist/credits/credits.js +100 -0
  68. package/apps/runtime/dist/credits/estimate.js +152 -0
  69. package/apps/runtime/dist/db/adopt.js +149 -0
  70. package/apps/runtime/dist/db/client.js +173 -0
  71. package/apps/runtime/dist/db/control-schema.js +133 -0
  72. package/apps/runtime/dist/db/migrations/control/0000_last_absorbing_man.sql +47 -0
  73. package/apps/runtime/dist/db/migrations/control/0001_gorgeous_nicolaos.sql +41 -0
  74. package/apps/runtime/dist/db/migrations/control/0002_secret_penance.sql +1 -0
  75. package/apps/runtime/dist/db/migrations/control/0003_nasty_mordo.sql +3 -0
  76. package/apps/runtime/dist/db/migrations/control/0004_breezy_eddie_brock.sql +6 -0
  77. package/apps/runtime/dist/db/migrations/control/0005_chilly_oracle.sql +1 -0
  78. package/apps/runtime/dist/db/migrations/control/0006_sleepy_mojo.sql +1 -0
  79. package/apps/runtime/dist/db/migrations/control/0007_common_the_hand.sql +15 -0
  80. package/apps/runtime/dist/db/migrations/control/0008_runner.sql +8 -0
  81. package/apps/runtime/dist/db/migrations/control/0009_plugin_claim.sql +9 -0
  82. package/apps/runtime/dist/db/migrations/control/0010_tan_snowbird.sql +6 -0
  83. package/apps/runtime/dist/db/migrations/control/meta/0000_snapshot.json +311 -0
  84. package/apps/runtime/dist/db/migrations/control/meta/0001_snapshot.json +599 -0
  85. package/apps/runtime/dist/db/migrations/control/meta/0002_snapshot.json +607 -0
  86. package/apps/runtime/dist/db/migrations/control/meta/0003_snapshot.json +311 -0
  87. package/apps/runtime/dist/db/migrations/control/meta/0004_snapshot.json +350 -0
  88. package/apps/runtime/dist/db/migrations/control/meta/0005_snapshot.json +358 -0
  89. package/apps/runtime/dist/db/migrations/control/meta/0006_snapshot.json +365 -0
  90. package/apps/runtime/dist/db/migrations/control/meta/0007_snapshot.json +460 -0
  91. package/apps/runtime/dist/db/migrations/control/meta/0008_snapshot.json +505 -0
  92. package/apps/runtime/dist/db/migrations/control/meta/0009_snapshot.json +561 -0
  93. package/apps/runtime/dist/db/migrations/control/meta/0010_snapshot.json +600 -0
  94. package/apps/runtime/dist/db/migrations/control/meta/_journal.json +83 -0
  95. package/apps/runtime/dist/db/migrations/tenant/0000_eminent_freak.sql +188 -0
  96. package/apps/runtime/dist/db/migrations/tenant/0001_salty_master_chief.sql +1 -0
  97. package/apps/runtime/dist/db/migrations/tenant/0002_cloudy_firebird.sql +1 -0
  98. package/apps/runtime/dist/db/migrations/tenant/0003_brown_warbound.sql +52 -0
  99. package/apps/runtime/dist/db/migrations/tenant/0004_romantic_nocturne.sql +6 -0
  100. package/apps/runtime/dist/db/migrations/tenant/0005_reflective_forgotten_one.sql +2 -0
  101. package/apps/runtime/dist/db/migrations/tenant/0006_hard_war_machine.sql +30 -0
  102. package/apps/runtime/dist/db/migrations/tenant/0007_space_data.sql +43 -0
  103. package/apps/runtime/dist/db/migrations/tenant/0008_run_event_note.sql +1 -0
  104. package/apps/runtime/dist/db/migrations/tenant/0009_knowledge.sql +104 -0
  105. package/apps/runtime/dist/db/migrations/tenant/0010_plugin_connection.sql +15 -0
  106. package/apps/runtime/dist/db/migrations/tenant/0011_runners.sql +2 -0
  107. package/apps/runtime/dist/db/migrations/tenant/0012_assistant_chat.sql +40 -0
  108. package/apps/runtime/dist/db/migrations/tenant/0013_run_origin.sql +1 -0
  109. package/apps/runtime/dist/db/migrations/tenant/0014_run_answer.sql +2 -0
  110. package/apps/runtime/dist/db/migrations/tenant/0015_device_calls.sql +13 -0
  111. package/apps/runtime/dist/db/migrations/tenant/0016_even_trish_tilby.sql +12 -0
  112. package/apps/runtime/dist/db/migrations/tenant/meta/0000_snapshot.json +1323 -0
  113. package/apps/runtime/dist/db/migrations/tenant/meta/0001_snapshot.json +1330 -0
  114. package/apps/runtime/dist/db/migrations/tenant/meta/0002_snapshot.json +1337 -0
  115. package/apps/runtime/dist/db/migrations/tenant/meta/0003_snapshot.json +1605 -0
  116. package/apps/runtime/dist/db/migrations/tenant/meta/0004_snapshot.json +1645 -0
  117. package/apps/runtime/dist/db/migrations/tenant/meta/0005_snapshot.json +1659 -0
  118. package/apps/runtime/dist/db/migrations/tenant/meta/0006_snapshot.json +1696 -0
  119. package/apps/runtime/dist/db/migrations/tenant/meta/0007_snapshot.json +2012 -0
  120. package/apps/runtime/dist/db/migrations/tenant/meta/0008_snapshot.json +2019 -0
  121. package/apps/runtime/dist/db/migrations/tenant/meta/0009_snapshot.json +2837 -0
  122. package/apps/runtime/dist/db/migrations/tenant/meta/0010_snapshot.json +2942 -0
  123. package/apps/runtime/dist/db/migrations/tenant/meta/0011_snapshot.json +2956 -0
  124. package/apps/runtime/dist/db/migrations/tenant/meta/0012_snapshot.json +3132 -0
  125. package/apps/runtime/dist/db/migrations/tenant/meta/0013_snapshot.json +3139 -0
  126. package/apps/runtime/dist/db/migrations/tenant/meta/0014_snapshot.json +3154 -0
  127. package/apps/runtime/dist/db/migrations/tenant/meta/0015_snapshot.json +3243 -0
  128. package/apps/runtime/dist/db/migrations/tenant/meta/0016_snapshot.json +3326 -0
  129. package/apps/runtime/dist/db/migrations/tenant/meta/_journal.json +125 -0
  130. package/apps/runtime/dist/db/schema.js +602 -0
  131. package/apps/runtime/dist/debug.js +197 -0
  132. package/apps/runtime/dist/docs/dev/index.md +41 -0
  133. package/apps/runtime/dist/docs/dev/meta.json +7 -0
  134. package/apps/runtime/dist/docs/dev/plugins/connections.md +74 -0
  135. package/apps/runtime/dist/docs/dev/plugins/events.md +89 -0
  136. package/apps/runtime/dist/docs/dev/plugins/examples.md +101 -0
  137. package/apps/runtime/dist/docs/dev/plugins/index.md +78 -0
  138. package/apps/runtime/dist/docs/dev/plugins/layout.md +90 -0
  139. package/apps/runtime/dist/docs/dev/plugins/meta.json +28 -0
  140. package/apps/runtime/dist/docs/dev/plugins/quick-start.md +94 -0
  141. package/apps/runtime/dist/docs/dev/plugins/reference.md +348 -0
  142. package/apps/runtime/dist/docs/dev/plugins/reloading.md +76 -0
  143. package/apps/runtime/dist/docs/dev/plugins/routes.md +185 -0
  144. package/apps/runtime/dist/docs/dev/plugins/runners.md +230 -0
  145. package/apps/runtime/dist/docs/dev/plugins/server.md +281 -0
  146. package/apps/runtime/dist/docs/dev/plugins/shipping.md +79 -0
  147. package/apps/runtime/dist/docs/dev/plugins/space.md +297 -0
  148. package/apps/runtime/dist/docs/dev/plugins/starters.md +70 -0
  149. package/apps/runtime/dist/docs/dev/plugins/studio.md +363 -0
  150. package/apps/runtime/dist/docs/dev/plugins/styling.md +129 -0
  151. package/apps/runtime/dist/docs/dev/plugins/tables.md +104 -0
  152. package/apps/runtime/dist/docs/dev/plugins/tools.md +139 -0
  153. package/apps/runtime/dist/docs/dev/wizard-format.md +125 -0
  154. package/apps/runtime/dist/docs/meta.json +3 -0
  155. package/apps/runtime/dist/docs/user/ai-apps.md +112 -0
  156. package/apps/runtime/dist/docs/user/assistant.md +77 -0
  157. package/apps/runtime/dist/docs/user/build-a-wizard.md +116 -0
  158. package/apps/runtime/dist/docs/user/command-line.md +86 -0
  159. package/apps/runtime/dist/docs/user/connectors.md +90 -0
  160. package/apps/runtime/dist/docs/user/create-account.md +52 -0
  161. package/apps/runtime/dist/docs/user/data.md +66 -0
  162. package/apps/runtime/dist/docs/user/first-start.md +60 -0
  163. package/apps/runtime/dist/docs/user/index.md +49 -0
  164. package/apps/runtime/dist/docs/user/install.md +123 -0
  165. package/apps/runtime/dist/docs/user/local-only.md +42 -0
  166. package/apps/runtime/dist/docs/user/meta.json +32 -0
  167. package/apps/runtime/dist/docs/user/models.md +156 -0
  168. package/apps/runtime/dist/docs/user/plugins.md +71 -0
  169. package/apps/runtime/dist/docs/user/run.md +98 -0
  170. package/apps/runtime/dist/docs/user/server.md +52 -0
  171. package/apps/runtime/dist/docs/user/space.md +126 -0
  172. package/apps/runtime/dist/docs/user/steps.md +220 -0
  173. package/apps/runtime/dist/docs/user/test-and-share.md +224 -0
  174. package/apps/runtime/dist/docs/user/troubleshooting.md +67 -0
  175. package/apps/runtime/dist/docs/user/wordpress.md +121 -0
  176. package/apps/runtime/dist/documents/parse.js +246 -0
  177. package/apps/runtime/dist/engenty/connections-external/build-connector.js +193 -0
  178. package/apps/runtime/dist/engenty/connections-external/errors.js +11 -0
  179. package/apps/runtime/dist/engenty/connections-external/import-service.js +270 -0
  180. package/apps/runtime/dist/engenty/connections-external/importer/classify.js +34 -0
  181. package/apps/runtime/dist/engenty/connections-external/importer/map-auth.js +185 -0
  182. package/apps/runtime/dist/engenty/connections-external/importer/normalize-mcp.js +63 -0
  183. package/apps/runtime/dist/engenty/connections-external/importer/normalize-openapi.js +130 -0
  184. package/apps/runtime/dist/engenty/connections-external/invoke/http-invoker.js +143 -0
  185. package/apps/runtime/dist/engenty/connections-external/invoke/mcp-client.js +143 -0
  186. package/apps/runtime/dist/engenty/connections-external/net/guarded-fetch.js +119 -0
  187. package/apps/runtime/dist/engenty/connections-external/oauth-dcr.js +88 -0
  188. package/apps/runtime/dist/engenty/connections-external/registry-client.js +336 -0
  189. package/apps/runtime/dist/engenty/connections-external/registry-source.js +82 -0
  190. package/apps/runtime/dist/engenty/connections-external/types.js +7 -0
  191. package/apps/runtime/dist/engenty/connections-github/connector.js +209 -0
  192. package/apps/runtime/dist/engenty/connections-google/connectors/calendar.js +381 -0
  193. package/apps/runtime/dist/engenty/connections-google/connectors/contacts.js +94 -0
  194. package/apps/runtime/dist/engenty/connections-google/connectors/drive.js +290 -0
  195. package/apps/runtime/dist/engenty/connections-google/connectors/gmail.js +567 -0
  196. package/apps/runtime/dist/engenty/connections-google/definitions.js +11 -0
  197. package/apps/runtime/dist/engenty/connections-google/shared.js +73 -0
  198. package/apps/runtime/dist/engenty/connections-hubspot/connector.js +140 -0
  199. package/apps/runtime/dist/engenty/connections-microsoft/action.js +17 -0
  200. package/apps/runtime/dist/engenty/connections-microsoft/graph.js +114 -0
  201. package/apps/runtime/dist/engenty/connections-microsoft/onedrive.js +286 -0
  202. package/apps/runtime/dist/engenty/connections-microsoft/outlook.js +492 -0
  203. package/apps/runtime/dist/engenty/connections-s3/s3.js +328 -0
  204. package/apps/runtime/dist/engenty/connections-sdk/files-capability.js +86 -0
  205. package/apps/runtime/dist/engenty/connections-sdk/oauth2.js +150 -0
  206. package/apps/runtime/dist/engenty/connections-sdk/registry.js +89 -0
  207. package/apps/runtime/dist/engenty/connections-sdk/storage-capability.js +84 -0
  208. package/apps/runtime/dist/engenty/connections-sdk/types.js +30 -0
  209. package/apps/runtime/dist/engenty/connections-slack/connector.js +360 -0
  210. package/apps/runtime/dist/engenty/csv-import/csv-matrix.js +178 -0
  211. package/apps/runtime/dist/engenty/csv-import/import-sources.js +87 -0
  212. package/apps/runtime/dist/engenty/csv-import/parse-csv.js +129 -0
  213. package/apps/runtime/dist/engenty/csv-import/serialize-csv.js +20 -0
  214. package/apps/runtime/dist/engenty/csv-import/types.js +1 -0
  215. package/apps/runtime/dist/engenty/csv-import/xlsx-workbook.js +159 -0
  216. package/apps/runtime/dist/engenty/doc-converter/interface.js +9 -0
  217. package/apps/runtime/dist/engenty/doc-converter/page-break.js +92 -0
  218. package/apps/runtime/dist/engenty/doc-converter/providers/local/index.js +72 -0
  219. package/apps/runtime/dist/engenty/document-scanner/schemas/classifier.js +30 -0
  220. package/apps/runtime/dist/engenty/document-scanner/schemas/index.js +4 -0
  221. package/apps/runtime/dist/engenty/document-scanner/schemas/invoice.js +73 -0
  222. package/apps/runtime/dist/engenty/document-scanner/schemas/receipt.js +63 -0
  223. package/apps/runtime/dist/engenty/document-scanner/schemas/shared.js +40 -0
  224. package/apps/runtime/dist/engenty/search-index/chunks.js +164 -0
  225. package/apps/runtime/dist/engenty/search-index/contracts.js +9 -0
  226. package/apps/runtime/dist/engenty/shims/connections-sdk.js +39 -0
  227. package/apps/runtime/dist/engenty/shims/plugin-sdk.js +1 -0
  228. package/apps/runtime/dist/engenty/shims/web-ingest.js +19 -0
  229. package/apps/runtime/dist/engenty/web-ingest/ssrf.js +277 -0
  230. package/apps/runtime/dist/engine/asks.js +82 -0
  231. package/apps/runtime/dist/engine/decide.js +82 -0
  232. package/apps/runtime/dist/engine/device.js +194 -0
  233. package/apps/runtime/dist/engine/events.js +128 -0
  234. package/apps/runtime/dist/engine/input.js +208 -0
  235. package/apps/runtime/dist/engine/owner.js +32 -0
  236. package/apps/runtime/dist/engine/prepare.js +90 -0
  237. package/apps/runtime/dist/engine/push.js +58 -0
  238. package/apps/runtime/dist/engine/requirements.js +355 -0
  239. package/apps/runtime/dist/engine/resources.js +198 -0
  240. package/apps/runtime/dist/engine/runner.js +970 -0
  241. package/apps/runtime/dist/engine/steps.js +1000 -0
  242. package/apps/runtime/dist/engine/surface.js +91 -0
  243. package/apps/runtime/dist/engine/template.js +240 -0
  244. package/apps/runtime/dist/engine/types.js +4 -0
  245. package/apps/runtime/dist/env.js +279 -0
  246. package/apps/runtime/dist/features.js +123 -0
  247. package/apps/runtime/dist/files/blobs.js +22 -0
  248. package/apps/runtime/dist/files/objects.js +86 -0
  249. package/apps/runtime/dist/files/storage.js +120 -0
  250. package/apps/runtime/dist/files/web-images.js +101 -0
  251. package/apps/runtime/dist/film/project.js +257 -0
  252. package/apps/runtime/dist/film/skills.js +91 -0
  253. package/apps/runtime/dist/film/sounds.js +122 -0
  254. package/apps/runtime/dist/film/step.js +289 -0
  255. package/apps/runtime/dist/film/tools.js +99 -0
  256. package/apps/runtime/dist/geocode.js +58 -0
  257. package/apps/runtime/dist/harness/antigravity.js +129 -0
  258. package/apps/runtime/dist/harness/apple.js +270 -0
  259. package/apps/runtime/dist/harness/claude.js +147 -0
  260. package/apps/runtime/dist/harness/codex.js +229 -0
  261. package/apps/runtime/dist/harness/cursor.js +93 -0
  262. package/apps/runtime/dist/harness/env.js +124 -0
  263. package/apps/runtime/dist/harness/index.js +95 -0
  264. package/apps/runtime/dist/harness/model.js +279 -0
  265. package/apps/runtime/dist/harness/prompt.js +121 -0
  266. package/apps/runtime/dist/harness/terminal.js +194 -0
  267. package/apps/runtime/dist/harness/types.js +7 -0
  268. package/apps/runtime/dist/index.js +113 -0
  269. package/apps/runtime/dist/limits.js +81 -0
  270. package/apps/runtime/dist/link-preview.js +60 -0
  271. package/apps/runtime/dist/log.js +106 -0
  272. package/apps/runtime/dist/manage.js +249 -0
  273. package/apps/runtime/dist/mcp/auth.js +161 -0
  274. package/apps/runtime/dist/mcp/bridge.js +59 -0
  275. package/apps/runtime/dist/mcp/flow-app.js +97 -0
  276. package/apps/runtime/dist/mcp/handler.js +54 -0
  277. package/apps/runtime/dist/mcp/scopes.js +14 -0
  278. package/apps/runtime/dist/mcp/seen.js +65 -0
  279. package/apps/runtime/dist/mcp/tools.js +572 -0
  280. package/apps/runtime/dist/media/conversation.js +167 -0
  281. package/apps/runtime/dist/media/ffmpeg.js +121 -0
  282. package/apps/runtime/dist/media/generate.js +120 -0
  283. package/apps/runtime/dist/media/marking.js +121 -0
  284. package/apps/runtime/dist/media/transcribe.js +198 -0
  285. package/apps/runtime/dist/model-errors.js +42 -0
  286. package/apps/runtime/dist/models.js +1164 -0
  287. package/apps/runtime/dist/plugin.js +61 -0
  288. package/apps/runtime/dist/plugins/channels.js +78 -0
  289. package/apps/runtime/dist/plugins/claims.js +56 -0
  290. package/apps/runtime/dist/plugins/discovery.js +155 -0
  291. package/apps/runtime/dist/plugins/events.js +63 -0
  292. package/apps/runtime/dist/plugins/jobs.js +64 -0
  293. package/apps/runtime/dist/plugins/loader.js +508 -0
  294. package/apps/runtime/dist/plugins/migrations.js +69 -0
  295. package/apps/runtime/dist/plugins/pro-keys.js +34 -0
  296. package/apps/runtime/dist/plugins/pro-state.js +83 -0
  297. package/apps/runtime/dist/plugins/pro.js +310 -0
  298. package/apps/runtime/dist/plugins/public.js +44 -0
  299. package/apps/runtime/dist/plugins/registry.js +122 -0
  300. package/apps/runtime/dist/plugins/runs.js +210 -0
  301. package/apps/runtime/dist/render/browser-path.js +54 -0
  302. package/apps/runtime/dist/render/chromium.js +98 -0
  303. package/apps/runtime/dist/render/convert.js +158 -0
  304. package/apps/runtime/dist/render/downloads.js +299 -0
  305. package/apps/runtime/dist/render/guard.js +25 -0
  306. package/apps/runtime/dist/render/shaped.js +137 -0
  307. package/apps/runtime/dist/routes/api.js +440 -0
  308. package/apps/runtime/dist/routes/debug.js +42 -0
  309. package/apps/runtime/dist/routes/delivery.js +90 -0
  310. package/apps/runtime/dist/routes/internal.js +81 -0
  311. package/apps/runtime/dist/routes/lang.js +25 -0
  312. package/apps/runtime/dist/routes/marketplace.js +43 -0
  313. package/apps/runtime/dist/routes/plugin-public.js +113 -0
  314. package/apps/runtime/dist/routes/plugins.js +212 -0
  315. package/apps/runtime/dist/routes/runs.js +1422 -0
  316. package/apps/runtime/dist/routes/studio.js +1629 -0
  317. package/apps/runtime/dist/routes/wizard-stream.js +30 -0
  318. package/apps/runtime/dist/running.js +39 -0
  319. package/apps/runtime/dist/sandbox/agentos-package.js +73 -0
  320. package/apps/runtime/dist/sandbox/agentos.js +162 -0
  321. package/apps/runtime/dist/sandbox/docker.js +45 -0
  322. package/apps/runtime/dist/sandbox/index.js +52 -0
  323. package/apps/runtime/dist/sandbox/install.js +122 -0
  324. package/apps/runtime/dist/sandbox/mastra.js +71 -0
  325. package/apps/runtime/dist/sandbox/types.js +1 -0
  326. package/apps/runtime/dist/secrets/crypto.js +28 -0
  327. package/apps/runtime/dist/secrets/signing.js +52 -0
  328. package/apps/runtime/dist/secrets/vault.js +59 -0
  329. package/apps/runtime/dist/services/access.js +88 -0
  330. package/apps/runtime/dist/services/api.js +248 -0
  331. package/apps/runtime/dist/services/architect.js +95 -0
  332. package/apps/runtime/dist/services/assistant-chats.js +311 -0
  333. package/apps/runtime/dist/services/brand.js +85 -0
  334. package/apps/runtime/dist/services/cloud.js +456 -0
  335. package/apps/runtime/dist/services/draft-events.js +27 -0
  336. package/apps/runtime/dist/services/engenty-shapes.js +71 -0
  337. package/apps/runtime/dist/services/errors.js +29 -0
  338. package/apps/runtime/dist/services/files.js +178 -0
  339. package/apps/runtime/dist/services/knowledge-model.js +559 -0
  340. package/apps/runtime/dist/services/knowledge.js +721 -0
  341. package/apps/runtime/dist/services/links.js +16 -0
  342. package/apps/runtime/dist/services/marketplace.js +492 -0
  343. package/apps/runtime/dist/services/onboarding.js +152 -0
  344. package/apps/runtime/dist/services/package-file.js +82 -0
  345. package/apps/runtime/dist/services/package.js +170 -0
  346. package/apps/runtime/dist/services/profile.js +51 -0
  347. package/apps/runtime/dist/services/project-files.js +439 -0
  348. package/apps/runtime/dist/services/project-index.js +707 -0
  349. package/apps/runtime/dist/services/projects.js +387 -0
  350. package/apps/runtime/dist/services/relevance.js +58 -0
  351. package/apps/runtime/dist/services/runners.js +178 -0
  352. package/apps/runtime/dist/services/runs.js +524 -0
  353. package/apps/runtime/dist/services/shares.js +130 -0
  354. package/apps/runtime/dist/services/site.js +270 -0
  355. package/apps/runtime/dist/services/space-categories.js +861 -0
  356. package/apps/runtime/dist/services/space-data.js +792 -0
  357. package/apps/runtime/dist/services/space-origin.js +207 -0
  358. package/apps/runtime/dist/services/spaces.js +657 -0
  359. package/apps/runtime/dist/services/translations.js +306 -0
  360. package/apps/runtime/dist/services/widgets.js +34 -0
  361. package/apps/runtime/dist/services/wizard-icon.js +157 -0
  362. package/apps/runtime/dist/services/wizards.js +420 -0
  363. package/apps/runtime/dist/settings.js +16 -0
  364. package/apps/runtime/dist/store/errors.js +4 -0
  365. package/apps/runtime/dist/store/index.js +299 -0
  366. package/apps/runtime/dist/store/shared-lists.js +123 -0
  367. package/apps/runtime/dist/tenants/control.js +238 -0
  368. package/apps/runtime/dist/tenants/tenant.js +26 -0
  369. package/apps/runtime/dist/texts.js +207 -0
  370. package/apps/runtime/dist/tools/browser.js +703 -0
  371. package/apps/runtime/dist/tools/connector.js +204 -0
  372. package/apps/runtime/dist/tools/index.js +291 -0
  373. package/apps/runtime/dist/tools/mail.js +104 -0
  374. package/apps/runtime/dist/tools/net-guard.js +170 -0
  375. package/apps/runtime/dist/tools/pages.js +81 -0
  376. package/apps/runtime/dist/tools/plugin.js +180 -0
  377. package/apps/runtime/dist/tools/project.js +154 -0
  378. package/apps/runtime/dist/tools/shared.js +69 -0
  379. package/apps/runtime/dist/tools/store.js +238 -0
  380. package/apps/runtime/dist/update.js +113 -0
  381. package/apps/runtime/dist/widgets/bundle.js +99 -0
  382. package/apps/runtime/dist/widgets/render.js +329 -0
  383. package/apps/runtime/dist/widgets/runtime.js +108 -0
  384. package/apps/runtime/dist/widgets/script.js +30 -0
  385. package/apps/runtime/dist/widgets/step.js +223 -0
  386. package/apps/web/dist/ai-labels/ai-black.svg +20 -0
  387. package/apps/web/dist/ai-labels/ai-generated-black.svg +31 -0
  388. package/apps/web/dist/ai-labels/ai-generated-white.svg +32 -0
  389. package/apps/web/dist/ai-labels/ai-modified-black.svg +30 -0
  390. package/apps/web/dist/ai-labels/ai-modified-white.svg +31 -0
  391. package/apps/web/dist/ai-labels/ai-white.svg +21 -0
  392. package/apps/web/dist/assets/AboutDialog-DYgXPagX.js +1 -0
  393. package/apps/web/dist/assets/ChangelogDialog-BXqBW8j3.js +1 -0
  394. package/apps/web/dist/assets/ChatView-CK2--tsE.js +10 -0
  395. package/apps/web/dist/assets/EditorPage-jvcwyXbU.js +3 -0
  396. package/apps/web/dist/assets/FlowPreview-CusP7ci9.js +1 -0
  397. package/apps/web/dist/assets/Harness-BrP-ENHg.css +1 -0
  398. package/apps/web/dist/assets/Harness-CV9uYzbv.js +16 -0
  399. package/apps/web/dist/assets/NewWizardChannels-BakW5q6s.js +1 -0
  400. package/apps/web/dist/assets/NewWizardPage-BoA-LJuW.js +7 -0
  401. package/apps/web/dist/assets/OnboardingPage-DaD1XnOe.js +1 -0
  402. package/apps/web/dist/assets/ProjectPage-BFgk1DEv.js +1 -0
  403. package/apps/web/dist/assets/PublicRunner-C9MwEvxy.js +1 -0
  404. package/apps/web/dist/assets/ReadOnly-DC2Kp7l8.js +1 -0
  405. package/apps/web/dist/assets/RunDrawer-CY6NZSrI.js +4 -0
  406. package/apps/web/dist/assets/RunnerView-BT26XgcY.js +1 -0
  407. package/apps/web/dist/assets/SettingsPage-AFt9IDLt.js +1 -0
  408. package/apps/web/dist/assets/SetupPage-DtFYAbwJ.js +1 -0
  409. package/apps/web/dist/assets/SharePage-t2xwNS61.js +1 -0
  410. package/apps/web/dist/assets/changelog-CATQFxCQ.js +1 -0
  411. package/apps/web/dist/assets/engenty-eXZ-EAwd.js +1 -0
  412. package/apps/web/dist/assets/esm-DXeMu8Lb.js +7 -0
  413. package/apps/web/dist/assets/fluffy-engenty-ClRou7yu.js +456 -0
  414. package/apps/web/dist/assets/index-AfUa8xKD.css +1 -0
  415. package/apps/web/dist/assets/index-Drw7FMTQ.js +14 -0
  416. package/apps/web/dist/assets/jsx-runtime-BNakU3Ej.js +1 -0
  417. package/apps/web/dist/assets/lock-R_CfU0Xo.js +1 -0
  418. package/apps/web/dist/assets/oss-credits-cZ9NUaLZ.js +1 -0
  419. package/apps/web/dist/assets/pdfmake-Cx9scTbD.js +228 -0
  420. package/apps/web/dist/assets/phone-CgrIS4_-.js +1 -0
  421. package/apps/web/dist/assets/preload-helper-CNEI7N0N.js +1 -0
  422. package/apps/web/dist/assets/qr-scanner-worker.min-CJaXo3yu.js +98 -0
  423. package/apps/web/dist/assets/qr-scanner.min-C_UI0gH-.js +1 -0
  424. package/apps/web/dist/assets/rolldown-runtime-hePW80VL.js +1 -0
  425. package/apps/web/dist/assets/settings-menu-2IMOOlLl.js +1 -0
  426. package/apps/web/dist/assets/ui-dzGvc3xk.js +137 -0
  427. package/apps/web/dist/assets/upload-BSuTilFF.js +1 -0
  428. package/apps/web/dist/assets/vfs_fonts-DllDSYt1.js +1 -0
  429. package/apps/web/dist/embed.js +266 -0
  430. package/apps/web/dist/favicon.svg +1 -0
  431. package/apps/web/dist/icons/apple-touch-icon.png +0 -0
  432. package/apps/web/dist/icons/icon-192.png +0 -0
  433. package/apps/web/dist/icons/icon-512.png +0 -0
  434. package/apps/web/dist/icons/icon-maskable-512.png +0 -0
  435. package/apps/web/dist/index.html +48 -0
  436. package/apps/web/dist/install.sh +577 -0
  437. package/apps/web/dist/manifest.webmanifest +21 -0
  438. package/apps/web/dist/mcp/flow.html +980 -0
  439. package/apps/web/dist/sw.js +15 -0
  440. package/bin/wizards.mjs +288 -0
  441. package/modules/README.md +15 -0
  442. package/node_modules/@engenty/wizards/dist/plugin/index.d.ts +1150 -0
  443. package/node_modules/@engenty/wizards/dist/plugin/index.js +4 -0
  444. package/node_modules/@engenty/wizards/dist/plugin/studio.d.ts +322 -0
  445. package/node_modules/@engenty/wizards/dist/plugin/studio.js +4 -0
  446. package/node_modules/@engenty/wizards/dist/plugin/thread.d.ts +174 -0
  447. package/node_modules/@engenty/wizards/dist/plugin/thread.js +821 -0
  448. package/node_modules/@engenty/wizards/package.json +11 -0
  449. package/node_modules/@engenty-wizards/shared/dist/assistant.d.ts +174 -0
  450. package/node_modules/@engenty-wizards/shared/dist/assistant.js +65 -0
  451. package/node_modules/@engenty-wizards/shared/dist/changes.d.ts +22 -0
  452. package/node_modules/@engenty-wizards/shared/dist/changes.js +61 -0
  453. package/node_modules/@engenty-wizards/shared/dist/connectors.d.ts +77 -0
  454. package/node_modules/@engenty-wizards/shared/dist/connectors.js +2 -0
  455. package/node_modules/@engenty-wizards/shared/dist/decision.d.ts +76 -0
  456. package/node_modules/@engenty-wizards/shared/dist/decision.js +147 -0
  457. package/node_modules/@engenty-wizards/shared/dist/definition.d.ts +2922 -0
  458. package/node_modules/@engenty-wizards/shared/dist/definition.js +1920 -0
  459. package/node_modules/@engenty-wizards/shared/dist/engenty/data-tables/columns-format.d.ts +5 -0
  460. package/node_modules/@engenty-wizards/shared/dist/engenty/data-tables/columns-format.js +132 -0
  461. package/node_modules/@engenty-wizards/shared/dist/engenty/data-tables/columns-value.d.ts +11 -0
  462. package/node_modules/@engenty-wizards/shared/dist/engenty/data-tables/columns-value.js +209 -0
  463. package/node_modules/@engenty-wizards/shared/dist/engenty/data-tables/columns.d.ts +532 -0
  464. package/node_modules/@engenty-wizards/shared/dist/engenty/data-tables/columns.js +217 -0
  465. package/node_modules/@engenty-wizards/shared/dist/engenty/data-tables/index.d.ts +3 -0
  466. package/node_modules/@engenty-wizards/shared/dist/engenty/data-tables/index.js +3 -0
  467. package/node_modules/@engenty-wizards/shared/dist/engenty/search-index/catalog-lexical.d.ts +14 -0
  468. package/node_modules/@engenty-wizards/shared/dist/engenty/search-index/catalog-lexical.js +166 -0
  469. package/node_modules/@engenty-wizards/shared/dist/engenty/search-index/catalog-rank.d.ts +33 -0
  470. package/node_modules/@engenty-wizards/shared/dist/engenty/search-index/catalog-rank.js +99 -0
  471. package/node_modules/@engenty-wizards/shared/dist/feature-flags.d.ts +78 -0
  472. package/node_modules/@engenty-wizards/shared/dist/feature-flags.js +70 -0
  473. package/node_modules/@engenty-wizards/shared/dist/features.d.ts +41 -0
  474. package/node_modules/@engenty-wizards/shared/dist/features.js +89 -0
  475. package/node_modules/@engenty-wizards/shared/dist/fit.d.ts +71 -0
  476. package/node_modules/@engenty-wizards/shared/dist/fit.js +382 -0
  477. package/node_modules/@engenty-wizards/shared/dist/json-schema.d.ts +14 -0
  478. package/node_modules/@engenty-wizards/shared/dist/json-schema.js +66 -0
  479. package/node_modules/@engenty-wizards/shared/dist/knowledge.d.ts +199 -0
  480. package/node_modules/@engenty-wizards/shared/dist/knowledge.js +68 -0
  481. package/node_modules/@engenty-wizards/shared/dist/marketplace-entry.d.ts +35 -0
  482. package/node_modules/@engenty-wizards/shared/dist/marketplace-entry.js +107 -0
  483. package/node_modules/@engenty-wizards/shared/dist/marketplace-search.d.ts +20 -0
  484. package/node_modules/@engenty-wizards/shared/dist/marketplace-search.js +136 -0
  485. package/node_modules/@engenty-wizards/shared/dist/marketplace.d.ts +227 -0
  486. package/node_modules/@engenty-wizards/shared/dist/marketplace.js +354 -0
  487. package/node_modules/@engenty-wizards/shared/dist/merge.d.ts +7 -0
  488. package/node_modules/@engenty-wizards/shared/dist/merge.js +40 -0
  489. package/node_modules/@engenty-wizards/shared/dist/numbers.d.ts +24 -0
  490. package/node_modules/@engenty-wizards/shared/dist/numbers.js +54 -0
  491. package/node_modules/@engenty-wizards/shared/dist/projects.d.ts +224 -0
  492. package/node_modules/@engenty-wizards/shared/dist/projects.js +319 -0
  493. package/node_modules/@engenty-wizards/shared/dist/providers.d.ts +101 -0
  494. package/node_modules/@engenty-wizards/shared/dist/providers.js +390 -0
  495. package/node_modules/@engenty-wizards/shared/dist/run.d.ts +680 -0
  496. package/node_modules/@engenty-wizards/shared/dist/run.js +620 -0
  497. package/node_modules/@engenty-wizards/shared/dist/runner-texts.d.ts +546 -0
  498. package/node_modules/@engenty-wizards/shared/dist/runner-texts.js +523 -0
  499. package/node_modules/@engenty-wizards/shared/dist/runners.d.ts +93 -0
  500. package/node_modules/@engenty-wizards/shared/dist/runners.js +146 -0
  501. package/node_modules/@engenty-wizards/shared/dist/shape.d.ts +102 -0
  502. package/node_modules/@engenty-wizards/shared/dist/shape.js +318 -0
  503. package/node_modules/@engenty-wizards/shared/dist/space-data.d.ts +89 -0
  504. package/node_modules/@engenty-wizards/shared/dist/space-data.js +47 -0
  505. package/node_modules/@engenty-wizards/shared/dist/store.d.ts +117 -0
  506. package/node_modules/@engenty-wizards/shared/dist/store.js +81 -0
  507. package/node_modules/@engenty-wizards/shared/dist/surface.d.ts +115 -0
  508. package/node_modules/@engenty-wizards/shared/dist/surface.js +442 -0
  509. package/node_modules/@engenty-wizards/shared/dist/texts.d.ts +45 -0
  510. package/node_modules/@engenty-wizards/shared/dist/texts.js +54 -0
  511. package/node_modules/@engenty-wizards/shared/dist/translation.d.ts +23 -0
  512. package/node_modules/@engenty-wizards/shared/dist/translation.js +154 -0
  513. package/node_modules/@engenty-wizards/shared/dist/wizard-package.d.ts +48 -0
  514. package/node_modules/@engenty-wizards/shared/dist/wizard-package.js +49 -0
  515. package/node_modules/@engenty-wizards/shared/dist/workspace.d.ts +16 -0
  516. package/node_modules/@engenty-wizards/shared/dist/workspace.js +51 -0
  517. package/node_modules/@engenty-wizards/shared/package.json +10 -0
  518. package/package.json +77 -4
  519. package/plugin/.claude-plugin/plugin.json +9 -0
  520. package/plugin/.mcp.json +8 -0
  521. package/plugin/commands/wizard.md +8 -0
  522. package/plugin/skills/build-wizard/SKILL.md +27 -0
  523. package/plugin/skills/use-wizard/SKILL.md +18 -0
@@ -0,0 +1,299 @@
1
+ import { note } from "@engenty-wizards/shared/run";
2
+ import { firstRowAt, newCells, patchCells, rowKey, STORE_LIMITS, } from "@engenty-wizards/shared/store";
3
+ import { cleanPath, mimeForPath } from "@engenty-wizards/shared/workspace";
4
+ import { and, asc, eq, inArray, like, lt, sql } from "drizzle-orm";
5
+ import { nanoid } from "nanoid";
6
+ import { db, schema } from "../db/client.js";
7
+ import { env } from "../env.js";
8
+ import { getBlob, putBlob } from "../files/blobs.js";
9
+ import { seal, unseal } from "../secrets/crypto.js";
10
+ import { StoreError } from "./errors.js";
11
+ import { deleteSharedRows, saveSharedRows, sharedRows } from "./shared-lists.js";
12
+ export { StoreError };
13
+ /** The person a run belongs to: the signed-in admin, else the visitor of the shared link. */
14
+ export function holderOf(run) {
15
+ if (run.userId) {
16
+ return `u:${run.userId}`;
17
+ }
18
+ return run.visitorId ? `v:${run.visitorId}` : `r:${run.id}`;
19
+ }
20
+ export function scopeOf(run) {
21
+ return { wizardId: run.wizardId, holder: holderOf(run) };
22
+ }
23
+ /** Marks the store as in use; stores nobody came back to are cleaned up. */
24
+ export async function touchStore(scope) {
25
+ await db
26
+ .insert(schema.storeHolder)
27
+ .values({ ...scope, usedAt: new Date() })
28
+ .onConflictDoUpdate({
29
+ target: [schema.storeHolder.wizardId, schema.storeHolder.holder],
30
+ set: { usedAt: new Date() },
31
+ });
32
+ }
33
+ // --- lists -------------------------------------------------------------------
34
+ const inList = (scope, list) => and(eq(schema.storeRow.wizardId, scope.wizardId), eq(schema.storeRow.holder, scope.holder), eq(schema.storeRow.list, list));
35
+ function toRow(r) {
36
+ return { id: r.id, cells: r.cells, updatedAt: r.updatedAt.toISOString() };
37
+ }
38
+ /** The rows of a list: the person's, or every run's where the list is shared. */
39
+ export async function listRows(scope, def) {
40
+ if (def.shared) {
41
+ return sharedRows(scope.wizardId, def);
42
+ }
43
+ const rows = await db.query.storeRow.findMany({
44
+ where: inList(scope, def.id),
45
+ orderBy: [asc(schema.storeRow.createdAt), asc(schema.storeRow.id)],
46
+ });
47
+ return rows.map(toRow);
48
+ }
49
+ /**
50
+ * Saves rows into a list. With a key column, a row whose key is already there is updated — only
51
+ * the columns given change, so a step can add "how to get the invoice" without wiping the rest.
52
+ * A row with a bad value is refused as a whole (`TableColumnValueError` names the column).
53
+ */
54
+ export async function saveRows(scope, def, input) {
55
+ if (def.shared) {
56
+ return saveSharedRows(scope.wizardId, def, input);
57
+ }
58
+ let added = 0;
59
+ let updated = 0;
60
+ const existing = await db.query.storeRow.findMany({ where: inList(scope, def.id) });
61
+ const byKey = new Map(existing.filter((r) => r.key).map((r) => [r.key, r]));
62
+ let count = existing.length;
63
+ const at = firstRowAt(existing);
64
+ for (const raw of input) {
65
+ const key = rowKey(def, raw);
66
+ const hit = key ? byKey.get(key) : undefined;
67
+ if (hit) {
68
+ const cells = patchCells(def, hit.cells, raw);
69
+ await db
70
+ .update(schema.storeRow)
71
+ .set({ cells, updatedAt: new Date() })
72
+ .where(eq(schema.storeRow.id, hit.id));
73
+ hit.cells = cells;
74
+ updated++;
75
+ continue;
76
+ }
77
+ const cells = newCells(def, raw);
78
+ if (Object.values(cells).every((v) => v === null)) {
79
+ continue;
80
+ }
81
+ if (count >= STORE_LIMITS.rowsPerList) {
82
+ throw new StoreError(`List "${def.id}" is full (${STORE_LIMITS.rowsPerList} rows).`);
83
+ }
84
+ const row = {
85
+ id: nanoid(14),
86
+ ...scope,
87
+ list: def.id,
88
+ key,
89
+ cells,
90
+ createdAt: new Date(at + added),
91
+ };
92
+ await db.insert(schema.storeRow).values(row);
93
+ if (key) {
94
+ byKey.set(key, { ...row, updatedAt: new Date() });
95
+ }
96
+ count++;
97
+ added++;
98
+ }
99
+ await touchStore(scope);
100
+ return { added, updated };
101
+ }
102
+ /** Changes cells of one row (the person edited it). A shared list is never the person's. */
103
+ export async function updateRow(scope, def, rowId, patch) {
104
+ if (def.shared) {
105
+ return false;
106
+ }
107
+ const row = await db.query.storeRow.findFirst({
108
+ where: and(inList(scope, def.id), eq(schema.storeRow.id, rowId)),
109
+ });
110
+ if (!row) {
111
+ return false;
112
+ }
113
+ const cells = patchCells(def, row.cells, patch);
114
+ const key = rowKey(def, cells);
115
+ const clash = key && key !== row.key
116
+ ? await db.query.storeRow.findFirst({
117
+ where: and(inList(scope, def.id), eq(schema.storeRow.key, key)),
118
+ })
119
+ : null;
120
+ if (clash) {
121
+ throw new StoreError(note("entryAlreadyExists"));
122
+ }
123
+ await db
124
+ .update(schema.storeRow)
125
+ .set({ cells, key, updatedAt: new Date() })
126
+ .where(eq(schema.storeRow.id, rowId));
127
+ return true;
128
+ }
129
+ /** Deletes rows by id, or by the value of the key column. */
130
+ export async function deleteRows(scope, def, idsOrKeys) {
131
+ if (def.shared) {
132
+ return deleteSharedRows(scope.wizardId, def, idsOrKeys);
133
+ }
134
+ if (!idsOrKeys.length) {
135
+ return 0;
136
+ }
137
+ const keys = idsOrKeys.map((k) => k.trim().toLowerCase());
138
+ const byId = await db
139
+ .delete(schema.storeRow)
140
+ .where(and(inList(scope, def.id), inArray(schema.storeRow.id, idsOrKeys)))
141
+ .returning({ id: schema.storeRow.id });
142
+ const byKey = await db
143
+ .delete(schema.storeRow)
144
+ .where(and(inList(scope, def.id), inArray(schema.storeRow.key, keys)))
145
+ .returning({ id: schema.storeRow.id });
146
+ return byId.length + byKey.length;
147
+ }
148
+ // --- files -------------------------------------------------------------------
149
+ const ownFiles = (scope) => and(eq(schema.storeFile.wizardId, scope.wizardId), eq(schema.storeFile.holder, scope.holder));
150
+ function toFile(r) {
151
+ return {
152
+ path: r.path,
153
+ mime: r.mime,
154
+ size: r.size,
155
+ source: r.source,
156
+ updatedAt: r.updatedAt.toISOString(),
157
+ };
158
+ }
159
+ /** Any name a model or a mail server comes up with, as a path the store accepts. */
160
+ export function storePath(raw) {
161
+ const path = raw
162
+ .normalize("NFKD")
163
+ .replace(/[̀-ͯ]/g, "")
164
+ .replace(/ß/g, "ss")
165
+ .split("/")
166
+ .map((part) => part
167
+ .trim()
168
+ .replace(/[^A-Za-z0-9_.-]+/g, "_")
169
+ .replace(/^[._]+/, "")
170
+ .slice(0, 80))
171
+ .filter(Boolean)
172
+ .join("/");
173
+ const clean = cleanPath(path);
174
+ if (!clean) {
175
+ throw new StoreError(`"${raw}" is not a usable file name.`);
176
+ }
177
+ return clean;
178
+ }
179
+ export async function storeFiles(scope, folder) {
180
+ const rows = await db.query.storeFile.findMany({
181
+ where: folder
182
+ ? and(ownFiles(scope), like(schema.storeFile.path, `${folder.replace(/\/+$/, "")}/%`))
183
+ : ownFiles(scope),
184
+ orderBy: [asc(schema.storeFile.path)],
185
+ });
186
+ return rows.map(toFile);
187
+ }
188
+ /** Whether the wizard keeps any file for the holder: one row, not the whole list. */
189
+ export async function hasStoreFiles(scope) {
190
+ const row = await db.query.storeFile.findFirst({
191
+ where: ownFiles(scope),
192
+ columns: { path: true },
193
+ });
194
+ return row !== undefined;
195
+ }
196
+ export async function readStoreFile(scope, rawPath) {
197
+ const path = cleanPath(rawPath);
198
+ const row = path
199
+ ? await db.query.storeFile.findFirst({
200
+ where: and(ownFiles(scope), eq(schema.storeFile.path, path)),
201
+ })
202
+ : undefined;
203
+ return row ? { file: toFile(row), data: await getBlob(row.hash) } : null;
204
+ }
205
+ export async function writeStoreFile(scope, rawPath, content, meta = {}) {
206
+ const path = storePath(rawPath);
207
+ const data = typeof content === "string" ? Buffer.from(content, "utf8") : Buffer.from(content);
208
+ if (data.byteLength > STORE_LIMITS.fileBytes) {
209
+ throw new StoreError(`${path} is larger than 15 MB.`);
210
+ }
211
+ const [usage] = await db
212
+ .select({
213
+ files: sql `count(*)`,
214
+ bytes: sql `coalesce(sum(${schema.storeFile.size}), 0)`,
215
+ })
216
+ .from(schema.storeFile)
217
+ .where(ownFiles(scope));
218
+ if (usage.files >= STORE_LIMITS.files ||
219
+ usage.bytes + data.byteLength > STORE_LIMITS.totalBytes) {
220
+ throw new StoreError("The wizard's storage is full.");
221
+ }
222
+ const hash = await putBlob(data);
223
+ const mime = meta.mime && meta.mime !== "application/octet-stream" ? meta.mime : mimeForPath(path);
224
+ const updatedAt = new Date();
225
+ await db
226
+ .insert(schema.storeFile)
227
+ .values({ ...scope, path, hash, mime, size: data.byteLength, source: meta.source ?? null })
228
+ .onConflictDoUpdate({
229
+ target: [schema.storeFile.wizardId, schema.storeFile.holder, schema.storeFile.path],
230
+ set: { hash, mime, size: data.byteLength, source: meta.source ?? null, updatedAt },
231
+ });
232
+ await touchStore(scope);
233
+ return {
234
+ path,
235
+ mime,
236
+ size: data.byteLength,
237
+ source: meta.source ?? null,
238
+ updatedAt: updatedAt.toISOString(),
239
+ };
240
+ }
241
+ // --- secrets -----------------------------------------------------------------
242
+ const ownSecrets = (scope) => and(eq(schema.storeSecret.wizardId, scope.wizardId), eq(schema.storeSecret.holder, scope.holder));
243
+ export async function getSecret(scope, slot) {
244
+ const row = await db.query.storeSecret.findFirst({
245
+ where: and(ownSecrets(scope), eq(schema.storeSecret.slot, slot)),
246
+ });
247
+ const data = row ? unseal(row.data) : null;
248
+ return row && data
249
+ ? { provider: row.provider, label: row.label, data, createdAt: row.createdAt }
250
+ : null;
251
+ }
252
+ export async function putSecret(scope, slot, provider, label, data) {
253
+ const sealed = seal(data);
254
+ await db
255
+ .insert(schema.storeSecret)
256
+ .values({ id: nanoid(14), ...scope, slot, provider, label, data: sealed })
257
+ .onConflictDoUpdate({
258
+ target: [schema.storeSecret.wizardId, schema.storeSecret.holder, schema.storeSecret.slot],
259
+ set: { provider, label, data: sealed, updatedAt: new Date() },
260
+ });
261
+ await touchStore(scope);
262
+ }
263
+ export async function deleteSecret(scope, slot) {
264
+ await db
265
+ .delete(schema.storeSecret)
266
+ .where(and(ownSecrets(scope), eq(schema.storeSecret.slot, slot)));
267
+ }
268
+ export async function listSecrets(scope) {
269
+ const rows = await db.query.storeSecret.findMany({ where: ownSecrets(scope) });
270
+ return rows.map((r) => ({
271
+ slot: r.slot,
272
+ provider: r.provider,
273
+ label: r.label,
274
+ createdAt: r.createdAt.toISOString(),
275
+ }));
276
+ }
277
+ // --- everything --------------------------------------------------------------
278
+ /** Deletes everything the wizard keeps for this person. Blobs stay: other files may share them. */
279
+ export async function clearStore(scope) {
280
+ await db
281
+ .delete(schema.storeRow)
282
+ .where(and(eq(schema.storeRow.wizardId, scope.wizardId), eq(schema.storeRow.holder, scope.holder)));
283
+ await db.delete(schema.storeFile).where(ownFiles(scope));
284
+ await db.delete(schema.storeSecret).where(ownSecrets(scope));
285
+ await db
286
+ .delete(schema.storeHolder)
287
+ .where(and(eq(schema.storeHolder.wizardId, scope.wizardId), eq(schema.storeHolder.holder, scope.holder)));
288
+ }
289
+ /** Stores nobody used for STORE_TTL_DAYS are deleted — a visitor's cookie is long gone by then. */
290
+ export async function purgeUnusedStores() {
291
+ const cutoff = new Date(Date.now() - env.limits.storeTtlDays * 24 * 60 * 60 * 1000);
292
+ const stale = await db.query.storeHolder.findMany({
293
+ where: lt(schema.storeHolder.usedAt, cutoff),
294
+ });
295
+ for (const scope of stale) {
296
+ await clearStore(scope);
297
+ }
298
+ return stale.length;
299
+ }
@@ -0,0 +1,123 @@
1
+ import { firstRowAt, newCells, patchCells, rowKey, STORE_LIMITS, } from "@engenty-wizards/shared/store";
2
+ import { and, asc, eq, inArray } from "drizzle-orm";
3
+ import { nanoid } from "nanoid";
4
+ import { db, schema } from "../db/client.js";
5
+ import { StoreError } from "./errors.js";
6
+ const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
7
+ /** The table a shared list is kept in: made the first time, following the list as it changes. */
8
+ export async function sharedTable(wizardId, def) {
9
+ const where = and(eq(schema.spaceTable.wizardId, wizardId), eq(schema.spaceTable.list, def.id));
10
+ const found = await db.query.spaceTable.findFirst({ where });
11
+ const shape = { title: def.title, columns: def.columns, keyColumn: def.key ?? null };
12
+ if (found) {
13
+ if (found.title !== shape.title ||
14
+ found.keyColumn !== shape.keyColumn ||
15
+ !same(found.columns, shape.columns)) {
16
+ await db
17
+ .update(schema.spaceTable)
18
+ .set({ ...shape, updatedAt: new Date() })
19
+ .where(eq(schema.spaceTable.id, found.id));
20
+ return { ...found, ...shape };
21
+ }
22
+ return found;
23
+ }
24
+ const wizard = await db.query.wizard.findFirst({
25
+ where: eq(schema.wizard.id, wizardId),
26
+ columns: { projectId: true, tenantId: true },
27
+ });
28
+ if (!wizard) {
29
+ throw new StoreError("The wizard is gone.");
30
+ }
31
+ // Two runs that start at once make it once.
32
+ await db
33
+ .insert(schema.spaceTable)
34
+ .values({ id: nanoid(12), ...wizard, wizardId, list: def.id, ...shape })
35
+ .onConflictDoNothing();
36
+ const made = await db.query.spaceTable.findFirst({ where });
37
+ if (!made) {
38
+ throw new StoreError(`List "${def.id}" could not be made.`);
39
+ }
40
+ return made;
41
+ }
42
+ async function touch(tableId) {
43
+ await db
44
+ .update(schema.spaceTable)
45
+ .set({ updatedAt: new Date() })
46
+ .where(eq(schema.spaceTable.id, tableId));
47
+ }
48
+ function toRow(r) {
49
+ return { id: r.id, cells: r.cells, updatedAt: r.updatedAt.toISOString() };
50
+ }
51
+ export async function sharedRows(wizardId, def) {
52
+ const table = await sharedTable(wizardId, def);
53
+ const rows = await db.query.spaceTableRow.findMany({
54
+ where: eq(schema.spaceTableRow.tableId, table.id),
55
+ orderBy: [asc(schema.spaceTableRow.createdAt), asc(schema.spaceTableRow.id)],
56
+ });
57
+ return rows.map(toRow);
58
+ }
59
+ /** As `saveRows` does for one person's list: a row with a known key updates that row. */
60
+ export async function saveSharedRows(wizardId, def, input) {
61
+ const table = await sharedTable(wizardId, def);
62
+ const existing = await db.query.spaceTableRow.findMany({
63
+ where: eq(schema.spaceTableRow.tableId, table.id),
64
+ });
65
+ const byKey = new Map(existing.filter((r) => r.key).map((r) => [r.key, r]));
66
+ let count = existing.length;
67
+ const at = firstRowAt(existing);
68
+ let added = 0;
69
+ let updated = 0;
70
+ for (const raw of input) {
71
+ const key = rowKey(def, raw);
72
+ const hit = key ? byKey.get(key) : undefined;
73
+ if (hit) {
74
+ const cells = patchCells(def, hit.cells, raw);
75
+ await db
76
+ .update(schema.spaceTableRow)
77
+ .set({ cells, updatedAt: new Date() })
78
+ .where(eq(schema.spaceTableRow.id, hit.id));
79
+ hit.cells = cells;
80
+ updated++;
81
+ continue;
82
+ }
83
+ const cells = newCells(def, raw);
84
+ if (Object.values(cells).every((v) => v === null)) {
85
+ continue;
86
+ }
87
+ if (count >= STORE_LIMITS.rowsPerList) {
88
+ throw new StoreError(`List "${def.id}" is full (${STORE_LIMITS.rowsPerList} rows).`);
89
+ }
90
+ const row = { id: nanoid(14), tableId: table.id, key, cells, createdAt: new Date(at + added) };
91
+ await db.insert(schema.spaceTableRow).values(row);
92
+ if (key) {
93
+ byKey.set(key, { ...row, updatedAt: new Date() });
94
+ }
95
+ count++;
96
+ added++;
97
+ }
98
+ if (added || updated) {
99
+ await touch(table.id);
100
+ }
101
+ return { added, updated };
102
+ }
103
+ /** Deletes rows by id, or by the value of the key column. */
104
+ export async function deleteSharedRows(wizardId, def, idsOrKeys) {
105
+ if (!idsOrKeys.length) {
106
+ return 0;
107
+ }
108
+ const table = await sharedTable(wizardId, def);
109
+ const keys = idsOrKeys.map((k) => k.trim().toLowerCase());
110
+ const inTable = eq(schema.spaceTableRow.tableId, table.id);
111
+ const byId = await db
112
+ .delete(schema.spaceTableRow)
113
+ .where(and(inTable, inArray(schema.spaceTableRow.id, idsOrKeys)))
114
+ .returning({ id: schema.spaceTableRow.id });
115
+ const byKey = await db
116
+ .delete(schema.spaceTableRow)
117
+ .where(and(inTable, inArray(schema.spaceTableRow.key, keys)))
118
+ .returning({ id: schema.spaceTableRow.id });
119
+ if (byId.length + byKey.length) {
120
+ await touch(table.id);
121
+ }
122
+ return byId.length + byKey.length;
123
+ }
@@ -0,0 +1,238 @@
1
+ import { and, count, eq, gte, inArray, isNull, lt, or, sql } from "drizzle-orm";
2
+ import { control, controlDb } from "../db/client.js";
3
+ import { env } from "../env.js";
4
+ import { currentTenant } from "./tenant.js";
5
+ /** Remembers which tenant a public token belongs to. */
6
+ export async function putLink(token, kind, ref) {
7
+ await controlDb
8
+ .insert(control.link)
9
+ .values({ token, tenantId: currentTenant(), kind, ref })
10
+ .onConflictDoUpdate({
11
+ target: control.link.token,
12
+ set: { tenantId: currentTenant(), kind, ref },
13
+ });
14
+ }
15
+ /** A token's link goes, and with a wizard's share token its ID. */
16
+ export async function dropLinks(tokens) {
17
+ if (tokens.length) {
18
+ await controlDb.delete(control.link).where(inArray(control.link.token, tokens));
19
+ await controlDb
20
+ .delete(control.link)
21
+ .where(and(eq(control.link.kind, "code"), inArray(control.link.ref, tokens)));
22
+ }
23
+ }
24
+ // --- wizard IDs ----------------------------------------------------------------------
25
+ // The share token is 14 characters of both cases, `_` and `-`: nobody types that on a phone.
26
+ // The ID is 8 capitals and digits without look-alikes (no 0 O 1 I), made the first time someone
27
+ // asks for it and gone when the share token rotates.
28
+ const CODE_ALPHABET = "23456789ABCDEFGHJKLMNPQRSTUVWXYZ";
29
+ const CODE_LENGTH = 8;
30
+ function newCode() {
31
+ const bytes = crypto.getRandomValues(new Uint8Array(CODE_LENGTH));
32
+ return Array.from(bytes, (b) => CODE_ALPHABET[b % CODE_ALPHABET.length]).join("");
33
+ }
34
+ /** What a person typed, as stored: capitals, no spaces or dashes. */
35
+ export const normalizeCode = (input) => input.toUpperCase().replace(/[\s-]/g, "");
36
+ /** The ID of a wizard's share token; made on the first ask. */
37
+ export async function codeOf(shareToken) {
38
+ const row = await controlDb.query.link.findFirst({
39
+ where: and(eq(control.link.kind, "code"), eq(control.link.ref, shareToken)),
40
+ });
41
+ if (row) {
42
+ return row.token;
43
+ }
44
+ for (;;) {
45
+ const code = newCode();
46
+ const made = await controlDb
47
+ .insert(control.link)
48
+ .values({ token: code, tenantId: currentTenant(), kind: "code", ref: shareToken })
49
+ .onConflictDoNothing()
50
+ .returning({ token: control.link.token });
51
+ if (made.length) {
52
+ return code;
53
+ }
54
+ }
55
+ }
56
+ /** The share token behind an ID, and its tenant. */
57
+ export async function tokenOfCode(input) {
58
+ const code = normalizeCode(input);
59
+ if (code.length !== CODE_LENGTH) {
60
+ return null;
61
+ }
62
+ const row = await controlDb.query.link.findFirst({
63
+ where: and(eq(control.link.kind, "code"), eq(control.link.token, code)),
64
+ });
65
+ return row ? { token: row.ref, tenantId: row.tenantId } : null;
66
+ }
67
+ export async function tenantOfLink(token, kind) {
68
+ const row = await controlDb.query.link.findFirst({
69
+ where: and(eq(control.link.token, token), eq(control.link.kind, kind)),
70
+ });
71
+ return row?.tenantId ?? null;
72
+ }
73
+ // --- runs ----------------------------------------------------------------------
74
+ const runTenants = new Map();
75
+ export async function indexRun(input) {
76
+ const tenantId = currentTenant();
77
+ runTenants.set(input.runId, tenantId);
78
+ await controlDb.insert(control.runIndex).values({
79
+ runId: input.runId,
80
+ tenantId,
81
+ visitorId: input.visitorId ?? null,
82
+ ipHash: input.ipHash ?? null,
83
+ });
84
+ }
85
+ export async function tenantOfRun(runId) {
86
+ const known = runTenants.get(runId);
87
+ if (known) {
88
+ return known;
89
+ }
90
+ const row = await controlDb.query.runIndex.findFirst({
91
+ where: eq(control.runIndex.runId, runId),
92
+ });
93
+ if (row) {
94
+ runTenants.set(runId, row.tenantId);
95
+ if (runTenants.size > 5000) {
96
+ runTenants.delete(runTenants.keys().next().value);
97
+ }
98
+ }
99
+ return row?.tenantId ?? null;
100
+ }
101
+ export async function dropRuns(runIds) {
102
+ for (const id of runIds) {
103
+ runTenants.delete(id);
104
+ }
105
+ for (let i = 0; i < runIds.length; i += 200) {
106
+ await controlDb
107
+ .delete(control.runIndex)
108
+ .where(inArray(control.runIndex.runId, runIds.slice(i, i + 200)));
109
+ }
110
+ }
111
+ // --- the queue: which runner works on which run ---------------------------------
112
+ // `active` = the run wants a runner. `owner` = the one driving it, `seen` = its last heartbeat.
113
+ /** An owner silent this long has lost the run: another runner takes it. */
114
+ export const RUNNER_STALE_MS = 30_000;
115
+ /** Puts a run in line for a runner (again): active, owned by nobody. */
116
+ export async function wakeRun(runId) {
117
+ await controlDb
118
+ .update(control.runIndex)
119
+ .set({ active: true, owner: null, seen: null })
120
+ .where(eq(control.runIndex.runId, runId));
121
+ }
122
+ /** The run is done with runners for now: a page, a review or the end. */
123
+ export async function restRun(runId) {
124
+ await controlDb
125
+ .update(control.runIndex)
126
+ .set({ active: false, owner: null, seen: null })
127
+ .where(eq(control.runIndex.runId, runId));
128
+ }
129
+ /**
130
+ * Takes a run for this runner, in one statement: only while nobody (alive) holds it and the
131
+ * tenant's owned runs stay under its limit. False: someone else has it, or the tenant is at its
132
+ * limit — the run stays in line.
133
+ */
134
+ export async function claimRun(runId, tenantId, limit) {
135
+ const now = Date.now();
136
+ const stale = now - RUNNER_STALE_MS;
137
+ const result = await controlDb.run(sql `
138
+ UPDATE run_index SET owner = ${env.runner.id}, seen = ${now}
139
+ WHERE run_id = ${runId} AND active = 1
140
+ AND (owner IS NULL OR seen IS NULL OR seen < ${stale})
141
+ AND (SELECT count(*) FROM run_index
142
+ WHERE tenant_id = ${tenantId} AND owner IS NOT NULL AND seen >= ${stale}
143
+ AND run_id != ${runId}) < ${limit}`);
144
+ return result.rowsAffected > 0;
145
+ }
146
+ /** This runner still works on these runs. */
147
+ export async function heartbeatRuns(runIds) {
148
+ if (runIds.length) {
149
+ await controlDb
150
+ .update(control.runIndex)
151
+ .set({ seen: new Date() })
152
+ .where(and(eq(control.runIndex.owner, env.runner.id), inArray(control.runIndex.runId, runIds)));
153
+ }
154
+ }
155
+ /** A runner that stops hands its runs back to the line; another takes them at once. */
156
+ export async function disownRuns() {
157
+ await controlDb
158
+ .update(control.runIndex)
159
+ .set({ owner: null, seen: null })
160
+ .where(eq(control.runIndex.owner, env.runner.id));
161
+ }
162
+ /** The runs in line: unowned, or whose owner fell silent. */
163
+ export async function waitingRuns() {
164
+ const stale = new Date(Date.now() - RUNNER_STALE_MS);
165
+ return controlDb
166
+ .select({ runId: control.runIndex.runId, tenantId: control.runIndex.tenantId })
167
+ .from(control.runIndex)
168
+ .where(and(eq(control.runIndex.active, true), or(isNull(control.runIndex.owner), isNull(control.runIndex.seen), lt(control.runIndex.seen, stale))))
169
+ .orderBy(control.runIndex.createdAt);
170
+ }
171
+ /** Where the runner holding a run is reached; null when none holds it or it named no address. */
172
+ export async function ownerUrl(runId) {
173
+ const row = await controlDb.query.runIndex.findFirst({
174
+ where: eq(control.runIndex.runId, runId),
175
+ });
176
+ if (!row?.owner || !row.seen || row.seen.getTime() < Date.now() - RUNNER_STALE_MS) {
177
+ return null;
178
+ }
179
+ const who = await controlDb.query.runner.findFirst({ where: eq(control.runner.id, row.owner) });
180
+ return who?.url || null;
181
+ }
182
+ /** This process reports as a runner: its address, and that it is alive. */
183
+ export async function registerRunner() {
184
+ const now = new Date();
185
+ await controlDb
186
+ .insert(control.runner)
187
+ .values({ id: env.runner.id, url: env.runner.url || null, seen: now })
188
+ .onConflictDoUpdate({
189
+ target: control.runner.id,
190
+ set: { url: env.runner.url || null, seen: now },
191
+ });
192
+ // Runners gone for a day are forgotten.
193
+ await controlDb
194
+ .delete(control.runner)
195
+ .where(lt(control.runner.seen, new Date(now.getTime() - 86_400_000)));
196
+ }
197
+ export async function forgetRunner() {
198
+ await controlDb.delete(control.runner).where(eq(control.runner.id, env.runner.id));
199
+ }
200
+ /** A visitor's runs are counted across every wizard of every tenant. */
201
+ export async function visitorOverLimit(visitorId, ipHash) {
202
+ const since = new Date(Date.now() - 3600_000);
203
+ const who = ipHash
204
+ ? or(eq(control.runIndex.visitorId, visitorId), eq(control.runIndex.ipHash, ipHash))
205
+ : eq(control.runIndex.visitorId, visitorId);
206
+ const [{ n }] = await controlDb
207
+ .select({ n: count() })
208
+ .from(control.runIndex)
209
+ .where(and(who, gte(control.runIndex.createdAt, since)));
210
+ return n >= env.limits.visitorRunsPerHour;
211
+ }
212
+ // --- tenants ---------------------------------------------------------------------
213
+ export async function tenantStatus(id) {
214
+ const row = await controlDb.query.tenant.findFirst({ where: eq(control.tenant.id, id) });
215
+ return row?.status ?? null;
216
+ }
217
+ export async function setTenantStatus(id, status) {
218
+ await controlDb.update(control.tenant).set({ status }).where(eq(control.tenant.id, id));
219
+ }
220
+ /** Everything the control database knows about a tenant goes with it. */
221
+ export async function forgetTenantRows(id) {
222
+ await controlDb.delete(control.link).where(eq(control.link.tenantId, id));
223
+ await controlDb.delete(control.runIndex).where(eq(control.runIndex.tenantId, id));
224
+ await controlDb.delete(control.session).where(eq(control.session.tenantId, id));
225
+ await controlDb.delete(control.tenant).where(eq(control.tenant.id, id));
226
+ for (const [runId, tenantId] of runTenants) {
227
+ if (tenantId === id) {
228
+ runTenants.delete(runId);
229
+ }
230
+ }
231
+ }
232
+ /** The line's view of one run: which tenant it belongs to, whether it is awake. */
233
+ export async function runIndexOf(runId) {
234
+ const row = await controlDb.query.runIndex.findFirst({
235
+ where: eq(control.runIndex.runId, runId),
236
+ });
237
+ return row ? { tenantId: row.tenantId, active: row.active } : null;
238
+ }
@@ -0,0 +1,26 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ /** The tenant a request, a run or a background job works for. Set once per entry, never from a request body. */
3
+ const current = new AsyncLocalStorage();
4
+ export class NoTenantError extends Error {
5
+ constructor() {
6
+ super("No tenant in context: tenant tables are reached only inside inTenant().");
7
+ }
8
+ }
9
+ export function inTenant(tenantId, fn) {
10
+ if (!/^[A-Za-z0-9_-]{1,64}$/.test(tenantId)) {
11
+ throw new Error("bad tenant id");
12
+ }
13
+ return current.run({ tenantId }, fn);
14
+ }
15
+ export function currentTenant() {
16
+ const store = current.getStore();
17
+ if (!store) {
18
+ throw new NoTenantError();
19
+ }
20
+ return store.tenantId;
21
+ }
22
+ export function currentTenantOrNull() {
23
+ return current.getStore()?.tenantId ?? null;
24
+ }
25
+ /** The one tenant of a runtime that runs alone (desktop app, own machine). */
26
+ export const LOCAL_TENANT = "local";