@hexclave/shared 1.0.61 → 1.0.62

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 (140) hide show
  1. package/dist/{admin-interface-ChuyYSXj.d.ts → admin-interface-D63SFHX_.d.ts} +94 -12
  2. package/dist/admin-interface-D63SFHX_.d.ts.map +1 -0
  3. package/dist/{admin-metrics-1yiWGq9z.d.ts → admin-metrics-bsOkw4YQ.d.ts} +15 -15
  4. package/dist/{admin-metrics-1yiWGq9z.d.ts.map → admin-metrics-bsOkw4YQ.d.ts.map} +1 -1
  5. package/dist/ai/unified-prompts/skill-site-prompt-parts/ai-setup-prompt.js +1 -1
  6. package/dist/ai/unified-prompts/skill-site-prompt-parts/deployments-skill.d.ts +5 -0
  7. package/dist/ai/unified-prompts/skill-site-prompt-parts/deployments-skill.d.ts.map +1 -0
  8. package/dist/ai/unified-prompts/skill-site-prompt-parts/deployments-skill.js +128 -0
  9. package/dist/ai/unified-prompts/skill-site-prompt-parts/deployments-skill.js.map +1 -0
  10. package/dist/ai/unified-prompts/skill-site-prompt.d.ts +3 -1
  11. package/dist/ai/unified-prompts/skill-site-prompt.d.ts.map +1 -1
  12. package/dist/ai/unified-prompts/skill-site-prompt.js +11 -0
  13. package/dist/ai/unified-prompts/skill-site-prompt.js.map +1 -1
  14. package/dist/apps/apps-config.d.ts +6 -0
  15. package/dist/apps/apps-config.d.ts.map +1 -1
  16. package/dist/apps/apps-config.js +6 -0
  17. package/dist/apps/apps-config.js.map +1 -1
  18. package/dist/{client-interface-C4H4gn_G.d.ts → client-interface-DvkciaL0.d.ts} +10 -10
  19. package/dist/{client-interface-C4H4gn_G.d.ts.map → client-interface-DvkciaL0.d.ts.map} +1 -1
  20. package/dist/config/schema-fuzzer.test.js +38 -1
  21. package/dist/config/schema-fuzzer.test.js.map +1 -1
  22. package/dist/config/schema.d.ts +2 -2
  23. package/dist/config/schema.js +45 -0
  24. package/dist/config/schema.js.map +1 -1
  25. package/dist/config-authoring.d.ts +1 -1
  26. package/dist/{current-user-BCkMYzHx.d.ts → current-user-BBoN3hsn.d.ts} +17 -17
  27. package/dist/{current-user-BCkMYzHx.d.ts.map → current-user-BBoN3hsn.d.ts.map} +1 -1
  28. package/dist/{email-outbox-BRZiK0ZG.d.ts → email-outbox-CqzwInBL.d.ts} +313 -313
  29. package/dist/{email-outbox-BRZiK0ZG.d.ts.map → email-outbox-CqzwInBL.d.ts.map} +1 -1
  30. package/dist/esm/ai/unified-prompts/skill-site-prompt-parts/ai-setup-prompt.js +1 -1
  31. package/dist/esm/ai/unified-prompts/skill-site-prompt-parts/deployments-skill.d.ts +5 -0
  32. package/dist/esm/ai/unified-prompts/skill-site-prompt-parts/deployments-skill.d.ts.map +1 -0
  33. package/dist/esm/ai/unified-prompts/skill-site-prompt-parts/deployments-skill.js +128 -0
  34. package/dist/esm/ai/unified-prompts/skill-site-prompt-parts/deployments-skill.js.map +1 -0
  35. package/dist/esm/ai/unified-prompts/skill-site-prompt.d.ts +3 -1
  36. package/dist/esm/ai/unified-prompts/skill-site-prompt.d.ts.map +1 -1
  37. package/dist/esm/ai/unified-prompts/skill-site-prompt.js +10 -1
  38. package/dist/esm/ai/unified-prompts/skill-site-prompt.js.map +1 -1
  39. package/dist/esm/apps/apps-config.d.ts +6 -0
  40. package/dist/esm/apps/apps-config.d.ts.map +1 -1
  41. package/dist/esm/apps/apps-config.js +6 -0
  42. package/dist/esm/apps/apps-config.js.map +1 -1
  43. package/dist/esm/config/schema-fuzzer.test.js +38 -1
  44. package/dist/esm/config/schema-fuzzer.test.js.map +1 -1
  45. package/dist/esm/config/schema.d.ts +249 -119
  46. package/dist/esm/config/schema.d.ts.map +1 -1
  47. package/dist/esm/config/schema.js +42 -1
  48. package/dist/esm/config/schema.js.map +1 -1
  49. package/dist/esm/interface/admin-interface.d.ts +83 -1
  50. package/dist/esm/interface/admin-interface.d.ts.map +1 -1
  51. package/dist/esm/interface/admin-interface.js +48 -0
  52. package/dist/esm/interface/admin-interface.js.map +1 -1
  53. package/dist/esm/interface/admin-metrics.d.ts +14 -14
  54. package/dist/esm/interface/conversations.d.ts +37 -37
  55. package/dist/esm/interface/crud/current-user.d.ts +16 -16
  56. package/dist/esm/interface/crud/email-outbox.d.ts +312 -312
  57. package/dist/esm/interface/crud/invoices.d.ts +2 -2
  58. package/dist/esm/interface/crud/products.d.ts +23 -23
  59. package/dist/esm/interface/crud/project-api-keys.d.ts +4 -4
  60. package/dist/esm/interface/crud/projects.d.ts +59 -59
  61. package/dist/esm/interface/crud/team-member-profiles.d.ts +32 -32
  62. package/dist/esm/interface/crud/transactions.d.ts +29 -29
  63. package/dist/esm/interface/crud/transactions.d.ts.map +1 -1
  64. package/dist/esm/interface/crud/users.d.ts +22 -22
  65. package/dist/esm/interface/plan-usage.d.ts +1 -1
  66. package/dist/esm/interface/webhooks.d.ts +4 -4
  67. package/dist/esm/known-errors.d.ts +4 -4
  68. package/dist/esm/schema-fields.d.ts +12 -12
  69. package/dist/esm/sessions.d.ts +9 -9
  70. package/dist/esm/utils/tar.d.ts +26 -0
  71. package/dist/esm/utils/tar.d.ts.map +1 -0
  72. package/dist/esm/utils/tar.js +136 -0
  73. package/dist/esm/utils/tar.js.map +1 -0
  74. package/dist/helpers/password.d.ts +1 -1
  75. package/dist/helpers/production-mode.d.ts +1 -1
  76. package/dist/index.d.ts +4 -4
  77. package/dist/interface/admin-interface.d.ts +3 -3
  78. package/dist/interface/admin-interface.js +48 -0
  79. package/dist/interface/admin-interface.js.map +1 -1
  80. package/dist/interface/admin-metrics.d.ts +1 -1
  81. package/dist/interface/client-interface.d.ts +1 -1
  82. package/dist/interface/conversations.d.ts +37 -37
  83. package/dist/interface/crud/current-user.d.ts +1 -1
  84. package/dist/interface/crud/email-outbox.d.ts +1 -1
  85. package/dist/interface/crud/invoices.d.ts +1 -1
  86. package/dist/interface/crud/products.d.ts +1 -1
  87. package/dist/interface/crud/project-api-keys.d.ts +1 -1
  88. package/dist/interface/crud/projects.d.ts +1 -1
  89. package/dist/interface/crud/team-member-profiles.d.ts +1 -1
  90. package/dist/interface/crud/transactions.d.ts +1 -1
  91. package/dist/interface/crud/users.d.ts +1 -1
  92. package/dist/interface/plan-usage.d.ts +1 -1
  93. package/dist/interface/server-interface.d.ts +1 -1
  94. package/dist/interface/webhooks.d.ts +1 -1
  95. package/dist/{invoices-BGLLnTlR.d.ts → invoices-CHo9YKbI.d.ts} +3 -3
  96. package/dist/{invoices-BGLLnTlR.d.ts.map → invoices-CHo9YKbI.d.ts.map} +1 -1
  97. package/dist/{known-errors-Bhd0JAgh.d.ts → known-errors-Bm4a61wL.d.ts} +5 -5
  98. package/dist/{known-errors-Bhd0JAgh.d.ts.map → known-errors-Bm4a61wL.d.ts.map} +1 -1
  99. package/dist/known-errors.d.ts +1 -1
  100. package/dist/{plan-usage-COq9I00z.d.ts → plan-usage-BaDxcLZ1.d.ts} +2 -2
  101. package/dist/{plan-usage-COq9I00z.d.ts.map → plan-usage-BaDxcLZ1.d.ts.map} +1 -1
  102. package/dist/{products-BkPf1X78.d.ts → products-D4o8A2W0.d.ts} +24 -24
  103. package/dist/{products-BkPf1X78.d.ts.map → products-D4o8A2W0.d.ts.map} +1 -1
  104. package/dist/{project-api-keys-CK9SIo0q.d.ts → project-api-keys--7V5X_UW.d.ts} +5 -5
  105. package/dist/{project-api-keys-CK9SIo0q.d.ts.map → project-api-keys--7V5X_UW.d.ts.map} +1 -1
  106. package/dist/{projects-CVqfljhv.d.ts → projects-BX5Y50kJ.d.ts} +60 -60
  107. package/dist/{projects-CVqfljhv.d.ts.map → projects-BX5Y50kJ.d.ts.map} +1 -1
  108. package/dist/{schema-BPMGhu_7.d.ts → schema-dJ_7rUCO.d.ts} +250 -120
  109. package/dist/schema-dJ_7rUCO.d.ts.map +1 -0
  110. package/dist/{schema-fields-C8sIQVTq.d.ts → schema-fields-DZtDQEEO.d.ts} +13 -13
  111. package/dist/{schema-fields-C8sIQVTq.d.ts.map → schema-fields-DZtDQEEO.d.ts.map} +1 -1
  112. package/dist/schema-fields.d.ts +1 -1
  113. package/dist/{server-interface-CRp4VVZh.d.ts → server-interface-Ra_vIHP4.d.ts} +8 -8
  114. package/dist/{server-interface-CRp4VVZh.d.ts.map → server-interface-Ra_vIHP4.d.ts.map} +1 -1
  115. package/dist/{sessions-COluKK6_.d.ts → sessions-CI-0TBQb.d.ts} +11 -11
  116. package/dist/{sessions-COluKK6_.d.ts.map → sessions-CI-0TBQb.d.ts.map} +1 -1
  117. package/dist/sessions.d.ts +1 -1
  118. package/dist/{team-member-profiles-CgYXR-Rx.d.ts → team-member-profiles-BoDEqx-0.d.ts} +33 -33
  119. package/dist/{team-member-profiles-CgYXR-Rx.d.ts.map → team-member-profiles-BoDEqx-0.d.ts.map} +1 -1
  120. package/dist/{transactions-7q13mxhG.d.ts → transactions-CHNUJ42J.d.ts} +30 -30
  121. package/dist/transactions-CHNUJ42J.d.ts.map +1 -0
  122. package/dist/{users-7MKfEyyq.d.ts → users-DHNRwoSz.d.ts} +24 -24
  123. package/dist/{users-7MKfEyyq.d.ts.map → users-DHNRwoSz.d.ts.map} +1 -1
  124. package/dist/utils/tar.d.ts +26 -0
  125. package/dist/utils/tar.d.ts.map +1 -0
  126. package/dist/utils/tar.js +138 -0
  127. package/dist/utils/tar.js.map +1 -0
  128. package/dist/{webhooks-C5gG85Ce.d.ts → webhooks-8Ts1Clcb.d.ts} +5 -5
  129. package/dist/{webhooks-C5gG85Ce.d.ts.map → webhooks-8Ts1Clcb.d.ts.map} +1 -1
  130. package/package.json +1 -1
  131. package/src/ai/unified-prompts/skill-site-prompt-parts/deployments-skill.ts +128 -0
  132. package/src/ai/unified-prompts/skill-site-prompt.ts +15 -0
  133. package/src/apps/apps-config.ts +9 -0
  134. package/src/config/schema-fuzzer.test.ts +37 -0
  135. package/src/config/schema.ts +193 -0
  136. package/src/interface/admin-interface.ts +173 -0
  137. package/src/utils/tar.tsx +261 -0
  138. package/dist/admin-interface-ChuyYSXj.d.ts.map +0 -1
  139. package/dist/schema-BPMGhu_7.d.ts.map +0 -1
  140. package/dist/transactions-7q13mxhG.d.ts.map +0 -1
@@ -0,0 +1,128 @@
1
+ import { deindent } from "../../../utils/strings";
2
+
3
+ // The Deployments-specific addendum served at https://skill.hexclave.com/deployments.
4
+ // It is appended verbatim after the full base skill, so this section assumes the
5
+ // reader already has the general Hexclave skill (CLI auth, config format, SDKs) in
6
+ // context and only teaches what is specific to the Deployments app.
7
+ export const deploymentsSkillSection = deindent`
8
+ # Hexclave Deployments
9
+
10
+ The Deployments app makes deploying a service very easy. You select a type of service to deploy and provide your code, build instructions, run instructions, environment variables, and the code framework. You can set up multiple deployments per Hexclave project — for example, a backend service and a frontend service. Once a service is deployed, it is publicly accessible.
11
+
12
+ For this iteration of the Deployments app, only the \`vercel\` service type is available. It can be used to set up serverless deployments.
13
+
14
+ To use Hexclave Deployments, you must do a few things. First, the Deployments app must be enabled, by adding \`deployments-alpha\` under \`apps.installed\`. Second, you must edit your \`hexclave.config.ts\` file to add the relevant configuration, as specified below. Both keys are \`deployments-alpha\`, not \`deployments\` — the app is in alpha and its id says so. Because of the hyphen, quote it.
15
+
16
+ ## Config
17
+
18
+ \`\`\`ts title="hexclave.config.ts"
19
+ import type { HexclaveConfig } from "@hexclave/js/config"; // replace \`js\` with the correct framework SDK package
20
+
21
+ export const config: HexclaveConfig = {
22
+ apps: {
23
+ installed: {
24
+ authentication: { enabled: true },
25
+ "deployments-alpha": { enabled: true },
26
+ },
27
+ },
28
+ "deployments-alpha": {
29
+ services: {
30
+ web: {
31
+ type: "vercel",
32
+ rootDirectory: "./",
33
+ framework: "nextjs",
34
+ installCommand: "pnpm install",
35
+ buildCommand: "pnpm build",
36
+ outputDirectory: ".next",
37
+ env: {
38
+ "MY_ENV_VAR": { value: "true" },
39
+ "DATABASE_CONNECTION_STRING": { type: "secret", key: "db_connection" },
40
+ "NEXT_PUBLIC_HEXCLAVE_PROJECT_ID": { type: "connection", value: "hexclave.projectId" },
41
+ },
42
+ },
43
+ },
44
+ },
45
+ };
46
+ \`\`\`
47
+
48
+ Here, \`web\` is the name of your service. \`type\` refers to the type of service you want to set up; for now, as mentioned earlier, only \`vercel\` is supported. \`rootDirectory\` is the directory in which the install and build commands are run, and where the code for your service is found. \`installCommand\` is the command run to install your application's dependencies, and \`buildCommand\` is what is run to build the necessary packages and files. \`framework\` is the framework used by your service; from it and \`outputDirectory\`, the run command is inferred.
49
+
50
+ The \`HexclaveConfig\` annotation requires the SDK package it is imported from to be a dependency of the project. If it isn't, either install it or drop the annotation (the config is plain TypeScript — the CLI does not type-check it).
51
+
52
+ Note also that the config file usually sits inside \`rootDirectory\`, which means it is uploaded with your source and compiled by the remote build like any other file. If the deployed app doesn't depend on the Hexclave SDK, add \`hexclave.config.ts\` to \`.vercelignore\`: the CLI reads the config from disk before packaging, so the build itself never needs it, and excluding it keeps an SDK import in the config from breaking a build that is otherwise unrelated to Hexclave.
53
+
54
+ ## Domains
55
+
56
+ Every Vercel service gets a Vercel domain autoprovisioned. If you want to attach a custom domain, there are two ways to do so.
57
+
58
+ **Option 1 — the dashboard.** Visit \`https://app.hexclave.com/projects/<project-id>/deployments\`, click the service of choice, go to the Domains tab, add your domain, and then create the returned DNS records at your DNS provider.
59
+
60
+ **Option 2 — the CLI.** Here is an example of adding \`app.example.com\` to the service \`api\` and receiving the DNS records that then need to be set with your DNS provider:
61
+
62
+ \`\`\`sh title="Terminal"
63
+ npx @hexclave/cli@latest exec --cloud-project-id <project-id> \\
64
+ "const p = await hexclaveServerApp.getProject(); \\
65
+ await p.addDeploymentServiceDomain('api', 'app.example.com'); \\
66
+ return p.getDeploymentServiceDomain('api', 'app.example.com');"
67
+ \`\`\`
68
+
69
+ ## Deploying
70
+
71
+ Before deploying, run the service's \`installCommand\` and \`buildCommand\` locally and confirm they succeed. The remote build runs the same commands, but \`deploy\` exits before the build finishes, so a broken build does not fail the command — it surfaces only when you check the run afterwards.
72
+
73
+ From the directory containing your config file:
74
+
75
+ \`\`\`sh title="Terminal"
76
+ npx @hexclave/cli@latest deploy web --secret db_connection="$DATABASE_URL"
77
+ \`\`\`
78
+
79
+ The CLI packages the service's root directory (respecting \`.gitignore\` and \`.vercelignore\`, and always excluding \`node_modules\` and \`.git\`), uploads it, and starts a remote build. Without a config file, the configuration stored in Hexclave governs the deploy, with the root directory resolved against the current directory. It always deploys to production, never prompts, and exits as soon as the build is queued, printing the run id. Follow the build in the dashboard's Deployments tab.
80
+
81
+ Every secret defined in the service's \`env\` must be passed via \`--secret\` on every deploy — a missing (or misspelled) one fails the deploy before anything is uploaded.
82
+
83
+ Options: \`--config <path>\` (default: auto-discover \`hexclave.config.ts\` in the current directory, dashboard configuration otherwise), \`--cloud-project-id <id>\` (default: the \`HEXCLAVE_PROJECT_ID\` env var), and \`--secret KEY=VALUE\` (repeatable, see above).
84
+
85
+ You can also deploy through GitHub Actions, like so:
86
+
87
+ \`\`\`yaml title=".github/workflows/deploy.yaml"
88
+ - run: npm i -g @hexclave/cli
89
+ - run: hexclave deploy web --secret db_connection="$DATABASE_URL"
90
+ env:
91
+ HEXCLAVE_PROJECT_ID: \${{ secrets.HEXCLAVE_PROJECT_ID }}
92
+ HEXCLAVE_SECRET_SERVER_KEY: \${{ secrets.HEXCLAVE_SECRET_SERVER_KEY }}
93
+ DATABASE_URL: \${{ secrets.DATABASE_URL }}
94
+ \`\`\`
95
+
96
+ ## Checking status and debugging failures
97
+
98
+ Because \`deploy\` returns as soon as the build is queued, a successful exit does not mean the deploy succeeded. Check the service to find out:
99
+
100
+ \`\`\`sh title="Terminal"
101
+ npx @hexclave/cli@latest exec --cloud-project-id <project-id> \\
102
+ "const p = await hexclaveServerApp.getProject(); \\
103
+ const svc = (await p.listDeploymentServices()).find(s => s.id === 'web'); \\
104
+ return { status: svc.status, url: svc.url, run: svc.latest_run };"
105
+ \`\`\`
106
+
107
+ A service's \`status\` is one of \`not_deployed\`, \`queued\`, \`building\`, \`deployed\`, \`failed\`, or \`canceled\`; a run's is \`queued\`, \`building\`, \`ready\`, \`error\`, or \`canceled\`. Poll until the run leaves \`queued\`/\`building\`.
108
+
109
+ A failed run's \`error\` field is only a one-line summary (for example \`Command "npm run build" exited with 1\`). Do not try to guess the cause from it, and do not reproduce the build locally to find out — fetch the actual build output by run id:
110
+
111
+ \`\`\`sh title="Terminal"
112
+ npx @hexclave/cli@latest exec --cloud-project-id <project-id> \\
113
+ "const p = await hexclaveServerApp.getProject(); \\
114
+ return p.getDeploymentRunLogs('<run-id>');"
115
+ \`\`\`
116
+
117
+ \`deploy\` prints the run id it started. If you don't have one, \`listDeploymentRuns('web', { limit: 5 })\` returns the most recent runs, newest first — call it through \`exec\` like the snippets above.
118
+
119
+ ## Deleting a service
120
+
121
+ If you want to delete a deployed service, you can do so through the dashboard, or through the CLI like so:
122
+
123
+ \`\`\`sh title="Terminal"
124
+ npx @hexclave/cli@latest exec --cloud-project-id <project-id> \\
125
+ "const p = await hexclaveServerApp.getProject(); \\
126
+ await p.deleteDeploymentService('api');"
127
+ \`\`\`
128
+ `;
@@ -6,6 +6,7 @@ import { cliHelpPrompt } from "./skill-site-prompt-parts/cli-help";
6
6
  import { configDocsPrompt } from "./skill-site-prompt-parts/config-docs";
7
7
  import { customComponentsInstructionsPrompt } from "./skill-site-prompt-parts/custom-components-instructions";
8
8
  import { dashboardInstructionsPrompt } from "./skill-site-prompt-parts/dashboard-instructions";
9
+ import { deploymentsSkillSection } from "./skill-site-prompt-parts/deployments-skill";
9
10
  import { docsIndexPrompt } from "./skill-site-prompt-parts/docs-index";
10
11
  import { sdkInterfaceSourcePrompt } from "./skill-site-prompt-parts/sdk-interface-source";
11
12
 
@@ -73,3 +74,17 @@ export function buildSkillSitePrompt(docsIndexPromptValue = docsIndexPrompt) {
73
74
  }
74
75
 
75
76
  export const skillSitePrompt = buildSkillSitePrompt();
77
+
78
+ // The /deployments variant of the skill: the entire base skill, followed by the
79
+ // Deployments-specific addendum. Served at https://skill.hexclave.com/deployments
80
+ // so an agent pointed at that URL gets the full Hexclave skill plus the deeper
81
+ // Deployments material in one document.
82
+ export function buildDeploymentsSkillSitePrompt(baseSkillPrompt = skillSitePrompt) {
83
+ return deindent`
84
+ ${baseSkillPrompt}
85
+
86
+ ${deploymentsSkillSection}
87
+ `;
88
+ }
89
+
90
+ export const deploymentsSkillSitePrompt = buildDeploymentsSkillSitePrompt();
@@ -192,6 +192,15 @@ export const ALL_APPS = {
192
192
  tags: ["auth", "developers"],
193
193
  stage: "alpha",
194
194
  },
195
+ // The app id carries the `-alpha` suffix deliberately: it is the key users
196
+ // write in `apps.installed` and under the top-level config, so the stage is
197
+ // visible in every hexclave.config.ts. The display name stays "Deployments".
198
+ "deployments-alpha": {
199
+ displayName: "Deployments",
200
+ subtitle: "Configure and connect the services that run your app",
201
+ tags: ["developers", "operations"],
202
+ stage: "alpha",
203
+ },
195
204
  } as const satisfies Record<string, App>;
196
205
 
197
206
  export function getParentAppId(appId: AppId): AppId | null {
@@ -185,6 +185,43 @@ const branchSchemaFuzzerConfig = [{
185
185
  onboarding: [{
186
186
  requireEmailVerification: [true, false],
187
187
  }],
188
+ "deployments-alpha": [{
189
+ services: [{
190
+ "some-service-id": [{
191
+ type: ["vercel"] as const,
192
+ framework: ["nextjs", "vite", ""],
193
+ installCommand: ["pnpm install", ""],
194
+ buildCommand: ["pnpm build", ""],
195
+ outputDirectory: [".next", "dist", ""],
196
+ rootDirectory: ["./", "./apps/web", ""],
197
+ env: [{
198
+ "SOME_PLAIN_VAR": [{
199
+ type: [undefined] as ("secret" | "connection" | undefined)[],
200
+ value: ["some-value", ""],
201
+ key: [undefined] as (string | undefined)[],
202
+ }],
203
+ "SOME_CONNECTION_VAR": [{
204
+ type: ["connection"] as const,
205
+ // Stays valid even when the fuzzer drops `type`: without it, the
206
+ // entry reads as a plain env var whose literal value happens to be
207
+ // "hexclave.projectId".
208
+ value: ["hexclave.projectId", "some-other-service.url"],
209
+ key: [undefined] as (string | undefined)[],
210
+ }],
211
+ "SOME_SECRET_VAR": [{
212
+ type: ["secret"] as const,
213
+ value: [undefined] as (string | undefined)[],
214
+ // Kept undefined: `key` is only allowed when type is "secret", and
215
+ // the fuzzer randomizes each field independently so it cannot honor
216
+ // that coupling (same situation as oauth's customCallbackUrl). The
217
+ // accept path is covered by the schema unit tests and the
218
+ // deployments e2e tests.
219
+ key: [undefined] as (string | undefined)[],
220
+ }],
221
+ }],
222
+ }],
223
+ }],
224
+ }],
188
225
  }] satisfies FuzzerConfig<BranchConfigNormalizedOverride>;
189
226
 
190
227
  const environmentSchemaFuzzerConfig = [{
@@ -293,6 +293,127 @@ const branchOnboardingSchema = yupObject({
293
293
  requireEmailVerification: yupBoolean(),
294
294
  });
295
295
 
296
+ // --- Deployments Schema ---
297
+ // Service DEFINITIONS (type, framework, build config, env vars) live in the
298
+ // branch config so they follow the project's config source: pushable from
299
+ // hexclave.config.ts / GitHub, or editable in the dashboard when the config is
300
+ // unlinked. Operational state (Vercel project id, deployment runs, custom
301
+ // domains and their verification) lives in the backend database instead, keyed
302
+ // by the service id.
303
+ export const DEPLOYMENT_ENV_VAR_KEY_REGEX = /^[A-Za-z_][A-Za-z0-9_]*$/;
304
+ export const DEPLOYMENT_SECRET_KEY_REGEX = /^[a-zA-Z0-9_-]+$/;
305
+ // A connection value is `<serviceId>.<outputKey>` — a typed pointer to another
306
+ // service's output (e.g. "hexclave.projectId") that the backend resolves at
307
+ // deploy time. This is deliberately its own env var TYPE rather than a
308
+ // `{serviceId.outputKey}` interpolation syntax inside plain values: with
309
+ // interpolation, a literal value that happens to contain `{...}` would be
310
+ // misinterpreted as a reference, so plain values must stay entirely literal.
311
+ export const DEPLOYMENT_CONNECTION_VALUE_REGEX = /^[a-zA-Z0-9_-]+\.[A-Za-z0-9_]+$/;
312
+
313
+ // One env var, discriminated by `type`:
314
+ // - absent (plain): a literal `value`, committed to the config.
315
+ // - "secret": only the secret's name (`key`) is in the config; its value must
316
+ // be supplied at deploy time via `hexclave deploy --secret <key>=<value>`
317
+ // and is never persisted by Hexclave.
318
+ // - "connection": `value` names another service's output (see the regex
319
+ // comment above); resolved server-side at deploy time.
320
+ // Exported so the backend's deployments API routes validate request bodies
321
+ // with the exact same rules as pushed configs.
322
+ export const deploymentEnvVarSchema = yupObject({
323
+ type: yupString().oneOf(["secret", "connection"]).optional(),
324
+ value: yupString().when("type", ([type], schema) => {
325
+ switch (type) {
326
+ case "secret": {
327
+ return schema.oneOf([undefined], 'deployment env vars with type "secret" must not have a value — the value is supplied at deploy time via `hexclave deploy --secret <key>=<value>`');
328
+ }
329
+ case "connection": {
330
+ return schema.defined().matches(DEPLOYMENT_CONNECTION_VALUE_REGEX, 'deployment env vars with type "connection" must reference a service output like "hexclave.projectId"');
331
+ }
332
+ default: {
333
+ return schema.defined();
334
+ }
335
+ }
336
+ }),
337
+ key: yupString().when("type", ([type], schema) => type === "secret"
338
+ ? schema.defined().matches(DEPLOYMENT_SECRET_KEY_REGEX, "deployment secret keys must contain only letters, numbers, underscores, and hyphens")
339
+ : schema.oneOf([undefined], 'deployment env vars may only have a key when their type is "secret"')),
340
+ });
341
+
342
+ const branchDeploymentsSchema = yupObject({
343
+ services: yupRecord(
344
+ // "hexclave" is the managed backend's slot on the deployments board; a
345
+ // config entry must never shadow it, so it's rejected at the schema level
346
+ // (not just in the dashboard-create route — pushed configs go through
347
+ // validation too).
348
+ userSpecifiedIdSchema("serviceId").notOneOf(["hexclave"], 'The service id "hexclave" is reserved for the managed Hexclave service'),
349
+ yupObject({
350
+ // Which platform runs the service. Only Vercel-backed services exist
351
+ // today, but the field is required so configs stay unambiguous once more
352
+ // types are added (every write path must state what it's creating).
353
+ type: yupString().oneOf(["vercel"]).defined(),
354
+ framework: yupString().optional(),
355
+ installCommand: yupString().optional(),
356
+ buildCommand: yupString().optional(),
357
+ outputDirectory: yupString().optional(),
358
+ rootDirectory: yupString().optional(),
359
+ env: yupRecord(
360
+ yupString().matches(DEPLOYMENT_ENV_VAR_KEY_REGEX, "deployment env var keys must start with a letter or underscore and contain only letters, digits, and underscores"),
361
+ deploymentEnvVarSchema,
362
+ ),
363
+ }),
364
+ ),
365
+ });
366
+
367
+ import.meta.vitest?.test("branchDeploymentsSchema accepts all three env var types", async ({ expect }) => {
368
+ await expect(branchDeploymentsSchema.validate({
369
+ services: {
370
+ web: {
371
+ type: "vercel",
372
+ rootDirectory: "./",
373
+ framework: "nextjs",
374
+ env: {
375
+ MY_ENV_VAR: { value: "true" },
376
+ DATABASE_CONNECTION_STRING: { type: "secret", key: "db_connection" },
377
+ NEXT_PUBLIC_HEXCLAVE_PROJECT_ID: { type: "connection", value: "hexclave.projectId" },
378
+ },
379
+ },
380
+ },
381
+ }, { abortEarly: false })).resolves.toBeDefined();
382
+ });
383
+
384
+ import.meta.vitest?.test("branchDeploymentsSchema rejects services without a type", async ({ expect }) => {
385
+ await expect(branchDeploymentsSchema.validate({
386
+ services: {
387
+ web: { framework: "nextjs" },
388
+ },
389
+ }, { abortEarly: false })).rejects.toThrow(/type/);
390
+ });
391
+
392
+ import.meta.vitest?.test("branchDeploymentsSchema rejects invalid env var shapes", async ({ expect }) => {
393
+ // A secret with an inline value would defeat the whole point of secrets.
394
+ await expect(branchDeploymentsSchema.validate({
395
+ services: { web: { type: "vercel", env: { A: { type: "secret", key: "a", value: "leaked" } } } },
396
+ }, { abortEarly: false })).rejects.toThrow(/must not have a value/);
397
+ // A secret without a key can never be filled at deploy time.
398
+ await expect(branchDeploymentsSchema.validate({
399
+ services: { web: { type: "vercel", env: { A: { type: "secret" } } } },
400
+ }, { abortEarly: false })).rejects.toThrow(/key/);
401
+ // Plain values may not carry a secret key.
402
+ await expect(branchDeploymentsSchema.validate({
403
+ services: { web: { type: "vercel", env: { A: { value: "x", key: "a" } } } },
404
+ }, { abortEarly: false })).rejects.toThrow(/only have a key/);
405
+ // Connections must point at `<serviceId>.<outputKey>` — braces in particular
406
+ // are the OLD interpolation syntax and must not validate.
407
+ await expect(branchDeploymentsSchema.validate({
408
+ services: { web: { type: "vercel", env: { A: { type: "connection", value: "{hexclave.projectId}" } } } },
409
+ }, { abortEarly: false })).rejects.toThrow(/service output/);
410
+ // Env var keys must be valid POSIX-ish env var names.
411
+ await expect(branchDeploymentsSchema.validate({
412
+ services: { web: { type: "vercel", env: { "1BAD": { value: "x" } } } },
413
+ }, { abortEarly: false })).rejects.toThrow(/env var keys/);
414
+ });
415
+ // --- END Deployments Schema ---
416
+
296
417
 
297
418
  export const branchConfigSchema = canNoLongerBeOverridden(projectConfigSchema, [
298
419
  "sourceOfTruth",
@@ -327,6 +448,7 @@ export const branchConfigSchema = canNoLongerBeOverridden(projectConfigSchema, [
327
448
 
328
449
  payments: branchPaymentsSchema,
329
450
 
451
+ "deployments-alpha": branchDeploymentsSchema,
330
452
 
331
453
  dataVault: yupObject({
332
454
  stores: yupRecord(
@@ -574,6 +696,16 @@ export function migrateConfigOverride(type: "project" | "branch" | "environment"
574
696
  }
575
697
  // END
576
698
 
699
+ // BEGIN 2026-07-24: the deployments app is now `deployments-alpha`, both as its
700
+ // `apps.installed` key and as its top-level config section, so the alpha stage is
701
+ // visible in every hexclave.config.ts that opts into it. Both renames are needed:
702
+ // an installed entry without the config section (or vice versa) is a valid state.
703
+ if (isBranchOrHigher) {
704
+ res = renameProperty(res, "deployments", "deployments-alpha");
705
+ res = renameProperty(res, "apps.installed.deployments", "deployments-alpha");
706
+ }
707
+ // END
708
+
577
709
  // return the result
578
710
  return res;
579
711
  };
@@ -608,6 +740,51 @@ import.meta.vitest?.test("migrateConfigOverride removes legacy branch-level dbSy
608
740
  expect(migrateConfigOverride("environment", { dbSync })).toEqual({ dbSync });
609
741
  });
610
742
 
743
+ import.meta.vitest?.test("migrateConfigOverride renames the deployments app to deployments-alpha", ({ expect }) => {
744
+ const services = { web: { type: "vercel", rootDirectory: "./" } };
745
+
746
+ // Fully nested overrides.
747
+ expect(migrateConfigOverride("branch", {
748
+ deployments: { services },
749
+ apps: { installed: { deployments: { enabled: true } } },
750
+ })).toEqual({
751
+ "deployments-alpha": { services },
752
+ apps: { installed: { "deployments-alpha": { enabled: true } } },
753
+ });
754
+
755
+ // Dot-notation overrides, which is how the dashboard and the Deployments app
756
+ // itself write single keys. The flat key form is preserved — only the renamed
757
+ // segment changes — so the override keeps overriding exactly one leaf rather
758
+ // than being widened into a whole subtree.
759
+ expect(migrateConfigOverride("branch", {
760
+ "deployments.services.web.buildCommand": "pnpm build",
761
+ "apps.installed.deployments.enabled": true,
762
+ })).toEqual({
763
+ "deployments-alpha.services.web.buildCommand": "pnpm build",
764
+ "apps.installed.deployments-alpha.enabled": true,
765
+ });
766
+
767
+ // A service literally named "deployments" sits below the renamed section and
768
+ // must survive untouched — only the top-level section and the installed-apps
769
+ // entry are renamed.
770
+ expect(migrateConfigOverride("branch", {
771
+ deployments: { services: { deployments: { type: "vercel" } } },
772
+ })).toEqual({
773
+ "deployments-alpha": { services: { deployments: { type: "vercel" } } },
774
+ });
775
+
776
+ // Already-migrated configs are left alone (the migration must be idempotent,
777
+ // since it re-runs on every read).
778
+ expect(migrateConfigOverride("branch", {
779
+ "deployments-alpha": { services },
780
+ })).toEqual({
781
+ "deployments-alpha": { services },
782
+ });
783
+
784
+ // Project level is not branch-or-higher, so nothing is renamed there.
785
+ expect(migrateConfigOverride("project", { deployments: { services } })).toEqual({ deployments: { services } });
786
+ });
787
+
611
788
  function removeProperty(obj: Record<string, any>, pathCond: (path: (string | symbol)[]) => boolean): any {
612
789
  return mapProperty(obj, pathCond, () => undefined);
613
790
  }
@@ -875,6 +1052,22 @@ const organizationConfigDefaults = {
875
1052
  }),
876
1053
  },
877
1054
 
1055
+ "deployments-alpha": {
1056
+ services: (key: string) => ({
1057
+ type: undefined,
1058
+ framework: undefined,
1059
+ installCommand: undefined,
1060
+ buildCommand: undefined,
1061
+ outputDirectory: undefined,
1062
+ rootDirectory: undefined,
1063
+ env: (envVarKey: string) => ({
1064
+ type: undefined,
1065
+ value: undefined,
1066
+ key: undefined,
1067
+ }),
1068
+ }),
1069
+ },
1070
+
878
1071
  dataVault: {
879
1072
  stores: (key: string) => ({
880
1073
  displayName: "Unnamed Vault",
@@ -36,6 +36,74 @@ export type ChatContent = Array<
36
36
  | { type: "tool-call", toolName: string, toolCallId: string, args: any, argsText: string, result: any }
37
37
  >;
38
38
 
39
+ export type AdminDeploymentRunJson = {
40
+ id: string,
41
+ service_id: string,
42
+ status: "queued" | "building" | "ready" | "error" | "canceled",
43
+ target: string,
44
+ triggered_by: string,
45
+ url: string | null,
46
+ error: string | null,
47
+ created_at_millis: number,
48
+ finished_at_millis: number | null,
49
+ };
50
+
51
+ // One env var of a deployment service, normalized from the config-side
52
+ // definition: "plain" vars carry their literal `value`, "connection" vars
53
+ // carry the "serviceId.outputKey" reference they resolve to at deploy time,
54
+ // and "secret" vars carry only the `secret_key` whose value is supplied via
55
+ // `hexclave deploy --secret <key>=<value>` (never stored).
56
+ export type AdminDeploymentEnvVarJson = {
57
+ key: string,
58
+ type: "plain" | "secret" | "connection",
59
+ value: string | null,
60
+ secret_key: string | null,
61
+ };
62
+
63
+ // The config-side shape of one env var, mirroring
64
+ // `deployments-alpha.services.<id>.env.<KEY>` in hexclave.config.ts: no type means a
65
+ // plain value, "secret" requires `key`, "connection" requires a
66
+ // "serviceId.outputKey" `value`.
67
+ export type AdminDeploymentEnvVarOptions =
68
+ | { type?: undefined, value: string }
69
+ | { type: "secret", key: string }
70
+ | { type: "connection", value: string };
71
+
72
+ export type AdminDeploymentServiceJson = {
73
+ id: string,
74
+ type: "vercel",
75
+ framework: string | null,
76
+ install_command: string | null,
77
+ build_command: string | null,
78
+ output_directory: string | null,
79
+ root_directory: string | null,
80
+ provisioned: boolean,
81
+ status: "not_deployed" | "queued" | "building" | "deployed" | "failed" | "canceled",
82
+ has_successful_deploy: boolean,
83
+ url: string | null,
84
+ env: AdminDeploymentEnvVarJson[],
85
+ domains: { hostname: string, is_primary: boolean, verified: boolean }[],
86
+ latest_run: AdminDeploymentRunJson | null,
87
+ };
88
+
89
+ // null means "unset this field" (falls back to platform auto-detection);
90
+ // undefined means "leave unchanged".
91
+ export type AdminDeploymentServiceBuildOptions = {
92
+ framework?: string | null,
93
+ install_command?: string | null,
94
+ build_command?: string | null,
95
+ output_directory?: string | null,
96
+ root_directory?: string | null,
97
+ };
98
+
99
+ export type AdminDeploymentDomainJson = {
100
+ hostname: string,
101
+ is_primary: boolean,
102
+ verified: boolean,
103
+ pending_first_deploy: boolean,
104
+ dns_records: { type: string, name: string, value: string }[],
105
+ };
106
+
39
107
  export type AdminAuthApplicationOptions = ServerAuthApplicationOptions &(
40
108
  | {
41
109
  superSecretAdminKey: string,
@@ -1204,4 +1272,109 @@ export class HexclaveAdminInterface extends HexclaveServerInterface {
1204
1272
  return await response.json();
1205
1273
  }
1206
1274
 
1275
+ // ---- Deployments app ------------------------------------------------------
1276
+
1277
+ async listDeploymentServices(): Promise<AdminDeploymentServiceJson[]> {
1278
+ const response = await this.sendAdminRequest(
1279
+ "/deployments/services",
1280
+ { method: "GET" },
1281
+ null,
1282
+ );
1283
+ return (await response.json()).items;
1284
+ }
1285
+
1286
+ async createDeploymentService(id: string, build: AdminDeploymentServiceBuildOptions): Promise<AdminDeploymentServiceJson> {
1287
+ const response = await this.sendAdminRequest(
1288
+ "/deployments/services",
1289
+ {
1290
+ method: "POST",
1291
+ headers: {
1292
+ "content-type": "application/json",
1293
+ },
1294
+ body: JSON.stringify({ id, ...build }),
1295
+ },
1296
+ null,
1297
+ );
1298
+ return await response.json();
1299
+ }
1300
+
1301
+ async updateDeploymentService(serviceId: string, update: AdminDeploymentServiceBuildOptions & {
1302
+ // Replaces the service's whole env var set (config-side definition shape).
1303
+ env?: Record<string, AdminDeploymentEnvVarOptions>,
1304
+ }): Promise<AdminDeploymentServiceJson> {
1305
+ const response = await this.sendAdminRequest(
1306
+ urlString`/deployments/services/${serviceId}`,
1307
+ {
1308
+ method: "PATCH",
1309
+ headers: {
1310
+ "content-type": "application/json",
1311
+ },
1312
+ body: JSON.stringify(update),
1313
+ },
1314
+ null,
1315
+ );
1316
+ return await response.json();
1317
+ }
1318
+
1319
+ async deleteDeploymentService(serviceId: string): Promise<void> {
1320
+ await this.sendAdminRequest(
1321
+ urlString`/deployments/services/${serviceId}`,
1322
+ { method: "DELETE" },
1323
+ null,
1324
+ );
1325
+ }
1326
+
1327
+ async listDeploymentRuns(serviceId: string, options?: { limit?: number }): Promise<AdminDeploymentRunJson[]> {
1328
+ const response = await this.sendAdminRequest(
1329
+ urlString`/deployments/services/${serviceId}/runs` + (options?.limit !== undefined ? `?limit=${options.limit}` : ""),
1330
+ { method: "GET" },
1331
+ null,
1332
+ );
1333
+ return (await response.json()).items;
1334
+ }
1335
+
1336
+ async getDeploymentRunLogs(runId: string, options?: { signal?: AbortSignal }): Promise<string> {
1337
+ // The endpoint streams chunked plain text until the run is terminal (or a
1338
+ // server-side cap); reading the full body gives "the logs so far". Pass a
1339
+ // signal so an abandoned view can abort — otherwise the server keeps
1340
+ // following the build for minutes.
1341
+ const response = await this.sendAdminRequest(
1342
+ urlString`/deployments/runs/${runId}/logs`,
1343
+ { method: "GET", signal: options?.signal },
1344
+ null,
1345
+ );
1346
+ return await response.text();
1347
+ }
1348
+
1349
+ async addDeploymentServiceDomain(serviceId: string, hostname: string, options?: { isPrimary?: boolean }): Promise<void> {
1350
+ await this.sendAdminRequest(
1351
+ urlString`/deployments/services/${serviceId}/domains`,
1352
+ {
1353
+ method: "POST",
1354
+ headers: {
1355
+ "content-type": "application/json",
1356
+ },
1357
+ body: JSON.stringify({ hostname, ...(options?.isPrimary ? { is_primary: true } : {}) }),
1358
+ },
1359
+ null,
1360
+ );
1361
+ }
1362
+
1363
+ async getDeploymentServiceDomain(serviceId: string, hostname: string): Promise<AdminDeploymentDomainJson> {
1364
+ const response = await this.sendAdminRequest(
1365
+ urlString`/deployments/services/${serviceId}/domains/${hostname}`,
1366
+ { method: "GET" },
1367
+ null,
1368
+ );
1369
+ return await response.json();
1370
+ }
1371
+
1372
+ async deleteDeploymentServiceDomain(serviceId: string, hostname: string): Promise<void> {
1373
+ await this.sendAdminRequest(
1374
+ urlString`/deployments/services/${serviceId}/domains/${hostname}`,
1375
+ { method: "DELETE" },
1376
+ null,
1377
+ );
1378
+ }
1379
+
1207
1380
  }