@hexclave/shared 1.0.60 → 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 (157) hide show
  1. package/dist/{admin-interface-BxUHaniY.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-DQ7vvSym.d.ts → admin-metrics-bsOkw4YQ.d.ts} +16 -16
  4. package/dist/{admin-metrics-DQ7vvSym.d.ts.map → admin-metrics-bsOkw4YQ.d.ts.map} +1 -1
  5. package/dist/ai/unified-prompts/reminders.js +1 -1
  6. package/dist/ai/unified-prompts/reminders.js.map +1 -1
  7. package/dist/ai/unified-prompts/skill-site-prompt-parts/ai-setup-prompt.d.ts.map +1 -1
  8. package/dist/ai/unified-prompts/skill-site-prompt-parts/ai-setup-prompt.js +12 -1
  9. package/dist/ai/unified-prompts/skill-site-prompt-parts/ai-setup-prompt.js.map +1 -1
  10. package/dist/ai/unified-prompts/skill-site-prompt-parts/deployments-skill.d.ts +5 -0
  11. package/dist/ai/unified-prompts/skill-site-prompt-parts/deployments-skill.d.ts.map +1 -0
  12. package/dist/ai/unified-prompts/skill-site-prompt-parts/deployments-skill.js +128 -0
  13. package/dist/ai/unified-prompts/skill-site-prompt-parts/deployments-skill.js.map +1 -0
  14. package/dist/ai/unified-prompts/skill-site-prompt-parts/docs-json.generated.d.ts +1 -1
  15. package/dist/ai/unified-prompts/skill-site-prompt-parts/docs-json.generated.js +1 -0
  16. package/dist/ai/unified-prompts/skill-site-prompt-parts/docs-json.generated.js.map +1 -1
  17. package/dist/ai/unified-prompts/skill-site-prompt.d.ts +3 -1
  18. package/dist/ai/unified-prompts/skill-site-prompt.d.ts.map +1 -1
  19. package/dist/ai/unified-prompts/skill-site-prompt.js +11 -0
  20. package/dist/ai/unified-prompts/skill-site-prompt.js.map +1 -1
  21. package/dist/apps/apps-config.d.ts +6 -0
  22. package/dist/apps/apps-config.d.ts.map +1 -1
  23. package/dist/apps/apps-config.js +6 -0
  24. package/dist/apps/apps-config.js.map +1 -1
  25. package/dist/{client-interface-D-cO5E3C.d.ts → client-interface-DvkciaL0.d.ts} +10 -10
  26. package/dist/{client-interface-D-cO5E3C.d.ts.map → client-interface-DvkciaL0.d.ts.map} +1 -1
  27. package/dist/config/schema-fuzzer.test.js +38 -1
  28. package/dist/config/schema-fuzzer.test.js.map +1 -1
  29. package/dist/config/schema.d.ts +2 -2
  30. package/dist/config/schema.js +45 -0
  31. package/dist/config/schema.js.map +1 -1
  32. package/dist/config-authoring.d.ts +1 -1
  33. package/dist/{current-user-Cg6by3KK.d.ts → current-user-BBoN3hsn.d.ts} +13 -13
  34. package/dist/{current-user-Cg6by3KK.d.ts.map → current-user-BBoN3hsn.d.ts.map} +1 -1
  35. package/dist/{email-outbox-B3lyx4Eg.d.ts → email-outbox-CqzwInBL.d.ts} +207 -207
  36. package/dist/{email-outbox-B3lyx4Eg.d.ts.map → email-outbox-CqzwInBL.d.ts.map} +1 -1
  37. package/dist/esm/ai/unified-prompts/reminders.js +1 -1
  38. package/dist/esm/ai/unified-prompts/reminders.js.map +1 -1
  39. package/dist/esm/ai/unified-prompts/skill-site-prompt-parts/ai-setup-prompt.d.ts.map +1 -1
  40. package/dist/esm/ai/unified-prompts/skill-site-prompt-parts/ai-setup-prompt.js +12 -1
  41. package/dist/esm/ai/unified-prompts/skill-site-prompt-parts/ai-setup-prompt.js.map +1 -1
  42. package/dist/esm/ai/unified-prompts/skill-site-prompt-parts/deployments-skill.d.ts +5 -0
  43. package/dist/esm/ai/unified-prompts/skill-site-prompt-parts/deployments-skill.d.ts.map +1 -0
  44. package/dist/esm/ai/unified-prompts/skill-site-prompt-parts/deployments-skill.js +128 -0
  45. package/dist/esm/ai/unified-prompts/skill-site-prompt-parts/deployments-skill.js.map +1 -0
  46. package/dist/esm/ai/unified-prompts/skill-site-prompt-parts/docs-json.generated.d.ts +1 -1
  47. package/dist/esm/ai/unified-prompts/skill-site-prompt-parts/docs-json.generated.js +1 -0
  48. package/dist/esm/ai/unified-prompts/skill-site-prompt-parts/docs-json.generated.js.map +1 -1
  49. package/dist/esm/ai/unified-prompts/skill-site-prompt.d.ts +3 -1
  50. package/dist/esm/ai/unified-prompts/skill-site-prompt.d.ts.map +1 -1
  51. package/dist/esm/ai/unified-prompts/skill-site-prompt.js +10 -1
  52. package/dist/esm/ai/unified-prompts/skill-site-prompt.js.map +1 -1
  53. package/dist/esm/apps/apps-config.d.ts +6 -0
  54. package/dist/esm/apps/apps-config.d.ts.map +1 -1
  55. package/dist/esm/apps/apps-config.js +6 -0
  56. package/dist/esm/apps/apps-config.js.map +1 -1
  57. package/dist/esm/config/schema-fuzzer.test.js +38 -1
  58. package/dist/esm/config/schema-fuzzer.test.js.map +1 -1
  59. package/dist/esm/config/schema.d.ts +220 -90
  60. package/dist/esm/config/schema.d.ts.map +1 -1
  61. package/dist/esm/config/schema.js +42 -1
  62. package/dist/esm/config/schema.js.map +1 -1
  63. package/dist/esm/interface/admin-interface.d.ts +83 -1
  64. package/dist/esm/interface/admin-interface.d.ts.map +1 -1
  65. package/dist/esm/interface/admin-interface.js +48 -0
  66. package/dist/esm/interface/admin-interface.js.map +1 -1
  67. package/dist/esm/interface/admin-metrics.d.ts +15 -15
  68. package/dist/esm/interface/conversations.d.ts +36 -36
  69. package/dist/esm/interface/crud/current-user.d.ts +12 -12
  70. package/dist/esm/interface/crud/email-outbox.d.ts +206 -206
  71. package/dist/esm/interface/crud/invoices.d.ts +2 -2
  72. package/dist/esm/interface/crud/products.d.ts +23 -23
  73. package/dist/esm/interface/crud/project-api-keys.d.ts +4 -4
  74. package/dist/esm/interface/crud/projects.d.ts +59 -59
  75. package/dist/esm/interface/crud/team-member-profiles.d.ts +26 -26
  76. package/dist/esm/interface/crud/transactions.d.ts +20 -20
  77. package/dist/esm/interface/crud/transactions.d.ts.map +1 -1
  78. package/dist/esm/interface/crud/users.d.ts +16 -16
  79. package/dist/esm/interface/plan-usage.d.ts +2 -2
  80. package/dist/esm/interface/webhooks.d.ts +2 -2
  81. package/dist/esm/known-errors.d.ts +1 -1
  82. package/dist/esm/schema-fields.d.ts +4 -4
  83. package/dist/esm/sessions.d.ts +11 -11
  84. package/dist/esm/utils/tar.d.ts +26 -0
  85. package/dist/esm/utils/tar.d.ts.map +1 -0
  86. package/dist/esm/utils/tar.js +136 -0
  87. package/dist/esm/utils/tar.js.map +1 -0
  88. package/dist/helpers/password.d.ts +1 -1
  89. package/dist/helpers/production-mode.d.ts +1 -1
  90. package/dist/index.d.ts +4 -4
  91. package/dist/interface/admin-interface.d.ts +3 -3
  92. package/dist/interface/admin-interface.js +48 -0
  93. package/dist/interface/admin-interface.js.map +1 -1
  94. package/dist/interface/admin-metrics.d.ts +1 -1
  95. package/dist/interface/client-interface.d.ts +1 -1
  96. package/dist/interface/conversations.d.ts +36 -36
  97. package/dist/interface/crud/current-user.d.ts +1 -1
  98. package/dist/interface/crud/email-outbox.d.ts +1 -1
  99. package/dist/interface/crud/invoices.d.ts +1 -1
  100. package/dist/interface/crud/products.d.ts +1 -1
  101. package/dist/interface/crud/project-api-keys.d.ts +1 -1
  102. package/dist/interface/crud/projects.d.ts +1 -1
  103. package/dist/interface/crud/team-member-profiles.d.ts +1 -1
  104. package/dist/interface/crud/transactions.d.ts +1 -1
  105. package/dist/interface/crud/users.d.ts +1 -1
  106. package/dist/interface/plan-usage.d.ts +1 -1
  107. package/dist/interface/server-interface.d.ts +1 -1
  108. package/dist/interface/webhooks.d.ts +1 -1
  109. package/dist/{invoices-BGLLnTlR.d.ts → invoices-CHo9YKbI.d.ts} +3 -3
  110. package/dist/{invoices-BGLLnTlR.d.ts.map → invoices-CHo9YKbI.d.ts.map} +1 -1
  111. package/dist/{known-errors-BFf8SVm8.d.ts → known-errors-Bm4a61wL.d.ts} +2 -2
  112. package/dist/{known-errors-BFf8SVm8.d.ts.map → known-errors-Bm4a61wL.d.ts.map} +1 -1
  113. package/dist/known-errors.d.ts +1 -1
  114. package/dist/{plan-usage-Dw09F0sh.d.ts → plan-usage-BaDxcLZ1.d.ts} +3 -3
  115. package/dist/{plan-usage-Dw09F0sh.d.ts.map → plan-usage-BaDxcLZ1.d.ts.map} +1 -1
  116. package/dist/{products-Cwn7_4uD.d.ts → products-D4o8A2W0.d.ts} +24 -24
  117. package/dist/{products-Cwn7_4uD.d.ts.map → products-D4o8A2W0.d.ts.map} +1 -1
  118. package/dist/{project-api-keys-CXTBQ2IZ.d.ts → project-api-keys--7V5X_UW.d.ts} +5 -5
  119. package/dist/{project-api-keys-CXTBQ2IZ.d.ts.map → project-api-keys--7V5X_UW.d.ts.map} +1 -1
  120. package/dist/{projects-Cw80wrT7.d.ts → projects-BX5Y50kJ.d.ts} +60 -60
  121. package/dist/{projects-Cw80wrT7.d.ts.map → projects-BX5Y50kJ.d.ts.map} +1 -1
  122. package/dist/{schema-DGZfskAR.d.ts → schema-dJ_7rUCO.d.ts} +221 -91
  123. package/dist/schema-dJ_7rUCO.d.ts.map +1 -0
  124. package/dist/{schema-fields-DPaAnhmc.d.ts → schema-fields-DZtDQEEO.d.ts} +5 -5
  125. package/dist/{schema-fields-DPaAnhmc.d.ts.map → schema-fields-DZtDQEEO.d.ts.map} +1 -1
  126. package/dist/schema-fields.d.ts +1 -1
  127. package/dist/{server-interface-BXeSO8Wj.d.ts → server-interface-Ra_vIHP4.d.ts} +8 -8
  128. package/dist/{server-interface-BXeSO8Wj.d.ts.map → server-interface-Ra_vIHP4.d.ts.map} +1 -1
  129. package/dist/{sessions-B7755rIu.d.ts → sessions-CI-0TBQb.d.ts} +13 -13
  130. package/dist/{sessions-B7755rIu.d.ts.map → sessions-CI-0TBQb.d.ts.map} +1 -1
  131. package/dist/sessions.d.ts +1 -1
  132. package/dist/{team-member-profiles-B9sAsj4g.d.ts → team-member-profiles-BoDEqx-0.d.ts} +27 -27
  133. package/dist/{team-member-profiles-B9sAsj4g.d.ts.map → team-member-profiles-BoDEqx-0.d.ts.map} +1 -1
  134. package/dist/{transactions-Brdmr3jE.d.ts → transactions-CHNUJ42J.d.ts} +21 -21
  135. package/dist/transactions-CHNUJ42J.d.ts.map +1 -0
  136. package/dist/{users-ydVQVHD_.d.ts → users-DHNRwoSz.d.ts} +18 -18
  137. package/dist/{users-ydVQVHD_.d.ts.map → users-DHNRwoSz.d.ts.map} +1 -1
  138. package/dist/utils/tar.d.ts +26 -0
  139. package/dist/utils/tar.d.ts.map +1 -0
  140. package/dist/utils/tar.js +138 -0
  141. package/dist/utils/tar.js.map +1 -0
  142. package/dist/{webhooks-ByXrdSOx.d.ts → webhooks-8Ts1Clcb.d.ts} +3 -3
  143. package/dist/{webhooks-ByXrdSOx.d.ts.map → webhooks-8Ts1Clcb.d.ts.map} +1 -1
  144. package/package.json +1 -1
  145. package/src/ai/unified-prompts/reminders.ts +1 -1
  146. package/src/ai/unified-prompts/skill-site-prompt-parts/ai-setup-prompt.ts +7 -7
  147. package/src/ai/unified-prompts/skill-site-prompt-parts/deployments-skill.ts +128 -0
  148. package/src/ai/unified-prompts/skill-site-prompt-parts/docs-json.generated.ts +1 -0
  149. package/src/ai/unified-prompts/skill-site-prompt.ts +15 -0
  150. package/src/apps/apps-config.ts +9 -0
  151. package/src/config/schema-fuzzer.test.ts +37 -0
  152. package/src/config/schema.ts +193 -0
  153. package/src/interface/admin-interface.ts +173 -0
  154. package/src/utils/tar.tsx +261 -0
  155. package/dist/admin-interface-BxUHaniY.d.ts.map +0 -1
  156. package/dist/schema-DGZfskAR.d.ts.map +0 -1
  157. package/dist/transactions-Brdmr3jE.d.ts.map +0 -1
@@ -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
  }