@wraps.dev/cli 2.26.0 → 2.27.0
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/api-lambda.zip +0 -0
- package/dist/cli.js +332 -55
- package/dist/cli.js.map +1 -1
- package/dist/lambda/agent-enforcer/.bundled +1 -1
- package/dist/lambda/event-processor/.bundled +1 -1
- package/dist/lambda/inbound-processor/.bundled +1 -1
- package/dist/lambda/sms-event-processor/.bundled +1 -1
- package/package.json +2 -1
package/dist/api-lambda.zip
CHANGED
|
Binary file
|
package/dist/cli.js
CHANGED
|
@@ -2679,10 +2679,9 @@ var init_costs = __esm({
|
|
|
2679
2679
|
// $0.20 per million API Destination invocations
|
|
2680
2680
|
};
|
|
2681
2681
|
FREE_TIER = {
|
|
2682
|
-
// SES: 3,000
|
|
2683
|
-
//
|
|
2682
|
+
// SES: no free sending tier (the 3,000/mo-for-12-months offer was retired
|
|
2683
|
+
// 2026-07-21; new accounts get generic $200 AWS credits instead)
|
|
2684
2684
|
SES_EMAILS: 0,
|
|
2685
|
-
// Conservative: assume no free tier (most users are past 12 months)
|
|
2686
2685
|
// Lambda: Permanently free tier
|
|
2687
2686
|
LAMBDA_REQUESTS: 1e6,
|
|
2688
2687
|
// 1M requests per month (always free)
|
|
@@ -6084,6 +6083,9 @@ async function findConnectionsWithService(accountId, service) {
|
|
|
6084
6083
|
const accountConnections = await findConnectionsForAccount(accountId);
|
|
6085
6084
|
return accountConnections.filter((conn) => hasService(conn, service));
|
|
6086
6085
|
}
|
|
6086
|
+
function normalizeWebhookBaseUrl(webhookUrl) {
|
|
6087
|
+
return webhookUrl.replace(/\/v1\/ses-events\/?$/, "").replace(/\/+$/, "");
|
|
6088
|
+
}
|
|
6087
6089
|
function buildEmailStackConfig(metadata, region, overrides) {
|
|
6088
6090
|
const emailService = metadata.services.email;
|
|
6089
6091
|
let webhook;
|
|
@@ -6092,7 +6094,10 @@ function buildEmailStackConfig(metadata, region, overrides) {
|
|
|
6092
6094
|
} else if (emailService?.webhookSecret) {
|
|
6093
6095
|
webhook = {
|
|
6094
6096
|
awsAccountNumber: metadata.accountId,
|
|
6095
|
-
webhookSecret: emailService.webhookSecret
|
|
6097
|
+
webhookSecret: emailService.webhookSecret,
|
|
6098
|
+
...emailService.webhookUrl && {
|
|
6099
|
+
webhookUrl: normalizeWebhookBaseUrl(emailService.webhookUrl)
|
|
6100
|
+
}
|
|
6096
6101
|
};
|
|
6097
6102
|
}
|
|
6098
6103
|
const emailConfig = overrides?.emailConfig ?? emailService?.config;
|
|
@@ -11450,7 +11455,7 @@ async function runFullSetup() {
|
|
|
11450
11455
|
console.log(` ${pc9.cyan("https://aws.amazon.com/free")}`);
|
|
11451
11456
|
console.log();
|
|
11452
11457
|
console.log(
|
|
11453
|
-
pc9.dim("
|
|
11458
|
+
pc9.dim(" New AWS accounts get $200 in Free Tier credits (6 months)")
|
|
11454
11459
|
);
|
|
11455
11460
|
console.log();
|
|
11456
11461
|
const hasAccount = await clack7.confirm({
|
|
@@ -16120,9 +16125,38 @@ async function deployEmailStack(config2) {
|
|
|
16120
16125
|
};
|
|
16121
16126
|
}
|
|
16122
16127
|
|
|
16128
|
+
// src/utils/shared/agent-api.ts
|
|
16129
|
+
init_esm_shims();
|
|
16130
|
+
init_config();
|
|
16131
|
+
async function createAgentApiClient(tokenFlag) {
|
|
16132
|
+
const token = await resolveTokenAsync({ token: tokenFlag });
|
|
16133
|
+
if (!token) {
|
|
16134
|
+
return { ok: false, reason: "not-authenticated" };
|
|
16135
|
+
}
|
|
16136
|
+
const apiBase = getApiBaseUrl();
|
|
16137
|
+
const orgHeader = {};
|
|
16138
|
+
const orgs = (await readAuthConfig())?.auth?.organizations;
|
|
16139
|
+
if (orgs && orgs.length === 1) {
|
|
16140
|
+
orgHeader["X-Organization-Id"] = orgs[0].id;
|
|
16141
|
+
}
|
|
16142
|
+
const call = (method) => (path3, body) => fetch(`${apiBase}${path3}`, {
|
|
16143
|
+
method,
|
|
16144
|
+
headers: {
|
|
16145
|
+
Authorization: `Bearer ${token}`,
|
|
16146
|
+
...body ? { "Content-Type": "application/json" } : {},
|
|
16147
|
+
...orgHeader
|
|
16148
|
+
},
|
|
16149
|
+
...body ? { body: JSON.stringify(body) } : {}
|
|
16150
|
+
});
|
|
16151
|
+
return { ok: true, get: call("GET"), post: call("POST") };
|
|
16152
|
+
}
|
|
16153
|
+
async function parseAgentApiError(resp) {
|
|
16154
|
+
const data = await resp.json().catch(() => ({}));
|
|
16155
|
+
return data.error ?? `HTTP ${resp.status}`;
|
|
16156
|
+
}
|
|
16157
|
+
|
|
16123
16158
|
// src/commands/email/agent.ts
|
|
16124
16159
|
init_aws();
|
|
16125
|
-
init_config();
|
|
16126
16160
|
init_fs();
|
|
16127
16161
|
init_json_output();
|
|
16128
16162
|
init_metadata();
|
|
@@ -16169,32 +16203,6 @@ var DEFAULT_AGENT_POLICY = {
|
|
|
16169
16203
|
allowedRecipientDomains: []
|
|
16170
16204
|
};
|
|
16171
16205
|
var AGENT_NAME_PATTERN = /^[a-z0-9][a-z0-9-]{0,51}$/;
|
|
16172
|
-
async function createAgentApiClient(tokenFlag) {
|
|
16173
|
-
const token = await resolveTokenAsync({ token: tokenFlag });
|
|
16174
|
-
if (!token) {
|
|
16175
|
-
return { ok: false, reason: "not-authenticated" };
|
|
16176
|
-
}
|
|
16177
|
-
const apiBase = getApiBaseUrl();
|
|
16178
|
-
const orgHeader = {};
|
|
16179
|
-
const orgs = (await readAuthConfig())?.auth?.organizations;
|
|
16180
|
-
if (orgs && orgs.length === 1) {
|
|
16181
|
-
orgHeader["X-Organization-Id"] = orgs[0].id;
|
|
16182
|
-
}
|
|
16183
|
-
const call = (method) => (path3, body) => fetch(`${apiBase}${path3}`, {
|
|
16184
|
-
method,
|
|
16185
|
-
headers: {
|
|
16186
|
-
Authorization: `Bearer ${token}`,
|
|
16187
|
-
...body ? { "Content-Type": "application/json" } : {},
|
|
16188
|
-
...orgHeader
|
|
16189
|
-
},
|
|
16190
|
-
...body ? { body: JSON.stringify(body) } : {}
|
|
16191
|
-
});
|
|
16192
|
-
return { ok: true, get: call("GET"), post: call("POST") };
|
|
16193
|
-
}
|
|
16194
|
-
async function parseError(resp) {
|
|
16195
|
-
const data = await resp.json().catch(() => ({}));
|
|
16196
|
-
return data.error ?? `HTTP ${resp.status}`;
|
|
16197
|
-
}
|
|
16198
16206
|
async function agentCreate(options) {
|
|
16199
16207
|
if (!isJsonMode()) {
|
|
16200
16208
|
clack15.intro(pc17.bold("Create Agent Mailbox"));
|
|
@@ -16250,7 +16258,7 @@ Sign in first: ${pc17.cyan("wraps auth login")}
|
|
|
16250
16258
|
async () => {
|
|
16251
16259
|
const resp = await api.get("/v1/connections");
|
|
16252
16260
|
if (!resp.ok) {
|
|
16253
|
-
throw new Error(await
|
|
16261
|
+
throw new Error(await parseAgentApiError(resp));
|
|
16254
16262
|
}
|
|
16255
16263
|
const data = await resp.json();
|
|
16256
16264
|
return data.connections.some((c) => c.accountId === identity.accountId);
|
|
@@ -16396,7 +16404,7 @@ Connect (or reconnect) first so it is stored locally:
|
|
|
16396
16404
|
return { kind: "created", agent: await resp.json() };
|
|
16397
16405
|
}
|
|
16398
16406
|
if (resp.status !== 409) {
|
|
16399
|
-
throw new Error(await
|
|
16407
|
+
throw new Error(await parseAgentApiError(resp));
|
|
16400
16408
|
}
|
|
16401
16409
|
const listResp = await api.get("/v1/agents");
|
|
16402
16410
|
const existing = listResp.ok ? (await listResp.json()).agents.find(
|
|
@@ -16567,7 +16575,7 @@ ${suggestion}
|
|
|
16567
16575
|
awsAccountId: identity.accountId
|
|
16568
16576
|
});
|
|
16569
16577
|
if (!resp.ok) {
|
|
16570
|
-
throw new Error(await
|
|
16578
|
+
throw new Error(await parseAgentApiError(resp));
|
|
16571
16579
|
}
|
|
16572
16580
|
});
|
|
16573
16581
|
if (isJsonMode()) {
|
|
@@ -16630,7 +16638,7 @@ async function agentList(options) {
|
|
|
16630
16638
|
}
|
|
16631
16639
|
const resp = await api.get("/v1/agents");
|
|
16632
16640
|
if (!resp.ok) {
|
|
16633
|
-
const message = await
|
|
16641
|
+
const message = await parseAgentApiError(resp);
|
|
16634
16642
|
if (isJsonMode()) {
|
|
16635
16643
|
jsonError("email.agent.list", { code: "API_ERROR", message });
|
|
16636
16644
|
} else {
|
|
@@ -16670,7 +16678,7 @@ async function agentKill(options) {
|
|
|
16670
16678
|
}
|
|
16671
16679
|
const listResp = await api.get("/v1/agents");
|
|
16672
16680
|
if (!listResp.ok) {
|
|
16673
|
-
const message = await
|
|
16681
|
+
const message = await parseAgentApiError(listResp);
|
|
16674
16682
|
if (isJsonMode()) {
|
|
16675
16683
|
jsonError("email.agent.kill", { code: "API_ERROR", message });
|
|
16676
16684
|
} else {
|
|
@@ -16729,7 +16737,7 @@ async function agentKill(options) {
|
|
|
16729
16737
|
}
|
|
16730
16738
|
const resp = await api.post(`/v1/agents/${target.id}/kill`);
|
|
16731
16739
|
if (!resp.ok) {
|
|
16732
|
-
const message = await
|
|
16740
|
+
const message = await parseAgentApiError(resp);
|
|
16733
16741
|
if (isJsonMode()) {
|
|
16734
16742
|
jsonError("email.agent.kill", { code: "API_ERROR", message });
|
|
16735
16743
|
} else {
|
|
@@ -20535,6 +20543,9 @@ ${pc20.dim("Example:")}`);
|
|
|
20535
20543
|
init_esm_shims();
|
|
20536
20544
|
init_events();
|
|
20537
20545
|
init_route53();
|
|
20546
|
+
import * as clack19 from "@clack/prompts";
|
|
20547
|
+
import * as pulumi18 from "@pulumi/pulumi";
|
|
20548
|
+
import pc21 from "picocolors";
|
|
20538
20549
|
init_aws();
|
|
20539
20550
|
init_errors();
|
|
20540
20551
|
init_fs();
|
|
@@ -20542,9 +20553,6 @@ init_json_output();
|
|
|
20542
20553
|
init_metadata();
|
|
20543
20554
|
init_output();
|
|
20544
20555
|
init_pulumi();
|
|
20545
|
-
import * as clack19 from "@clack/prompts";
|
|
20546
|
-
import * as pulumi18 from "@pulumi/pulumi";
|
|
20547
|
-
import pc21 from "picocolors";
|
|
20548
20556
|
async function getEmailIdentityInfo(domain, region) {
|
|
20549
20557
|
try {
|
|
20550
20558
|
const { SESv2Client: SESv2Client10, GetEmailIdentityCommand: GetEmailIdentityCommand7 } = await import("@aws-sdk/client-sesv2");
|
|
@@ -20563,6 +20571,30 @@ async function getEmailIdentityInfo(domain, region) {
|
|
|
20563
20571
|
throw error;
|
|
20564
20572
|
}
|
|
20565
20573
|
}
|
|
20574
|
+
function selectAgentsToDestroy(emailConfig) {
|
|
20575
|
+
const agentsConfig = emailConfig?.agents;
|
|
20576
|
+
if (!agentsConfig?.enabled) {
|
|
20577
|
+
return [];
|
|
20578
|
+
}
|
|
20579
|
+
return (agentsConfig.agents ?? []).map((a) => ({
|
|
20580
|
+
id: a.id,
|
|
20581
|
+
name: a.name,
|
|
20582
|
+
emailAddress: a.emailAddress
|
|
20583
|
+
}));
|
|
20584
|
+
}
|
|
20585
|
+
function buildAgentDestroySummaryLines(agents) {
|
|
20586
|
+
if (agents.length === 0) {
|
|
20587
|
+
return [];
|
|
20588
|
+
}
|
|
20589
|
+
const addresses = agents.map((a) => a.emailAddress).join(", ");
|
|
20590
|
+
return [
|
|
20591
|
+
"",
|
|
20592
|
+
`Agent mailboxes (${agents.length}): ${addresses}`,
|
|
20593
|
+
pc21.yellow(
|
|
20594
|
+
"Agent scoped credentials are revoked (IAM users wraps-agent-* deleted), the wraps-agent-enforcer Lambda and wraps-email-agent-policy table are destroyed, and any pending approvals become undeliverable."
|
|
20595
|
+
)
|
|
20596
|
+
];
|
|
20597
|
+
}
|
|
20566
20598
|
async function emailDestroy(options) {
|
|
20567
20599
|
const startTime = Date.now();
|
|
20568
20600
|
if (isJsonMode() && !options.force) {
|
|
@@ -20623,6 +20655,7 @@ async function emailDestroy(options) {
|
|
|
20623
20655
|
const emailConfig = emailService?.config;
|
|
20624
20656
|
const domain = emailConfig?.domain;
|
|
20625
20657
|
const storedStackName = emailService?.pulumiStackName;
|
|
20658
|
+
const agentsToDestroy = selectAgentsToDestroy(emailConfig);
|
|
20626
20659
|
if (!(options.force || options.preview)) {
|
|
20627
20660
|
const additionalDomainNames = (emailConfig?.additionalDomains ?? []).map((d) => d.domain).filter(Boolean);
|
|
20628
20661
|
const domainList = [domain, ...additionalDomainNames].filter(Boolean);
|
|
@@ -20635,6 +20668,7 @@ async function emailDestroy(options) {
|
|
|
20635
20668
|
"Event streaming stops: the Wraps dashboard email timeline and event history for THIS ENTIRE AWS ACCOUNT stop receiving events. Historical events in DynamoDB are destroyed with the table. This cannot be undone."
|
|
20636
20669
|
)
|
|
20637
20670
|
];
|
|
20671
|
+
summaryLines.push(...buildAgentDestroySummaryLines(agentsToDestroy));
|
|
20638
20672
|
if (emailService?.webhookSecret) {
|
|
20639
20673
|
summaryLines.push(
|
|
20640
20674
|
"",
|
|
@@ -20731,6 +20765,13 @@ async function emailDestroy(options) {
|
|
|
20731
20765
|
);
|
|
20732
20766
|
}
|
|
20733
20767
|
}
|
|
20768
|
+
const agentSummaryLines = buildAgentDestroySummaryLines(agentsToDestroy);
|
|
20769
|
+
if (agentSummaryLines.length > 0) {
|
|
20770
|
+
clack19.note(
|
|
20771
|
+
agentSummaryLines.filter(Boolean).join("\n"),
|
|
20772
|
+
"Agent mailboxes"
|
|
20773
|
+
);
|
|
20774
|
+
}
|
|
20734
20775
|
clack19.outro(
|
|
20735
20776
|
pc21.green("Preview complete. Run without --preview to destroy.")
|
|
20736
20777
|
);
|
|
@@ -20850,6 +20891,33 @@ async function emailDestroy(options) {
|
|
|
20850
20891
|
}
|
|
20851
20892
|
} catch {
|
|
20852
20893
|
}
|
|
20894
|
+
if (agentsToDestroy.length > 0) {
|
|
20895
|
+
try {
|
|
20896
|
+
const agentApi = await createAgentApiClient();
|
|
20897
|
+
if (agentApi.ok) {
|
|
20898
|
+
let killed = 0;
|
|
20899
|
+
for (const agent of agentsToDestroy) {
|
|
20900
|
+
try {
|
|
20901
|
+
const resp = await agentApi.post(`/v1/agents/${agent.id}/kill`);
|
|
20902
|
+
if (resp.ok) {
|
|
20903
|
+
killed++;
|
|
20904
|
+
}
|
|
20905
|
+
} catch {
|
|
20906
|
+
}
|
|
20907
|
+
}
|
|
20908
|
+
if (!isJsonMode()) {
|
|
20909
|
+
clack19.log.info(
|
|
20910
|
+
`Marked ${killed}/${agentsToDestroy.length} agent(s) as killed in the Wraps dashboard.`
|
|
20911
|
+
);
|
|
20912
|
+
}
|
|
20913
|
+
} else if (!isJsonMode()) {
|
|
20914
|
+
clack19.log.warn(
|
|
20915
|
+
"Could not reach the Wraps Platform to update agent status \u2014 your agents will still show as active in the dashboard. Kill them there: wraps email agent kill (or the dashboard)."
|
|
20916
|
+
);
|
|
20917
|
+
}
|
|
20918
|
+
} catch {
|
|
20919
|
+
}
|
|
20920
|
+
}
|
|
20853
20921
|
await deleteConnectionMetadata(identity.accountId, region);
|
|
20854
20922
|
progress.stop();
|
|
20855
20923
|
if (isJsonMode()) {
|
|
@@ -20857,7 +20925,12 @@ async function emailDestroy(options) {
|
|
|
20857
20925
|
destroyed: !destroyFailed,
|
|
20858
20926
|
region,
|
|
20859
20927
|
dns_cleaned: shouldCleanDNS,
|
|
20860
|
-
partial_failure: destroyFailed
|
|
20928
|
+
partial_failure: destroyFailed,
|
|
20929
|
+
agents: agentsToDestroy.map((a) => ({
|
|
20930
|
+
id: a.id,
|
|
20931
|
+
name: a.name,
|
|
20932
|
+
emailAddress: a.emailAddress
|
|
20933
|
+
}))
|
|
20861
20934
|
});
|
|
20862
20935
|
trackServiceRemoved("email", {
|
|
20863
20936
|
reason: "user_initiated",
|
|
@@ -35258,6 +35331,128 @@ import { dirname as dirname5, join as join20 } from "path";
|
|
|
35258
35331
|
import { fileURLToPath as fileURLToPath5 } from "url";
|
|
35259
35332
|
import * as clack45 from "@clack/prompts";
|
|
35260
35333
|
import * as pulumi28 from "@pulumi/pulumi";
|
|
35334
|
+
|
|
35335
|
+
// ../db/dist/connection-url.js
|
|
35336
|
+
init_esm_shims();
|
|
35337
|
+
|
|
35338
|
+
// ../db/dist/connection-url-CJ0MYp2V.js
|
|
35339
|
+
init_esm_shims();
|
|
35340
|
+
var POSTGRES_SCHEMES = ["postgres:", "postgresql:"];
|
|
35341
|
+
var TRUST_STORE_SENTINELS = /* @__PURE__ */ new Set(["system", "os"]);
|
|
35342
|
+
var TLS_OPT_OUT = /* @__PURE__ */ new Set(["disable", "no-verify"]);
|
|
35343
|
+
function pinVerification(parsed) {
|
|
35344
|
+
const modes = parsed.searchParams.getAll("sslmode");
|
|
35345
|
+
const effective = modes.at(-1)?.toLowerCase();
|
|
35346
|
+
const hadCompat = parsed.searchParams.has("uselibpqcompat");
|
|
35347
|
+
if (effective && TLS_OPT_OUT.has(effective)) {
|
|
35348
|
+
parsed.searchParams.set("sslmode", effective);
|
|
35349
|
+
parsed.searchParams.delete("uselibpqcompat");
|
|
35350
|
+
return;
|
|
35351
|
+
}
|
|
35352
|
+
const alreadyVerifying = modes.length === 1 && effective === "verify-full" && !hadCompat;
|
|
35353
|
+
parsed.searchParams.set("sslmode", "verify-full");
|
|
35354
|
+
parsed.searchParams.delete("uselibpqcompat");
|
|
35355
|
+
return alreadyVerifying ? void 0 : "Pinned sslmode=verify-full \u2014 without the CA reference the original mode would have connected unverified (or in cleartext).";
|
|
35356
|
+
}
|
|
35357
|
+
function normalizeDatabaseUrl(raw) {
|
|
35358
|
+
const url = raw.trim();
|
|
35359
|
+
const notes = [];
|
|
35360
|
+
let parsed;
|
|
35361
|
+
try {
|
|
35362
|
+
parsed = new URL(url);
|
|
35363
|
+
} catch {
|
|
35364
|
+
return {
|
|
35365
|
+
url,
|
|
35366
|
+
notes
|
|
35367
|
+
};
|
|
35368
|
+
}
|
|
35369
|
+
const certKey = [...parsed.searchParams.keys()].find((key) => key.toLowerCase() === "sslrootcert");
|
|
35370
|
+
const sslRootCert = certKey ? parsed.searchParams.get(certKey) : void 0;
|
|
35371
|
+
if (!(sslRootCert && TRUST_STORE_SENTINELS.has(sslRootCert.toLowerCase()))) return {
|
|
35372
|
+
url,
|
|
35373
|
+
notes
|
|
35374
|
+
};
|
|
35375
|
+
parsed.searchParams.delete(certKey);
|
|
35376
|
+
notes.push(`Dropped sslrootcert=${sslRootCert} \u2014 node-postgres would read it as a filename. TLS stays on and the certificate is verified against Node's bundled CA set (set NODE_EXTRA_CA_CERTS for a private CA).`);
|
|
35377
|
+
const pinNote = pinVerification(parsed);
|
|
35378
|
+
if (pinNote) notes.push(pinNote);
|
|
35379
|
+
return {
|
|
35380
|
+
url: parsed.toString(),
|
|
35381
|
+
notes
|
|
35382
|
+
};
|
|
35383
|
+
}
|
|
35384
|
+
function describeMigrationError(error) {
|
|
35385
|
+
const parts = [];
|
|
35386
|
+
let current = error;
|
|
35387
|
+
const seen = /* @__PURE__ */ new Set();
|
|
35388
|
+
while (current instanceof Error && !seen.has(current)) {
|
|
35389
|
+
seen.add(current);
|
|
35390
|
+
const code = current.code;
|
|
35391
|
+
const message = current.message.startsWith("Failed query:") ? current.message.split("\n")[0] : current.message;
|
|
35392
|
+
const described = code ? `${message} (${code})` : message;
|
|
35393
|
+
if (!parts.includes(described)) parts.push(described);
|
|
35394
|
+
current = current.cause;
|
|
35395
|
+
}
|
|
35396
|
+
return parts.length > 0 ? parts.join(" \u2014 ") : String(error);
|
|
35397
|
+
}
|
|
35398
|
+
var HINTS = [
|
|
35399
|
+
{
|
|
35400
|
+
match: /\b(ENOENT|EACCES)\b/,
|
|
35401
|
+
hint: "A file named in DATABASE_URL (usually sslrootcert) is missing or unreadable. Point it at a readable CA file, or set NODE_EXTRA_CA_CERTS if the certificate chains to a private CA."
|
|
35402
|
+
},
|
|
35403
|
+
{
|
|
35404
|
+
match: /password authentication failed|28P01|28000/i,
|
|
35405
|
+
hint: "The credentials in DATABASE_URL were rejected. Check the user and password."
|
|
35406
|
+
},
|
|
35407
|
+
{
|
|
35408
|
+
match: /CREATE SCHEMA|permission denied for database|must be owner of database/i,
|
|
35409
|
+
hint: "The database user cannot create schemas. Grant it CREATE on the database (GRANT CREATE ON DATABASE <db> TO <user>), or use an owner role."
|
|
35410
|
+
},
|
|
35411
|
+
{
|
|
35412
|
+
match: /permission denied|42501/i,
|
|
35413
|
+
hint: "The database user lacks a privilege this migration needs. The failing statement above names the object \u2014 grant on that, or run migrations as the database owner."
|
|
35414
|
+
},
|
|
35415
|
+
{
|
|
35416
|
+
match: /database .* does not exist|3D000/i,
|
|
35417
|
+
hint: "The database named in DATABASE_URL does not exist. Create it first."
|
|
35418
|
+
},
|
|
35419
|
+
{
|
|
35420
|
+
match: /no pg_hba\.conf entry|SSL|self[- ]signed|certificate/i,
|
|
35421
|
+
hint: "TLS negotiation failed. Most managed Postgres requires ?sslmode=verify-full on DATABASE_URL."
|
|
35422
|
+
},
|
|
35423
|
+
{
|
|
35424
|
+
match: /ECONNREFUSED|ETIMEDOUT|ENOTFOUND|EAI_AGAIN/i,
|
|
35425
|
+
hint: "Could not reach the database host. Check the host and port, and that this machine is allowed through the firewall or IP allowlist."
|
|
35426
|
+
}
|
|
35427
|
+
];
|
|
35428
|
+
function migrationHint(description) {
|
|
35429
|
+
return HINTS.find(({ match }) => match.test(description))?.hint;
|
|
35430
|
+
}
|
|
35431
|
+
function describeMigrationFailure(error) {
|
|
35432
|
+
const description = describeMigrationError(error);
|
|
35433
|
+
const hint = migrationHint(description);
|
|
35434
|
+
return hint ? `Database migrations failed: ${description}
|
|
35435
|
+
${hint}` : `Database migrations failed: ${description}`;
|
|
35436
|
+
}
|
|
35437
|
+
function assertPostgresUrl(raw) {
|
|
35438
|
+
const url = raw.trim();
|
|
35439
|
+
if (!url) throw new Error("DATABASE_URL is empty.");
|
|
35440
|
+
if (!url.includes("://")) {
|
|
35441
|
+
if (/(^|\s)(host|dbname|user)=/.test(url)) throw new Error("DATABASE_URL is a libpq keyword/value connection string, which node-postgres cannot parse. Use the URI form instead: postgresql://USER:PASSWORD@HOST:5432/DATABASE?sslmode=verify-full");
|
|
35442
|
+
throw new Error("DATABASE_URL is not a connection URI. Expected postgresql://USER:PASSWORD@HOST:5432/DATABASE");
|
|
35443
|
+
}
|
|
35444
|
+
let parsed;
|
|
35445
|
+
try {
|
|
35446
|
+
parsed = new URL(url);
|
|
35447
|
+
} catch {
|
|
35448
|
+
throw new Error("DATABASE_URL could not be parsed as a URI. If the password contains @ / : or ?, percent-encode it.");
|
|
35449
|
+
}
|
|
35450
|
+
if (POSTGRES_SCHEMES.includes(parsed.protocol)) return;
|
|
35451
|
+
if (parsed.protocol === "mysql:" || parsed.protocol === "mysql2:") throw new Error(`DATABASE_URL points at MySQL (${parsed.protocol}//). Wraps requires PostgreSQL \u2014 its schema uses Postgres types, enums and partial indexes that have no MySQL equivalent.`);
|
|
35452
|
+
throw new Error(`DATABASE_URL has an unsupported scheme (${parsed.protocol}//). Wraps requires PostgreSQL: postgresql://USER:PASSWORD@HOST:5432/DATABASE`);
|
|
35453
|
+
}
|
|
35454
|
+
|
|
35455
|
+
// src/commands/selfhost/deploy.ts
|
|
35261
35456
|
import pc48 from "picocolors";
|
|
35262
35457
|
|
|
35263
35458
|
// src/infrastructure/selfhost-stack.ts
|
|
@@ -35514,6 +35709,29 @@ function buildNeonProjectName(accountId, region) {
|
|
|
35514
35709
|
return `wraps-selfhost-${accountId}-${region}`;
|
|
35515
35710
|
}
|
|
35516
35711
|
|
|
35712
|
+
// src/utils/selfhost/variant.ts
|
|
35713
|
+
init_esm_shims();
|
|
35714
|
+
var SHARED_SCHEDULER_ROLE = "wraps-selfhost-scheduler-role";
|
|
35715
|
+
async function detectSelfhostVariant(region) {
|
|
35716
|
+
try {
|
|
35717
|
+
const { LambdaClient: LambdaClient3, GetFunctionCommand } = await import("@aws-sdk/client-lambda");
|
|
35718
|
+
const lambda7 = new LambdaClient3({ region });
|
|
35719
|
+
await lambda7.send(
|
|
35720
|
+
new GetFunctionCommand({ FunctionName: SELFHOST_API_FUNCTION_NAME })
|
|
35721
|
+
);
|
|
35722
|
+
return "pulumi";
|
|
35723
|
+
} catch {
|
|
35724
|
+
}
|
|
35725
|
+
try {
|
|
35726
|
+
const { IAMClient: IAMClient5, GetRoleCommand: GetRoleCommand3 } = await import("@aws-sdk/client-iam");
|
|
35727
|
+
const iam13 = new IAMClient5({ region });
|
|
35728
|
+
await iam13.send(new GetRoleCommand3({ RoleName: SHARED_SCHEDULER_ROLE }));
|
|
35729
|
+
return "sst";
|
|
35730
|
+
} catch {
|
|
35731
|
+
return null;
|
|
35732
|
+
}
|
|
35733
|
+
}
|
|
35734
|
+
|
|
35517
35735
|
// src/commands/selfhost/deploy.ts
|
|
35518
35736
|
init_aws();
|
|
35519
35737
|
init_errors();
|
|
@@ -35571,17 +35789,42 @@ async function selfhostDeploy(options) {
|
|
|
35571
35789
|
region
|
|
35572
35790
|
);
|
|
35573
35791
|
if (existingMetadata?.services?.selfhost) {
|
|
35792
|
+
const existing = existingMetadata.services.selfhost;
|
|
35574
35793
|
clack45.log.warn(
|
|
35575
35794
|
`Self-hosted deployment already exists for account ${pc48.cyan(identity.accountId)} in region ${pc48.cyan(region)}`
|
|
35576
35795
|
);
|
|
35796
|
+
clack45.log.info(`API URL: ${pc48.cyan(existing.apiUrl)}`);
|
|
35797
|
+
clack45.log.info(`Deployed: ${existing.deployedAt}`);
|
|
35798
|
+
clack45.log.info(
|
|
35799
|
+
existing.variant === "sst" ? `This is a full-platform (SST) deployment. To update: run ${pc48.cyan("pnpm selfhost:upgrade")} from your fork.` : `To update: run ${pc48.cyan("wraps selfhost upgrade")}`
|
|
35800
|
+
);
|
|
35801
|
+
process.exit(0);
|
|
35802
|
+
}
|
|
35803
|
+
const deployedVariant = await progress.execute(
|
|
35804
|
+
"Checking for existing selfhost deployments",
|
|
35805
|
+
async () => detectSelfhostVariant(region)
|
|
35806
|
+
);
|
|
35807
|
+
if (deployedVariant === "sst") {
|
|
35808
|
+
progress.stop();
|
|
35809
|
+
clack45.log.error(
|
|
35810
|
+
"A full-platform (SST) selfhost deployment already exists in this AWS account."
|
|
35811
|
+
);
|
|
35577
35812
|
clack45.log.info(
|
|
35578
|
-
`
|
|
35813
|
+
`The two selfhost variants share IAM resources and cannot coexist.
|
|
35814
|
+
To update it: run ${pc48.cyan("pnpm selfhost:upgrade")} from your fork.
|
|
35815
|
+
To replace it with the API-only control plane: run ${pc48.cyan("pnpm selfhost:destroy")} from your fork first.`
|
|
35816
|
+
);
|
|
35817
|
+
process.exit(1);
|
|
35818
|
+
}
|
|
35819
|
+
if (deployedVariant === "pulumi") {
|
|
35820
|
+
progress.stop();
|
|
35821
|
+
clack45.log.error(
|
|
35822
|
+
"An API-only selfhost control plane already exists in this AWS account, but no local metadata was found."
|
|
35579
35823
|
);
|
|
35580
35824
|
clack45.log.info(
|
|
35581
|
-
`
|
|
35825
|
+
`It was deployed from another machine. Run ${pc48.cyan("wraps selfhost upgrade")} there, or remove the existing stack before redeploying from this one.`
|
|
35582
35826
|
);
|
|
35583
|
-
|
|
35584
|
-
process.exit(0);
|
|
35827
|
+
process.exit(1);
|
|
35585
35828
|
}
|
|
35586
35829
|
let resolvedDatabaseUrl = options.databaseUrl;
|
|
35587
35830
|
let resolvedNeonProjectId;
|
|
@@ -35709,6 +35952,7 @@ async function selfhostDeploy(options) {
|
|
|
35709
35952
|
);
|
|
35710
35953
|
}
|
|
35711
35954
|
const databaseUrl = resolvedDatabaseUrl;
|
|
35955
|
+
assertPostgresUrl(databaseUrl);
|
|
35712
35956
|
const unsubscribeSecret = randomBytes6(32).toString("hex");
|
|
35713
35957
|
const betterAuthSecret = randomBytes6(32).toString("hex");
|
|
35714
35958
|
const selfhostConfig = {
|
|
@@ -35734,19 +35978,26 @@ async function selfhostDeploy(options) {
|
|
|
35734
35978
|
deployedAt,
|
|
35735
35979
|
pulumiStackName: stackName,
|
|
35736
35980
|
config: selfhostConfig,
|
|
35737
|
-
apiUrl: priorApiUrl
|
|
35981
|
+
apiUrl: priorApiUrl,
|
|
35738
35982
|
// Refreshed from Pulumi outputs after deploy succeeds
|
|
35983
|
+
variant: "pulumi"
|
|
35739
35984
|
};
|
|
35740
35985
|
savedMetadata.timestamp = deployedAt;
|
|
35741
35986
|
await saveConnectionMetadata(savedMetadata);
|
|
35987
|
+
const { url: migrationUrl, notes: connectionNotes } = normalizeDatabaseUrl(databaseUrl);
|
|
35988
|
+
for (const note22 of connectionNotes) {
|
|
35989
|
+
progress.info(note22);
|
|
35990
|
+
}
|
|
35742
35991
|
await progress.execute("Running database migrations", async () => {
|
|
35743
35992
|
const { Pool } = await import("pg");
|
|
35744
35993
|
const { drizzle } = await import("drizzle-orm/node-postgres");
|
|
35745
35994
|
const { migrate } = await import("drizzle-orm/node-postgres/migrator");
|
|
35746
|
-
const pool = new Pool({ connectionString:
|
|
35995
|
+
const pool = new Pool({ connectionString: migrationUrl });
|
|
35747
35996
|
const db = drizzle(pool);
|
|
35748
35997
|
try {
|
|
35749
35998
|
await migrate(db, { migrationsFolder: bundledMigrationsDir });
|
|
35999
|
+
} catch (error) {
|
|
36000
|
+
throw new Error(describeMigrationFailure(error));
|
|
35750
36001
|
} finally {
|
|
35751
36002
|
await pool.end();
|
|
35752
36003
|
}
|
|
@@ -36018,7 +36269,7 @@ Run ${pc50.cyan("wraps selfhost deploy")} to deploy the self-hosted control plan
|
|
|
36018
36269
|
);
|
|
36019
36270
|
console.log(
|
|
36020
36271
|
`
|
|
36021
|
-
The deployment may have failed partway through.
|
|
36272
|
+
The deployment may have failed partway through. Run ${pc50.cyan("pnpm selfhost:upgrade")} from your fork (SST deployments) or re-run ${pc50.cyan("wraps selfhost deploy")} (API-only deployments) to complete it.
|
|
36022
36273
|
`
|
|
36023
36274
|
);
|
|
36024
36275
|
process.exit(1);
|
|
@@ -36329,15 +36580,30 @@ init_output();
|
|
|
36329
36580
|
init_region_resolver();
|
|
36330
36581
|
function displaySelfhostStatus(options) {
|
|
36331
36582
|
const lines = [];
|
|
36332
|
-
|
|
36583
|
+
const incomplete = !(options.apiUrl && options.appUrl);
|
|
36584
|
+
lines.push(
|
|
36585
|
+
incomplete ? pc53.bold(pc53.yellow("Self-Hosted Control Plane Incomplete")) : pc53.bold(pc53.green("Self-Hosted Control Plane Active"))
|
|
36586
|
+
);
|
|
36587
|
+
if (incomplete) {
|
|
36588
|
+
lines.push(
|
|
36589
|
+
pc53.yellow(
|
|
36590
|
+
`A previous deploy did not finish. Run ${pc53.cyan("pnpm selfhost:upgrade")} from your fork to complete it.`
|
|
36591
|
+
)
|
|
36592
|
+
);
|
|
36593
|
+
}
|
|
36333
36594
|
lines.push("");
|
|
36334
36595
|
lines.push(pc53.bold("API"));
|
|
36335
|
-
lines.push(` URL: ${pc53.cyan(options.apiUrl)}`);
|
|
36596
|
+
lines.push(` URL: ${pc53.cyan(options.apiUrl || "(not deployed)")}`);
|
|
36336
36597
|
lines.push(` Region: ${pc53.cyan(options.region)}`);
|
|
36337
36598
|
lines.push(` Deployed: ${pc53.dim(options.deployedAt)}`);
|
|
36599
|
+
if (options.variant) {
|
|
36600
|
+
lines.push(
|
|
36601
|
+
` Type: ${pc53.cyan(options.variant === "sst" ? "Full platform (SST, deployed from fork)" : "API-only control plane (CLI)")}`
|
|
36602
|
+
);
|
|
36603
|
+
}
|
|
36338
36604
|
lines.push("");
|
|
36339
36605
|
lines.push(pc53.bold("Configuration"));
|
|
36340
|
-
lines.push(` App URL: ${pc53.cyan(options.appUrl)}`);
|
|
36606
|
+
lines.push(` App URL: ${pc53.cyan(options.appUrl || "(not deployed)")}`);
|
|
36341
36607
|
lines.push(` License Key: ${pc53.dim(`${options.licenseKeyPrefix}...`)}`);
|
|
36342
36608
|
if (options.neonProjectId) {
|
|
36343
36609
|
lines.push(` Neon Project: ${pc53.dim(options.neonProjectId)}`);
|
|
@@ -36366,7 +36632,8 @@ async function selfhostStatus(options) {
|
|
|
36366
36632
|
clack50.log.error("No self-hosted deployment found");
|
|
36367
36633
|
console.log(
|
|
36368
36634
|
`
|
|
36369
|
-
Run ${pc53.cyan("
|
|
36635
|
+
Run ${pc53.cyan("pnpm selfhost:deploy")} from your fork to deploy the full platform (API + dashboard),
|
|
36636
|
+
or ${pc53.cyan("wraps selfhost deploy")} for the API-only control plane.
|
|
36370
36637
|
`
|
|
36371
36638
|
);
|
|
36372
36639
|
process.exit(1);
|
|
@@ -36375,13 +36642,23 @@ Run ${pc53.cyan("wraps selfhost deploy")} to deploy the self-hosted control plan
|
|
|
36375
36642
|
const selfhostService = metadata.services.selfhost;
|
|
36376
36643
|
const config2 = selfhostService.config;
|
|
36377
36644
|
const apiUrl = await reconcileSelfhostApiUrl(metadata, region) ?? selfhostService.apiUrl;
|
|
36645
|
+
if (!selfhostService.variant) {
|
|
36646
|
+
const inferred = await detectSelfhostVariant(region);
|
|
36647
|
+
if (inferred) {
|
|
36648
|
+
selfhostService.variant = inferred;
|
|
36649
|
+
metadata.timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
36650
|
+
await saveConnectionMetadata(metadata);
|
|
36651
|
+
}
|
|
36652
|
+
}
|
|
36653
|
+
const variant = selfhostService.variant;
|
|
36378
36654
|
const statusData = {
|
|
36379
36655
|
region,
|
|
36380
36656
|
apiUrl,
|
|
36381
36657
|
deployedAt: selfhostService.deployedAt,
|
|
36382
36658
|
neonProjectId: config2.neonProjectId,
|
|
36383
36659
|
appUrl: config2.appUrl,
|
|
36384
|
-
licenseKeyPrefix: config2.licenseKey.slice(0, 12)
|
|
36660
|
+
licenseKeyPrefix: config2.licenseKey.slice(0, 12),
|
|
36661
|
+
variant
|
|
36385
36662
|
};
|
|
36386
36663
|
if (isJsonMode()) {
|
|
36387
36664
|
jsonSuccess("selfhost.status", statusData);
|
|
@@ -36391,7 +36668,7 @@ Run ${pc53.cyan("wraps selfhost deploy")} to deploy the self-hosted control plan
|
|
|
36391
36668
|
console.log("");
|
|
36392
36669
|
clack50.log.info(pc53.bold("Commands:"));
|
|
36393
36670
|
console.log(
|
|
36394
|
-
` ${pc53.cyan("wraps selfhost upgrade")} - Rebuild and redeploy the API Lambda`
|
|
36671
|
+
variant === "sst" ? ` ${pc53.cyan("pnpm selfhost:upgrade")} (from your fork) - Redeploy the platform and run migrations` : ` ${pc53.cyan("wraps selfhost upgrade")} - Rebuild and redeploy the API Lambda`
|
|
36395
36672
|
);
|
|
36396
36673
|
trackCommand("selfhost:status", {
|
|
36397
36674
|
success: true,
|