@h-ai/deploy 0.1.0-alpha.19 → 0.1.0-alpha.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,12 +1,13 @@
1
1
  import { z } from 'zod';
2
- import { core, ok, err } from '@h-ai/core';
2
+ import { core, err, ok } from '@h-ai/core';
3
3
  import { existsSync, readFileSync, writeFileSync, readdirSync, mkdirSync, statSync } from 'fs';
4
4
  import { homedir } from 'os';
5
- import { join, dirname, relative } from 'path';
5
+ import { join, dirname, relative, sep } from 'path';
6
6
  import process from 'process';
7
7
  import { parse, stringify } from 'yaml';
8
8
  import { execSync } from 'child_process';
9
9
  import { createHash } from 'crypto';
10
+ import { Buffer } from 'buffer';
10
11
 
11
12
  // src/deploy-config.ts
12
13
 
@@ -28,12 +29,13 @@ var en_US_default = {
28
29
  deploy_scanFailed: "Application scan failed: {error}",
29
30
  deploy_envVarFailed: "Environment variable setup failed: {error}",
30
31
  deploy_credentialError: "Credential read/write failed: {error}",
32
+ deploy_credentialInvalidKey: "Unsupported credential key: {key}. Only HAI_DEPLOY_* keys are allowed",
31
33
  deploy_credentialSaved: "Credentials saved",
32
34
  deploy_credentialLoaded: "Credentials loaded",
33
35
  deploy_configError: "Deploy config error: {error}",
34
36
  deploy_credentialMissing: "Missing credential fields: {fields}",
35
- deploy_apiError: "{service} API error ({status}): {body}",
36
- deploy_fileUploadFailed: "File upload failed ({status}): {body}",
37
+ deploy_apiError: "{service} API error ({status})",
38
+ deploy_fileUploadFailed: "File upload failed ({status})",
37
39
  deploy_provisionNoResult: "No connection info returned from {service}"
38
40
  };
39
41
 
@@ -55,12 +57,13 @@ var zh_CN_default = {
55
57
  deploy_scanFailed: "\u5E94\u7528\u626B\u63CF\u5931\u8D25\uFF1A{error}",
56
58
  deploy_envVarFailed: "\u73AF\u5883\u53D8\u91CF\u8BBE\u7F6E\u5931\u8D25\uFF1A{error}",
57
59
  deploy_credentialError: "\u51ED\u8BC1\u8BFB\u5199\u5931\u8D25\uFF1A{error}",
60
+ deploy_credentialInvalidKey: "\u4E0D\u652F\u6301\u7684\u51ED\u8BC1\u952E\u540D\uFF1A{key}\u3002\u4EC5\u5141\u8BB8 HAI_DEPLOY_*",
58
61
  deploy_credentialSaved: "\u51ED\u8BC1\u5DF2\u4FDD\u5B58",
59
62
  deploy_credentialLoaded: "\u51ED\u8BC1\u5DF2\u52A0\u8F7D",
60
63
  deploy_configError: "\u90E8\u7F72\u914D\u7F6E\u9519\u8BEF\uFF1A{error}",
61
64
  deploy_credentialMissing: "\u7F3A\u5C11\u51ED\u8BC1\u5B57\u6BB5\uFF1A{fields}",
62
- deploy_apiError: "{service} API \u8BF7\u6C42\u5931\u8D25\uFF08{status}\uFF09\uFF1A{body}",
63
- deploy_fileUploadFailed: "\u6587\u4EF6\u4E0A\u4F20\u5931\u8D25\uFF08{status}\uFF09\uFF1A{body}",
65
+ deploy_apiError: "{service} API \u8BF7\u6C42\u5931\u8D25\uFF08{status}\uFF09",
66
+ deploy_fileUploadFailed: "\u6587\u4EF6\u4E0A\u4F20\u5931\u8D25\uFF08{status}\uFF09",
64
67
  deploy_provisionNoResult: "{service} \u672A\u8FD4\u56DE\u8FDE\u63A5\u4FE1\u606F"
65
68
  };
66
69
 
@@ -122,8 +125,8 @@ var DeployErrorInfo = {
122
125
  PROVISION_FAILED: "007:500",
123
126
  ADAPTER_MISSING: "008:500",
124
127
  SCAN_FAILED: "009:500",
125
- ENV_VAR_FAILED: "010:500",
126
- NOT_INITIALIZED: "011:500",
128
+ NOT_INITIALIZED: "010:500",
129
+ ENV_VAR_FAILED: "011:500",
127
130
  UNSUPPORTED_TYPE: "012:400",
128
131
  CONFIG_ERROR: "013:500",
129
132
  CREDENTIAL_ERROR: "014:500"
@@ -132,6 +135,35 @@ var HaiDeployError = core.error.buildHaiErrorsDef("deploy", DeployErrorInfo);
132
135
 
133
136
  // src/deploy-credentials.ts
134
137
  var logger = core.logger.child({ module: "deploy", scope: "credentials" });
138
+ var DEPLOY_CREDENTIAL_KEY_PATTERN = /^HAI_DEPLOY_[A-Z0-9_]+$/;
139
+ function isAllowedCredentialKey(key) {
140
+ return DEPLOY_CREDENTIAL_KEY_PATTERN.test(key);
141
+ }
142
+ function normalizeCredentialEntries(entries) {
143
+ const normalized = {};
144
+ for (const [key, value] of Object.entries(entries)) {
145
+ if (!isAllowedCredentialKey(key)) {
146
+ logger.warn("Ignoring unsupported deploy credential key", { key });
147
+ continue;
148
+ }
149
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
150
+ normalized[key] = String(value);
151
+ continue;
152
+ }
153
+ logger.warn("Ignoring deploy credential with non-scalar value", { key });
154
+ }
155
+ return normalized;
156
+ }
157
+ function assertAllowedCredentialKey(key) {
158
+ if (!isAllowedCredentialKey(key)) {
159
+ throw new Error(deployM("deploy_credentialInvalidKey", { params: { key } }));
160
+ }
161
+ }
162
+ function assertAllowedCredentialEntries(entries) {
163
+ for (const key of Object.keys(entries)) {
164
+ assertAllowedCredentialKey(key);
165
+ }
166
+ }
135
167
  function getCredentialsPath() {
136
168
  return join(homedir(), ".hai", "credentials.yml");
137
169
  }
@@ -148,10 +180,10 @@ function readCredentialsFile() {
148
180
  }
149
181
  const content = readFileSync(filePath, "utf-8");
150
182
  const parsed = parse(content);
151
- if (parsed === null || parsed === void 0 || typeof parsed !== "object") {
183
+ if (parsed === null || parsed === void 0 || typeof parsed !== "object" || Array.isArray(parsed)) {
152
184
  return {};
153
185
  }
154
- return parsed;
186
+ return normalizeCredentialEntries(parsed);
155
187
  }
156
188
  function writeCredentialsFile(data) {
157
189
  const filePath = getCredentialsPath();
@@ -182,6 +214,7 @@ function loadCredentials() {
182
214
  }
183
215
  function saveCredential(key, value) {
184
216
  try {
217
+ assertAllowedCredentialKey(key);
185
218
  const data = readCredentialsFile();
186
219
  data[key] = value;
187
220
  writeCredentialsFile(data);
@@ -201,6 +234,7 @@ function saveCredential(key, value) {
201
234
  }
202
235
  function saveCredentials(entries) {
203
236
  try {
237
+ assertAllowedCredentialEntries(entries);
204
238
  const data = readCredentialsFile();
205
239
  for (const [key, value] of Object.entries(entries)) {
206
240
  data[key] = value;
@@ -426,6 +460,12 @@ var logger4 = core.logger.child({ module: "deploy", scope: "provider-vercel" });
426
460
  var VERCEL_API = "https://api.vercel.com";
427
461
  var POLL_INTERVAL = 3e3;
428
462
  var MAX_POLL_ATTEMPTS = 60;
463
+ var VercelApiError = class extends Error {
464
+ constructor(status) {
465
+ super(deployM("deploy_apiError", { params: { service: "Vercel", status: String(status) } }));
466
+ this.status = status;
467
+ }
468
+ };
429
469
  function toDeployError(codeDef, messageKey, error) {
430
470
  return {
431
471
  ...codeDef,
@@ -446,8 +486,7 @@ async function vercelFetch(token, path, options) {
446
486
  }
447
487
  });
448
488
  if (!response.ok) {
449
- const body = await response.text();
450
- throw new Error(deployM("deploy_apiError", { params: { service: "Vercel", status: String(response.status), body } }));
489
+ throw new VercelApiError(response.status);
451
490
  }
452
491
  return response.json();
453
492
  }
@@ -459,7 +498,7 @@ function collectFiles(dir, baseDir) {
459
498
  if (entry.isDirectory()) {
460
499
  results.push(...collectFiles(fullPath, baseDir));
461
500
  } else {
462
- results.push(relative(baseDir, fullPath));
501
+ results.push(relative(baseDir, fullPath).split(sep).join("/"));
463
502
  }
464
503
  }
465
504
  return results;
@@ -498,19 +537,33 @@ function createVercelProvider() {
498
537
  logger4.info("Vercel project found", { projectName, projectId: existingId });
499
538
  return ok(existingId);
500
539
  }
501
- const project = await vercelFetch(
502
- token,
503
- "/v10/projects",
504
- {
505
- method: "POST",
506
- body: JSON.stringify({
507
- name: projectName,
508
- framework: "sveltekit"
509
- })
540
+ try {
541
+ const project = await vercelFetch(
542
+ token,
543
+ "/v11/projects",
544
+ {
545
+ method: "POST",
546
+ body: JSON.stringify({
547
+ name: projectName,
548
+ framework: "sveltekit"
549
+ })
550
+ }
551
+ );
552
+ logger4.info("Vercel project created", { projectName, projectId: project.id });
553
+ return ok(project.id);
554
+ } catch (error) {
555
+ if (error instanceof VercelApiError && error.status === 409) {
556
+ const concurrentProjectId = await findExistingProject(token, projectName);
557
+ if (concurrentProjectId !== null) {
558
+ logger4.info("Vercel project reused after create conflict", {
559
+ projectName,
560
+ projectId: concurrentProjectId
561
+ });
562
+ return ok(concurrentProjectId);
563
+ }
510
564
  }
511
- );
512
- logger4.info("Vercel project created", { projectName, projectId: project.id });
513
- return ok(project.id);
565
+ throw error;
566
+ }
514
567
  } catch (error) {
515
568
  logger4.error("Vercel project creation failed", { projectName, error });
516
569
  return err(toDeployError(HaiDeployError.PROJECT_CREATE_FAILED, "deploy_projectCreateFailed", error));
@@ -533,7 +586,7 @@ function createVercelProvider() {
533
586
  }));
534
587
  await vercelFetch(
535
588
  token,
536
- `/v10/projects/${projectId}/env`,
589
+ `/v10/projects/${projectId}/env?upsert=true`,
537
590
  {
538
591
  method: "POST",
539
592
  body: JSON.stringify(envEntries)
@@ -602,12 +655,16 @@ async function findExistingProject(token, projectName) {
602
655
  try {
603
656
  const project = await vercelFetch(token, `/v10/projects/${projectName}`);
604
657
  return project.id;
605
- } catch {
606
- return null;
658
+ } catch (error) {
659
+ if (error instanceof VercelApiError && error.status === 404) {
660
+ return null;
661
+ }
662
+ throw error;
607
663
  }
608
664
  }
609
665
  async function uploadFile(token, content, sha) {
610
666
  const url = `${VERCEL_API}/v2/files`;
667
+ const uploadContent = new Uint8Array(content);
611
668
  const response = await fetch(url, {
612
669
  method: "POST",
613
670
  headers: {
@@ -615,11 +672,10 @@ async function uploadFile(token, content, sha) {
615
672
  "Content-Type": "application/octet-stream",
616
673
  "x-vercel-digest": sha
617
674
  },
618
- body: new Blob([content.buffer])
675
+ body: new Blob([uploadContent])
619
676
  });
620
677
  if (!response.ok && response.status !== 409) {
621
- const body = await response.text();
622
- throw new Error(deployM("deploy_fileUploadFailed", { params: { status: String(response.status), body } }));
678
+ throw new Error(deployM("deploy_fileUploadFailed", { params: { status: String(response.status) } }));
623
679
  }
624
680
  }
625
681
  async function pollDeploymentStatus(token, deploymentId) {
@@ -690,6 +746,56 @@ function createAliyunProvisioner() {
690
746
  }
691
747
  var logger6 = core.logger.child({ module: "deploy", scope: "provisioner-neon" });
692
748
  var NEON_API = "https://console.neon.tech/api/v2";
749
+ var DEFAULT_NEON_DATABASE_NAME = "neondb";
750
+ var DEFAULT_NEON_ROLE_NAME = "neondb_owner";
751
+ async function neonFetch(token, path, options) {
752
+ const response = await fetch(`${NEON_API}${path}`, {
753
+ ...options,
754
+ headers: {
755
+ "Accept": "application/json",
756
+ "Authorization": `Bearer ${token}`,
757
+ "Content-Type": "application/json",
758
+ ...options?.headers
759
+ }
760
+ });
761
+ if (!response.ok) {
762
+ throw new Error(deployM("deploy_apiError", { params: { service: "Neon", status: String(response.status) } }));
763
+ }
764
+ return response.json();
765
+ }
766
+ async function listNeonProjects(token) {
767
+ const data = await neonFetch(token, "/projects");
768
+ return data.projects ?? [];
769
+ }
770
+ async function findExistingNeonProject(token, projectName) {
771
+ const projects = await listNeonProjects(token);
772
+ return projects.find((project) => project.name === projectName) ?? null;
773
+ }
774
+ async function getNeonConnectionUri(token, projectId) {
775
+ const query = new URLSearchParams({
776
+ database_name: DEFAULT_NEON_DATABASE_NAME,
777
+ role_name: DEFAULT_NEON_ROLE_NAME
778
+ });
779
+ const data = await neonFetch(
780
+ token,
781
+ `/projects/${projectId}/connection_uri?${query.toString()}`,
782
+ { headers: { "Content-Type": "application/json" } }
783
+ );
784
+ if (!data.uri) {
785
+ throw new Error(deployM("deploy_provisionNoResult", { params: { service: "Neon" } }));
786
+ }
787
+ return data.uri;
788
+ }
789
+ function buildNeonProvisionResult(projectId, connectionUri) {
790
+ return {
791
+ serviceType: "db",
792
+ provisionerName: "neon",
793
+ envVars: {
794
+ HAI_RELDB_URL: connectionUri
795
+ },
796
+ resourceInfo: `neon-project:${projectId}`
797
+ };
798
+ }
693
799
  function createNeonProvisioner() {
694
800
  let token = null;
695
801
  return {
@@ -702,12 +808,7 @@ function createNeonProvisioner() {
702
808
  if (!apiToken) {
703
809
  throw new Error(deployM("deploy_credentialMissing", { params: { fields: "token" } }));
704
810
  }
705
- const res = await fetch(`${NEON_API}/projects`, {
706
- headers: { Authorization: `Bearer ${apiToken}` }
707
- });
708
- if (!res.ok) {
709
- throw new Error(deployM("deploy_apiError", { params: { service: "Neon", status: String(res.status), body: await res.text() } }));
710
- }
811
+ await listNeonProjects(apiToken);
711
812
  token = apiToken;
712
813
  logger6.info("Neon authenticated");
713
814
  return ok("neon-user");
@@ -731,36 +832,32 @@ function createNeonProvisioner() {
731
832
  }
732
833
  logger6.debug("Provisioning Neon database", { appName });
733
834
  try {
734
- const res = await fetch(`${NEON_API}/projects`, {
835
+ const projectName = `${appName}-db`;
836
+ const existingProject = await findExistingNeonProject(token, projectName);
837
+ if (existingProject !== null) {
838
+ const connectionUri2 = await getNeonConnectionUri(token, existingProject.id);
839
+ logger6.info("Neon project reused", {
840
+ projectId: existingProject.id,
841
+ projectName
842
+ });
843
+ return ok(buildNeonProvisionResult(existingProject.id, connectionUri2));
844
+ }
845
+ const data = await neonFetch(token, "/projects", {
735
846
  method: "POST",
736
- headers: {
737
- "Authorization": `Bearer ${token}`,
738
- "Content-Type": "application/json"
739
- },
740
847
  body: JSON.stringify({
741
848
  project: {
742
- name: `${appName}-db`,
849
+ name: projectName,
743
850
  pg_version: 16
744
851
  }
745
852
  })
746
853
  });
747
- if (!res.ok) {
748
- throw new Error(deployM("deploy_apiError", { params: { service: "Neon", status: String(res.status), body: await res.text() } }));
749
- }
750
- const data = await res.json();
751
- const connectionUri = data.connection_uris?.[0]?.connection_uri;
752
- if (!connectionUri) {
854
+ const projectId = data.project?.id ?? "";
855
+ const connectionUri = data.connection_uris?.[0]?.connection_uri ?? "";
856
+ if (!projectId || !connectionUri) {
753
857
  throw new Error(deployM("deploy_provisionNoResult", { params: { service: "Neon" } }));
754
858
  }
755
- logger6.info("Neon database provisioned", { projectId: data.project.id });
756
- return ok({
757
- serviceType: "db",
758
- provisionerName: "neon",
759
- envVars: {
760
- HAI_RELDB_URL: connectionUri
761
- },
762
- resourceInfo: `neon-project:${data.project.id}`
763
- });
859
+ logger6.info("Neon database provisioned", { projectId });
860
+ return ok(buildNeonProvisionResult(projectId, connectionUri));
764
861
  } catch (error) {
765
862
  logger6.error("Neon provisioning failed", { appName, error });
766
863
  return err(
@@ -797,7 +894,7 @@ function createR2Provisioner() {
797
894
  headers: { Authorization: `Bearer ${apiTok}` }
798
895
  });
799
896
  if (!res.ok) {
800
- throw new Error(deployM("deploy_apiError", { params: { service: "Cloudflare", status: String(res.status), body: await res.text() } }));
897
+ throw new Error(deployM("deploy_apiError", { params: { service: "Cloudflare", status: String(res.status) } }));
801
898
  }
802
899
  accountId = acctId;
803
900
  token = apiTok;
@@ -836,7 +933,7 @@ function createR2Provisioner() {
836
933
  }
837
934
  );
838
935
  if (!createRes.ok && createRes.status !== 409) {
839
- throw new Error(deployM("deploy_apiError", { params: { service: "Cloudflare", status: String(createRes.status), body: await createRes.text() } }));
936
+ throw new Error(deployM("deploy_apiError", { params: { service: "Cloudflare", status: String(createRes.status) } }));
840
937
  }
841
938
  const tokenRes = await fetch(
842
939
  `${CF_API}/user/tokens`,
@@ -856,12 +953,14 @@ function createR2Provisioner() {
856
953
  })
857
954
  }
858
955
  );
859
- let accessKey = "";
860
- let secretKey = "";
861
- if (tokenRes.ok) {
862
- const tokenData = await tokenRes.json();
863
- accessKey = tokenData.result.id;
864
- secretKey = tokenData.result.value;
956
+ if (!tokenRes.ok) {
957
+ throw new Error(deployM("deploy_apiError", { params: { service: "Cloudflare", status: String(tokenRes.status) } }));
958
+ }
959
+ const tokenData = await tokenRes.json();
960
+ const accessKey = tokenData.result?.id ?? "";
961
+ const secretKey = tokenData.result?.value ?? "";
962
+ if (!accessKey || !secretKey) {
963
+ throw new Error(deployM("deploy_provisionNoResult", { params: { service: "Cloudflare R2" } }));
865
964
  }
866
965
  logger7.info("R2 bucket provisioned", { bucketName });
867
966
  return ok({
@@ -909,7 +1008,7 @@ function createResendProvisioner() {
909
1008
  headers: { Authorization: `Bearer ${apiToken}` }
910
1009
  });
911
1010
  if (!res.ok) {
912
- throw new Error(deployM("deploy_apiError", { params: { service: "Resend", status: String(res.status), body: await res.text() } }));
1011
+ throw new Error(deployM("deploy_apiError", { params: { service: "Resend", status: String(res.status) } }));
913
1012
  }
914
1013
  token = apiToken;
915
1014
  logger8.info("Resend authenticated");
@@ -946,6 +1045,69 @@ function createResendProvisioner() {
946
1045
  }
947
1046
  var logger9 = core.logger.child({ module: "deploy", scope: "provisioner-upstash" });
948
1047
  var UPSTASH_API = "https://api.upstash.com";
1048
+ function createUpstashAuthHeader(email, apiKey) {
1049
+ return `Basic ${Buffer.from(`${email}:${apiKey}`).toString("base64")}`;
1050
+ }
1051
+ async function upstashFetch(email, apiKey, path, options) {
1052
+ const response = await fetch(`${UPSTASH_API}${path}`, {
1053
+ ...options,
1054
+ headers: {
1055
+ "Authorization": createUpstashAuthHeader(email, apiKey),
1056
+ "Content-Type": "application/json",
1057
+ ...options?.headers
1058
+ }
1059
+ });
1060
+ if (!response.ok) {
1061
+ throw new Error(deployM("deploy_apiError", { params: { service: "Upstash", status: String(response.status) } }));
1062
+ }
1063
+ return response.json();
1064
+ }
1065
+ async function listUpstashDatabases(email, apiKey) {
1066
+ return upstashFetch(email, apiKey, "/v2/redis/databases");
1067
+ }
1068
+ async function findExistingUpstashDatabase(email, apiKey, databaseName) {
1069
+ const databases = await listUpstashDatabases(email, apiKey);
1070
+ return databases.find((database) => database.database_name === databaseName) ?? null;
1071
+ }
1072
+ async function getUpstashDatabase(email, apiKey, databaseId) {
1073
+ return upstashFetch(email, apiKey, `/v2/redis/database/${databaseId}`);
1074
+ }
1075
+ function resolveUpstashRestUrl(database) {
1076
+ if (typeof database.rest_url === "string" && database.rest_url.length > 0) {
1077
+ return database.rest_url;
1078
+ }
1079
+ const endpoint = database.endpoint?.trim() ?? "";
1080
+ if (!endpoint) {
1081
+ return "";
1082
+ }
1083
+ if (endpoint.startsWith("http://") || endpoint.startsWith("https://")) {
1084
+ return endpoint;
1085
+ }
1086
+ if (endpoint.includes(".")) {
1087
+ return `https://${endpoint}`;
1088
+ }
1089
+ return `https://${endpoint}.upstash.io`;
1090
+ }
1091
+ function resolveUpstashRestToken(database) {
1092
+ return database.rest_token ?? database.token ?? database.password ?? "";
1093
+ }
1094
+ function buildUpstashProvisionResult(database) {
1095
+ const databaseId = database.database_id ?? "";
1096
+ const restUrl = resolveUpstashRestUrl(database);
1097
+ const restToken = resolveUpstashRestToken(database);
1098
+ if (!databaseId || !restUrl || !restToken) {
1099
+ throw new Error(deployM("deploy_provisionNoResult", { params: { service: "Upstash" } }));
1100
+ }
1101
+ return {
1102
+ serviceType: "cache",
1103
+ provisionerName: "upstash",
1104
+ envVars: {
1105
+ HAI_CACHE_UPSTASH_URL: restUrl,
1106
+ HAI_CACHE_UPSTASH_TOKEN: restToken
1107
+ },
1108
+ resourceInfo: `upstash-db:${databaseId}`
1109
+ };
1110
+ }
949
1111
  function createUpstashProvisioner() {
950
1112
  let apiKey = null;
951
1113
  let email = null;
@@ -960,14 +1122,7 @@ function createUpstashProvisioner() {
960
1122
  if (!userEmail || !userKey) {
961
1123
  throw new Error(deployM("deploy_credentialMissing", { params: { fields: "email, api_key" } }));
962
1124
  }
963
- const res = await fetch(`${UPSTASH_API}/v2/redis/databases`, {
964
- headers: {
965
- Authorization: `Basic ${btoa(`${userEmail}:${userKey}`)}`
966
- }
967
- });
968
- if (!res.ok) {
969
- throw new Error(deployM("deploy_apiError", { params: { service: "Upstash", status: String(res.status), body: await res.text() } }));
970
- }
1125
+ await listUpstashDatabases(userEmail, userKey);
971
1126
  email = userEmail;
972
1127
  apiKey = userKey;
973
1128
  logger9.info("Upstash authenticated", { email: userEmail });
@@ -992,32 +1147,27 @@ function createUpstashProvisioner() {
992
1147
  }
993
1148
  logger9.debug("Provisioning Upstash Redis", { appName });
994
1149
  try {
995
- const res = await fetch(`${UPSTASH_API}/v2/redis/database`, {
1150
+ const databaseName = `${appName}-cache`;
1151
+ const existingDatabase = await findExistingUpstashDatabase(email, apiKey, databaseName);
1152
+ if (existingDatabase !== null) {
1153
+ const databaseId = existingDatabase.database_id ?? "";
1154
+ if (!databaseId) {
1155
+ throw new Error(deployM("deploy_provisionNoResult", { params: { service: "Upstash" } }));
1156
+ }
1157
+ const detail = await getUpstashDatabase(email, apiKey, databaseId);
1158
+ logger9.info("Upstash Redis reused", { databaseId, databaseName });
1159
+ return ok(buildUpstashProvisionResult({ ...existingDatabase, ...detail }));
1160
+ }
1161
+ const data = await upstashFetch(email, apiKey, "/v2/redis/database", {
996
1162
  method: "POST",
997
- headers: {
998
- "Authorization": `Basic ${btoa(`${email}:${apiKey}`)}`,
999
- "Content-Type": "application/json"
1000
- },
1001
1163
  body: JSON.stringify({
1002
- name: `${appName}-cache`,
1164
+ name: databaseName,
1003
1165
  region: "global",
1004
1166
  tls: true
1005
1167
  })
1006
1168
  });
1007
- if (!res.ok) {
1008
- throw new Error(deployM("deploy_apiError", { params: { service: "Upstash", status: String(res.status), body: await res.text() } }));
1009
- }
1010
- const data = await res.json();
1011
1169
  logger9.info("Upstash Redis provisioned", { databaseId: data.database_id });
1012
- return ok({
1013
- serviceType: "cache",
1014
- provisionerName: "upstash",
1015
- envVars: {
1016
- HAI_CACHE_UPSTASH_URL: data.rest_url,
1017
- HAI_CACHE_UPSTASH_TOKEN: data.rest_token
1018
- },
1019
- resourceInfo: `upstash-db:${data.database_id}`
1020
- });
1170
+ return ok(buildUpstashProvisionResult(data));
1021
1171
  } catch (error) {
1022
1172
  logger9.error("Upstash provisioning failed", { appName, error });
1023
1173
  return err(
@@ -1078,7 +1228,14 @@ function extractCredentials(serviceConfig) {
1078
1228
  }
1079
1229
  return result;
1080
1230
  }
1231
+ var deployCredentials = {
1232
+ getPath: () => getCredentialsPath(),
1233
+ load: () => loadCredentials(),
1234
+ save: (key, value) => saveCredential(key, value),
1235
+ saveAll: (entries) => saveCredentials(entries)
1236
+ };
1081
1237
  var deploy = {
1238
+ credentials: deployCredentials,
1082
1239
  async init(config) {
1083
1240
  if (initInProgress) {
1084
1241
  logger10.warn("Deploy init already in progress, skipping concurrent call");
@@ -1121,16 +1278,17 @@ var deploy = {
1121
1278
  const credentials = extractCredentials(serviceConfig);
1122
1279
  const provAuthResult = await provisioner.authenticate(credentials);
1123
1280
  if (!provAuthResult.success) {
1124
- logger10.warn("Provisioner authentication failed", {
1281
+ logger10.error("Provisioner authentication failed", {
1125
1282
  service: serviceType,
1126
1283
  provisioner: serviceConfig.provisioner
1127
1284
  });
1128
- } else {
1129
- logger10.info("Provisioner authenticated", {
1130
- service: serviceType,
1131
- user: provAuthResult.data
1132
- });
1285
+ currentProvisioners.clear();
1286
+ return err(provAuthResult.error);
1133
1287
  }
1288
+ logger10.info("Provisioner authenticated", {
1289
+ service: serviceType,
1290
+ user: provAuthResult.data
1291
+ });
1134
1292
  currentProvisioners.set(serviceType, provisioner);
1135
1293
  }
1136
1294
  }
@@ -1265,6 +1423,6 @@ var deploy = {
1265
1423
  }
1266
1424
  };
1267
1425
 
1268
- export { DeployConfigSchema, HaiDeployError, ProviderConfigSchema, deploy, getCredentialsPath, loadCredentials, saveCredential, saveCredentials, scanApp };
1426
+ export { DeployConfigSchema, HaiDeployError, ProviderConfigSchema, deploy };
1269
1427
  //# sourceMappingURL=index.js.map
1270
1428
  //# sourceMappingURL=index.js.map