@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/README.md +22 -4
- package/dist/index.d.ts +24 -150
- package/dist/index.js +259 -101
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { 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})
|
|
36
|
-
deploy_fileUploadFailed: "File upload failed ({status})
|
|
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
|
|
63
|
-
deploy_fileUploadFailed: "\u6587\u4EF6\u4E0A\u4F20\u5931\u8D25\uFF08{status}\uFF09
|
|
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
|
-
|
|
126
|
-
|
|
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
|
-
|
|
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
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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([
|
|
675
|
+
body: new Blob([uploadContent])
|
|
619
676
|
});
|
|
620
677
|
if (!response.ok && response.status !== 409) {
|
|
621
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
849
|
+
name: projectName,
|
|
743
850
|
pg_version: 16
|
|
744
851
|
}
|
|
745
852
|
})
|
|
746
853
|
});
|
|
747
|
-
|
|
748
|
-
|
|
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
|
|
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)
|
|
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)
|
|
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
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
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)
|
|
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
|
-
|
|
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
|
|
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:
|
|
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.
|
|
1281
|
+
logger10.error("Provisioner authentication failed", {
|
|
1125
1282
|
service: serviceType,
|
|
1126
1283
|
provisioner: serviceConfig.provisioner
|
|
1127
1284
|
});
|
|
1128
|
-
|
|
1129
|
-
|
|
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
|
|
1426
|
+
export { DeployConfigSchema, HaiDeployError, ProviderConfigSchema, deploy };
|
|
1269
1427
|
//# sourceMappingURL=index.js.map
|
|
1270
1428
|
//# sourceMappingURL=index.js.map
|