run402 3.5.3 → 3.5.5
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/cli.mjs +20 -0
- package/core-dist/config.js +86 -4
- package/lib/archives.mjs +55 -0
- package/lib/cache.mjs +10 -9
- package/lib/cloud.mjs +254 -0
- package/lib/config.mjs +27 -2
- package/lib/core.mjs +122 -0
- package/lib/deploy-v2.mjs +95 -67
- package/lib/doctor-source-scan.mjs +1 -1
- package/lib/functions.mjs +2 -1
- package/lib/init.mjs +106 -1
- package/lib/next-actions.mjs +30 -15
- package/lib/projects.mjs +13 -6
- package/lib/secrets.mjs +3 -2
- package/lib/status.mjs +25 -3
- package/lib/subdomains.mjs +2 -1
- package/package.json +1 -1
- package/sdk/core-dist/config.js +86 -4
- package/sdk/dist/credentials.d.ts +1 -1
- package/sdk/dist/credentials.d.ts.map +1 -1
- package/sdk/dist/index.d.ts +4 -0
- package/sdk/dist/index.d.ts.map +1 -1
- package/sdk/dist/index.js +4 -0
- package/sdk/dist/index.js.map +1 -1
- package/sdk/dist/namespaces/archives.d.ts +12 -0
- package/sdk/dist/namespaces/archives.d.ts.map +1 -0
- package/sdk/dist/namespaces/archives.js +202 -0
- package/sdk/dist/namespaces/archives.js.map +1 -0
- package/sdk/dist/namespaces/archives.types.d.ts +163 -0
- package/sdk/dist/namespaces/archives.types.d.ts.map +1 -0
- package/sdk/dist/namespaces/archives.types.js +2 -0
- package/sdk/dist/namespaces/archives.types.js.map +1 -0
- package/sdk/dist/namespaces/deploy.d.ts.map +1 -1
- package/sdk/dist/namespaces/deploy.js +173 -15
- package/sdk/dist/namespaces/deploy.js.map +1 -1
- package/sdk/dist/namespaces/deploy.types.d.ts +16 -1
- package/sdk/dist/namespaces/deploy.types.d.ts.map +1 -1
- package/sdk/dist/namespaces/deploy.types.js.map +1 -1
- package/sdk/dist/namespaces/projects.d.ts.map +1 -1
- package/sdk/dist/namespaces/projects.js +1 -0
- package/sdk/dist/namespaces/projects.js.map +1 -1
- package/sdk/dist/namespaces/projects.types.d.ts +7 -0
- package/sdk/dist/namespaces/projects.types.d.ts.map +1 -1
- package/sdk/dist/node/archives-node.d.ts +14 -0
- package/sdk/dist/node/archives-node.d.ts.map +1 -0
- package/sdk/dist/node/archives-node.js +715 -0
- package/sdk/dist/node/archives-node.js.map +1 -0
- package/sdk/dist/node/deploy-manifest.d.ts.map +1 -1
- package/sdk/dist/node/deploy-manifest.js +15 -2
- package/sdk/dist/node/deploy-manifest.js.map +1 -1
- package/sdk/dist/node/index.d.ts +4 -1
- package/sdk/dist/node/index.d.ts.map +1 -1
- package/sdk/dist/node/index.js +3 -0
- package/sdk/dist/node/index.js.map +1 -1
- package/sdk/dist/scoped.d.ts +12 -0
- package/sdk/dist/scoped.d.ts.map +1 -1
- package/sdk/dist/scoped.js +25 -0
- package/sdk/dist/scoped.js.map +1 -1
package/lib/core.mjs
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { importArchiveToCore } from "#sdk/node";
|
|
2
|
+
import { fail, reportSdkError } from "./sdk-errors.mjs";
|
|
3
|
+
import { assertKnownFlags, flagValue, hasHelp, normalizeArgv, positionalArgs } from "./argparse.mjs";
|
|
4
|
+
|
|
5
|
+
const HELP = `run402 core — Local Run402 Core commands
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
run402 core projects import <archive-path> --name <project-name> [options]
|
|
9
|
+
|
|
10
|
+
Options:
|
|
11
|
+
--name <name> New Core project name (default imported-project)
|
|
12
|
+
--env-file <path> Env file satisfying required archive secrets.
|
|
13
|
+
--secret KEY=VALUE Inline secret value; repeatable. Overrides --env-file.
|
|
14
|
+
--core-url <url> Core gateway URL (default RUN402_CORE_URL or http://127.0.0.1:4020)
|
|
15
|
+
--dry-run Verify and plan without creating a Core project.
|
|
16
|
+
--require-runnable Block import unless required secrets are supplied.
|
|
17
|
+
--json Emit final JSON.
|
|
18
|
+
--json-stream Emit NDJSON progress events and final result event.
|
|
19
|
+
`;
|
|
20
|
+
|
|
21
|
+
const FLAG_VALUES = ["--name", "--env-file", "--secret", "--core-url"];
|
|
22
|
+
const FLAGS = new Set([...FLAG_VALUES, "--dry-run", "--require-runnable", "--json", "--json-stream", "--help", "-h"]);
|
|
23
|
+
|
|
24
|
+
export async function run(sub, args = []) {
|
|
25
|
+
const all = [sub, ...args].filter(Boolean);
|
|
26
|
+
if (hasHelp(all)) {
|
|
27
|
+
console.log(HELP);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
if (sub !== "projects") {
|
|
31
|
+
fail({ code: "BAD_USAGE", message: "Usage: run402 core projects import <archive-path> [options]" });
|
|
32
|
+
}
|
|
33
|
+
const action = args[0];
|
|
34
|
+
if (action === "import") return importProject(args.slice(1));
|
|
35
|
+
fail({ code: "BAD_USAGE", message: "Usage: run402 core projects import <archive-path> [options]" });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function importProject(rawArgs) {
|
|
39
|
+
const args = normalizeArgv(rawArgs);
|
|
40
|
+
assertKnownFlags(args, [...FLAGS], FLAG_VALUES);
|
|
41
|
+
const archivePath = positionalArgs(args, FLAG_VALUES)[0];
|
|
42
|
+
if (!archivePath) {
|
|
43
|
+
fail({ code: "BAD_USAGE", message: "Usage: run402 core projects import <archive-path> --name <project-name> [options]" });
|
|
44
|
+
}
|
|
45
|
+
const jsonStream = args.includes("--json-stream");
|
|
46
|
+
const secretValues = parseSecrets(args);
|
|
47
|
+
const name = flagValue(args, "--name") ?? "imported-project";
|
|
48
|
+
const coreUrl = flagValue(args, "--core-url") ?? undefined;
|
|
49
|
+
const startedEvent = {
|
|
50
|
+
event: "core_archive_import_started",
|
|
51
|
+
stage: "verify",
|
|
52
|
+
resource_type: "project_archive",
|
|
53
|
+
resource_id: archivePath,
|
|
54
|
+
status: "running",
|
|
55
|
+
completed_units: 0,
|
|
56
|
+
total_units: 1,
|
|
57
|
+
code: null,
|
|
58
|
+
message: "Verifying archive locally before Core import.",
|
|
59
|
+
next_action: { type: "none" },
|
|
60
|
+
retryable: false,
|
|
61
|
+
context: { archive_path: archivePath, core_url: coreUrl ?? null, project_name: name },
|
|
62
|
+
};
|
|
63
|
+
if (jsonStream) console.log(JSON.stringify(startedEvent));
|
|
64
|
+
try {
|
|
65
|
+
const result = await importArchiveToCore({
|
|
66
|
+
archivePath,
|
|
67
|
+
name,
|
|
68
|
+
coreUrl,
|
|
69
|
+
envFile: flagValue(args, "--env-file") ?? undefined,
|
|
70
|
+
secretValues,
|
|
71
|
+
dryRun: args.includes("--dry-run"),
|
|
72
|
+
requireRunnable: args.includes("--require-runnable"),
|
|
73
|
+
});
|
|
74
|
+
if (jsonStream) {
|
|
75
|
+
console.log(JSON.stringify({
|
|
76
|
+
event: "core_archive_import_complete",
|
|
77
|
+
stage: "complete",
|
|
78
|
+
resource_type: "project_archive",
|
|
79
|
+
resource_id: archivePath,
|
|
80
|
+
status: result.status,
|
|
81
|
+
completed_units: result.status === "imported" || result.status === "dry_run" ? 1 : 0,
|
|
82
|
+
total_units: 1,
|
|
83
|
+
code: firstDiagnosticCode(result),
|
|
84
|
+
message: `Core archive import status: ${result.status}`,
|
|
85
|
+
next_action: result.next_action,
|
|
86
|
+
retryable: result.status === "failed" && result.diagnostics.some((d) => d.retryable),
|
|
87
|
+
result,
|
|
88
|
+
}));
|
|
89
|
+
} else {
|
|
90
|
+
console.log(JSON.stringify({ ok: result.status === "imported" || result.status === "dry_run", import: result }, null, 2));
|
|
91
|
+
}
|
|
92
|
+
if (result.status !== "imported" && result.status !== "dry_run") process.exit(1);
|
|
93
|
+
} catch (err) {
|
|
94
|
+
reportSdkError(err);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function parseSecrets(args) {
|
|
99
|
+
const out = {};
|
|
100
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
101
|
+
if (args[i] !== "--secret") continue;
|
|
102
|
+
const value = args[i + 1];
|
|
103
|
+
if (!value || value.startsWith("--")) {
|
|
104
|
+
fail({ code: "BAD_FLAG", message: "--secret requires KEY=VALUE" });
|
|
105
|
+
}
|
|
106
|
+
const eq = value.indexOf("=");
|
|
107
|
+
if (eq <= 0) {
|
|
108
|
+
fail({ code: "BAD_FLAG", message: "--secret requires KEY=VALUE", details: { value } });
|
|
109
|
+
}
|
|
110
|
+
const key = value.slice(0, eq);
|
|
111
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
|
|
112
|
+
fail({ code: "BAD_FLAG", message: `Invalid secret env var name: ${key}` });
|
|
113
|
+
}
|
|
114
|
+
out[key] = value.slice(eq + 1);
|
|
115
|
+
i += 1;
|
|
116
|
+
}
|
|
117
|
+
return out;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function firstDiagnosticCode(result) {
|
|
121
|
+
return result.diagnostics.find((d) => d.severity === "blocking")?.code ?? null;
|
|
122
|
+
}
|
package/lib/deploy-v2.mjs
CHANGED
|
@@ -32,10 +32,11 @@ import {
|
|
|
32
32
|
} from "#sdk/node";
|
|
33
33
|
import { getSdk } from "./sdk.mjs";
|
|
34
34
|
import { reportSdkError, fail } from "./sdk-errors.mjs";
|
|
35
|
-
import { API, allowanceAuthHeaders, getActiveProjectId, resolveProjectId } from "./config.mjs";
|
|
35
|
+
import { API, allowanceAuthHeaders, getActiveProjectId, resolveProjectId, isCoreApiTarget } from "./config.mjs";
|
|
36
36
|
import { normalizeArgv } from "./argparse.mjs";
|
|
37
37
|
import { loadLiveControlPlaneSession } from "../core-dist/control-plane-session.js";
|
|
38
38
|
import { withAutoApprove } from "./operator.mjs";
|
|
39
|
+
import { editRequestAction, nextAction, retryAction } from "./next-actions.mjs";
|
|
39
40
|
|
|
40
41
|
const APPLY_HELP = `run402 deploy apply — Unified deploy primitive (v1.34+)
|
|
41
42
|
|
|
@@ -123,6 +124,11 @@ Routes:
|
|
|
123
124
|
Routes activate atomically with the release. Direct /functions/v1/:name remains API-key protected.
|
|
124
125
|
Runtime route failure codes: ROUTE_MANIFEST_LOAD_FAILED, ROUTED_INVOKE_WORKER_SECRET_MISSING, ROUTED_INVOKE_AUTH_FAILED, ROUTED_ROUTE_STALE, ROUTE_METHOD_NOT_ALLOWED, ROUTED_RESPONSE_TOO_LARGE.
|
|
125
126
|
|
|
127
|
+
Function capabilities:
|
|
128
|
+
functions.replace.<name>.capabilities is an array of runtime capability strings.
|
|
129
|
+
Framework adapters use it for contracts such as "astro.ssr.v1"; omit it for
|
|
130
|
+
ordinary user-authored functions unless a documented helper requires it.
|
|
131
|
+
|
|
126
132
|
Internationalization (routed functions):
|
|
127
133
|
"i18n": { "defaultLocale": "en", "locales": ["en", "es", "fr"], "detect": ["cookie:wl_locale", "accept-language"] }
|
|
128
134
|
Omit i18n to carry forward from base release; pass "i18n": null to clear the slice on the new release.
|
|
@@ -663,7 +669,16 @@ async function mergeAstroReleaseSlice(spec, dirArg) {
|
|
|
663
669
|
* the manifest normalizer is the authority on shape validity, not this
|
|
664
670
|
* best-effort extractor.
|
|
665
671
|
*/
|
|
666
|
-
function
|
|
672
|
+
function isAstroSsrManifestFunction(entry) {
|
|
673
|
+
return (
|
|
674
|
+
entry &&
|
|
675
|
+
typeof entry === "object" &&
|
|
676
|
+
Array.isArray(entry.capabilities) &&
|
|
677
|
+
entry.capabilities.includes("astro.ssr.v1")
|
|
678
|
+
);
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
export function collectManifestSourceFiles(spec, baseDir) {
|
|
667
682
|
const out = new Set();
|
|
668
683
|
if (!spec || typeof spec !== "object") return [];
|
|
669
684
|
|
|
@@ -688,13 +703,20 @@ function collectManifestSourceFiles(spec, baseDir) {
|
|
|
688
703
|
if (!map || typeof map !== "object") return;
|
|
689
704
|
for (const entry of Object.values(map)) resolveEntryPath(entry);
|
|
690
705
|
};
|
|
706
|
+
const eachFunctionValue = (map) => {
|
|
707
|
+
if (!map || typeof map !== "object") return;
|
|
708
|
+
for (const entry of Object.values(map)) {
|
|
709
|
+
if (isAstroSsrManifestFunction(entry)) continue;
|
|
710
|
+
resolveEntryPath(entry);
|
|
711
|
+
}
|
|
712
|
+
};
|
|
691
713
|
|
|
692
714
|
const fns = spec.functions;
|
|
693
715
|
if (fns && typeof fns === "object") {
|
|
694
|
-
|
|
716
|
+
eachFunctionValue(fns.replace);
|
|
695
717
|
if (fns.patch && typeof fns.patch === "object") {
|
|
696
|
-
|
|
697
|
-
|
|
718
|
+
eachFunctionValue(fns.patch.put);
|
|
719
|
+
eachFunctionValue(fns.patch.set);
|
|
698
720
|
}
|
|
699
721
|
}
|
|
700
722
|
|
|
@@ -895,9 +917,10 @@ async function applyCmd(args) {
|
|
|
895
917
|
credentials: githubActionsCredentials({ projectId: releaseSpec.project, apiBase: API }),
|
|
896
918
|
disablePaidFetch: true,
|
|
897
919
|
};
|
|
898
|
-
} else if (!loadLiveControlPlaneSession()) {
|
|
920
|
+
} else if (!isCoreApiTarget() && !loadLiveControlPlaneSession()) {
|
|
899
921
|
// Aggressive early exit when no allowance is configured — unless a
|
|
900
|
-
// wallet-less human is deploying via their operator (control-plane) session
|
|
922
|
+
// wallet-less human is deploying via their operator (control-plane) session
|
|
923
|
+
// or the active target is a self-hosted Core Gateway.
|
|
901
924
|
allowanceAuthHeaders("/apply/v1/plans");
|
|
902
925
|
}
|
|
903
926
|
|
|
@@ -908,6 +931,7 @@ async function applyCmd(args) {
|
|
|
908
931
|
idempotencyKey,
|
|
909
932
|
allowWarnings: opts.allowWarnings,
|
|
910
933
|
allowWarningCodes: opts.allowWarningCodes,
|
|
934
|
+
target: isCoreApiTarget() ? "core" : "cloud",
|
|
911
935
|
}),
|
|
912
936
|
);
|
|
913
937
|
console.log(JSON.stringify(result, null, 2));
|
|
@@ -939,66 +963,66 @@ const CI_DEPLOY_ERROR_GUIDANCE = {
|
|
|
939
963
|
invalid_token: {
|
|
940
964
|
hint: "Ensure the workflow has permissions: id-token: write and is running in the repository/branch linked with run402 ci link github.",
|
|
941
965
|
next_actions: [
|
|
942
|
-
"Check the workflow permissions block includes id-token: write.",
|
|
943
|
-
"
|
|
966
|
+
nextAction("edit_request", { why: "Check the workflow permissions block includes id-token: write." }),
|
|
967
|
+
editRequestAction("run402 ci link github", "Re-link if the repository, branch, or environment changed."),
|
|
944
968
|
],
|
|
945
969
|
},
|
|
946
970
|
access_denied: {
|
|
947
971
|
hint: "The OIDC token was valid, but no active Run402 CI binding allowed this workflow.",
|
|
948
972
|
next_actions: [
|
|
949
|
-
"
|
|
950
|
-
"
|
|
973
|
+
editRequestAction("run402 ci list --project <prj_...>", "Inspect active CI bindings for this project."),
|
|
974
|
+
editRequestAction("run402 ci link github", "Link this repository, branch, and environment to Run402."),
|
|
951
975
|
],
|
|
952
976
|
},
|
|
953
977
|
binding_revoked: {
|
|
954
978
|
hint: "A matching CI binding existed but was revoked — most often because the project was transferred or handed to a new owner, which suspends the prior org's CI bindings.",
|
|
955
979
|
next_actions: [
|
|
956
|
-
"
|
|
957
|
-
"Do not run run402 ci set-asset-scopes
|
|
958
|
-
"
|
|
980
|
+
editRequestAction("run402 ci link github", "Re-create the CI binding from this repository."),
|
|
981
|
+
nextAction("edit_request", { why: "Do not run run402 ci set-asset-scopes; it returns 409 on a revoked binding." }),
|
|
982
|
+
editRequestAction("run402 ci list --project <prj_...>", "Confirm the binding state locally."),
|
|
959
983
|
],
|
|
960
984
|
},
|
|
961
985
|
event_not_allowed: {
|
|
962
986
|
hint: "This binding only allows push and workflow_dispatch events in v1.",
|
|
963
987
|
next_actions: [
|
|
964
|
-
"Trigger the workflow with push or workflow_dispatch.",
|
|
965
|
-
"Create a separate follow-up design before enabling PR deploy events.",
|
|
988
|
+
nextAction("retry", { why: "Trigger the workflow with push or workflow_dispatch." }),
|
|
989
|
+
nextAction("edit_request", { why: "Create a separate follow-up design before enabling PR deploy events." }),
|
|
966
990
|
],
|
|
967
991
|
},
|
|
968
992
|
repository_id_mismatch: {
|
|
969
993
|
hint: "The GitHub repository id in the OIDC token does not match the linked binding.",
|
|
970
994
|
next_actions: [
|
|
971
|
-
"
|
|
972
|
-
"
|
|
995
|
+
editRequestAction("run402 ci link github", "Re-link from the current repository."),
|
|
996
|
+
editRequestAction("run402 ci link github --repository-id <id>", "Pass the numeric GitHub repository id if automatic lookup fails."),
|
|
973
997
|
],
|
|
974
998
|
},
|
|
975
999
|
forbidden_spec_field: {
|
|
976
1000
|
hint: "CI deploys can deploy site/functions/database content and route declarations only when the binding includes covering route scopes.",
|
|
977
1001
|
next_actions: [
|
|
978
|
-
"Remove forbidden fields such as secrets, subdomains, or checks from the CI manifest.",
|
|
979
|
-
"Keep the normalized manifest small enough to avoid manifest_ref.",
|
|
1002
|
+
nextAction("edit_request", { why: "Remove forbidden fields such as secrets, subdomains, or checks from the CI manifest." }),
|
|
1003
|
+
nextAction("edit_request", { why: "Keep the normalized manifest small enough to avoid manifest_ref." }),
|
|
980
1004
|
],
|
|
981
1005
|
},
|
|
982
1006
|
CI_ROUTE_SCOPE_DENIED: {
|
|
983
1007
|
hint: "This CI binding does not cover one or more route declarations in the deploy manifest.",
|
|
984
1008
|
next_actions: [
|
|
985
|
-
"
|
|
986
|
-
"Use exact scopes such as --route-scope /admin or prefix scopes such as --route-scope /api/*.",
|
|
987
|
-
"
|
|
1009
|
+
editRequestAction("run402 ci link github --route-scope <pattern>", "Add a CI route scope for every exact or prefix path the workflow may deploy."),
|
|
1010
|
+
nextAction("edit_request", { why: "Use exact scopes such as --route-scope /admin or prefix scopes such as --route-scope /api/*." }),
|
|
1011
|
+
editRequestAction("run402 deploy apply", "Run route changes locally when they are outside the CI delegation."),
|
|
988
1012
|
],
|
|
989
1013
|
},
|
|
990
1014
|
forbidden_plan: {
|
|
991
1015
|
hint: "The gateway rejected this deploy plan for CI. Keep CI deploys to the allowed resources and re-link if policy changed.",
|
|
992
1016
|
next_actions: [
|
|
993
|
-
"Inspect the gateway error details for the rejected resource.",
|
|
994
|
-
"
|
|
1017
|
+
nextAction("edit_request", { why: "Inspect the gateway error details for the rejected resource." }),
|
|
1018
|
+
editRequestAction("run402 deploy apply", "Run operations outside the CI allowlist locally."),
|
|
995
1019
|
],
|
|
996
1020
|
},
|
|
997
1021
|
payment_required: {
|
|
998
1022
|
hint: "The project tier or payment state does not allow this CI deploy.",
|
|
999
1023
|
next_actions: [
|
|
1000
|
-
"
|
|
1001
|
-
"Renew or upgrade the project tier, then re-run the workflow.",
|
|
1024
|
+
editRequestAction("run402 tier status --project <prj_...>", "Inspect project tier and payment state locally."),
|
|
1025
|
+
editRequestAction("run402 tier set <tier>", "Renew or upgrade the project tier, then re-run the workflow."),
|
|
1002
1026
|
],
|
|
1003
1027
|
},
|
|
1004
1028
|
};
|
|
@@ -1032,18 +1056,22 @@ function enhanceDeployWarningError(err) {
|
|
|
1032
1056
|
: code
|
|
1033
1057
|
? [code]
|
|
1034
1058
|
: [];
|
|
1035
|
-
const
|
|
1036
|
-
? `
|
|
1037
|
-
:
|
|
1038
|
-
|
|
1039
|
-
|
|
1059
|
+
const allowWarningCommand = unacknowledgedCodes.length > 0
|
|
1060
|
+
? `run402 deploy apply ${unacknowledgedCodes.map((warningCode) => `--allow-warning ${warningCode}`).join(" ")}`
|
|
1061
|
+
: "run402 deploy apply --allow-warning <code>";
|
|
1062
|
+
const allowWarningAction = editRequestAction(
|
|
1063
|
+
allowWarningCommand,
|
|
1064
|
+
unacknowledgedCodes.length > 0
|
|
1065
|
+
? `Retry only after reviewing warning code${unacknowledgedCodes.length === 1 ? "" : "s"}: ${unacknowledgedCodes.join(", ")}.`
|
|
1066
|
+
: "Retry only after reviewing the warning code.",
|
|
1067
|
+
);
|
|
1040
1068
|
const defaultNextActions = [
|
|
1041
1069
|
...(affected.length > 0
|
|
1042
|
-
? [`Set or inspect affected secrets: ${Array.from(new Set(affected)).join(", ")}`]
|
|
1070
|
+
? [editRequestAction("run402 secrets set <project> <KEY> --stdin", `Set or inspect affected secrets: ${Array.from(new Set(affected)).join(", ")}`)]
|
|
1043
1071
|
: []),
|
|
1044
|
-
"
|
|
1072
|
+
retryAction("run402 deploy apply", "Retry after resolving warnings."),
|
|
1045
1073
|
allowWarningAction,
|
|
1046
|
-
"
|
|
1074
|
+
editRequestAction("run402 deploy apply --allow-warnings", "Use only when every warning was explicitly reviewed."),
|
|
1047
1075
|
];
|
|
1048
1076
|
enhanced.body = {
|
|
1049
1077
|
...existingBody,
|
|
@@ -1066,98 +1094,98 @@ const ROUTE_WARNING_GUIDANCE = {
|
|
|
1066
1094
|
PUBLIC_ROUTED_FUNCTION: {
|
|
1067
1095
|
hint: "A deploy route makes a function public same-origin browser ingress; direct /functions/v1/:name remains API-key protected.",
|
|
1068
1096
|
next_actions: [
|
|
1069
|
-
"Review application auth and authorization in the routed function.",
|
|
1070
|
-
"Add CSRF protection for cookie-authenticated POST/PUT/PATCH/DELETE routes.",
|
|
1071
|
-
"Implement CORS and OPTIONS explicitly when cross-origin callers are intended.",
|
|
1072
|
-
"
|
|
1097
|
+
nextAction("edit_request", { why: "Review application auth and authorization in the routed function." }),
|
|
1098
|
+
nextAction("edit_request", { why: "Add CSRF protection for cookie-authenticated POST/PUT/PATCH/DELETE routes." }),
|
|
1099
|
+
nextAction("edit_request", { why: "Implement CORS and OPTIONS explicitly when cross-origin callers are intended." }),
|
|
1100
|
+
retryAction("run402 deploy apply --allow-warnings", "Retry only after the public ingress review is intentional."),
|
|
1073
1101
|
],
|
|
1074
1102
|
},
|
|
1075
1103
|
ROUTE_TARGET_CARRIED_FORWARD: {
|
|
1076
1104
|
hint: "A carried-forward route still points at a base-release function target.",
|
|
1077
1105
|
next_actions: [
|
|
1078
|
-
"
|
|
1079
|
-
"Deploy routes.replace if the target should change in this release.",
|
|
1106
|
+
editRequestAction("run402 deploy release active", "Inspect the active release."),
|
|
1107
|
+
editRequestAction("run402 deploy apply --manifest <path>", "Deploy routes.replace if the target should change in this release."),
|
|
1080
1108
|
],
|
|
1081
1109
|
},
|
|
1082
1110
|
ROUTE_SHADOWS_STATIC_PATH: {
|
|
1083
1111
|
hint: "A dynamic route shadows a static site path.",
|
|
1084
1112
|
next_actions: [
|
|
1085
|
-
"Inspect the warning details for affected static paths.",
|
|
1086
|
-
"
|
|
1087
|
-
"
|
|
1113
|
+
nextAction("edit_request", { why: "Inspect the warning details for affected static paths." }),
|
|
1114
|
+
editRequestAction("run402 deploy release active", "Inspect live routes."),
|
|
1115
|
+
retryAction("run402 deploy apply --allow-warnings", "Retry only when dynamic shadowing is intentional."),
|
|
1088
1116
|
],
|
|
1089
1117
|
},
|
|
1090
1118
|
WILDCARD_ROUTE_SHADOWS_STATIC_PATHS: {
|
|
1091
1119
|
hint: "A prefix wildcard route shadows one or more static site paths.",
|
|
1092
1120
|
next_actions: [
|
|
1093
|
-
"Review affected route/static path details.",
|
|
1094
|
-
"Split exact routes or move static paths if the shadowing is accidental.",
|
|
1095
|
-
"
|
|
1121
|
+
nextAction("edit_request", { why: "Review affected route/static path details." }),
|
|
1122
|
+
nextAction("edit_request", { why: "Split exact routes or move static paths if the shadowing is accidental." }),
|
|
1123
|
+
retryAction("run402 deploy apply --allow-warnings", "Retry only when the wildcard shadowing is intentional."),
|
|
1096
1124
|
],
|
|
1097
1125
|
},
|
|
1098
1126
|
METHOD_SPECIFIC_ROUTE_ALLOWS_GET_STATIC_FALLBACK: {
|
|
1099
1127
|
hint: "A method-specific route allows static fallback for unmatched methods such as GET.",
|
|
1100
1128
|
next_actions: [
|
|
1101
|
-
"Confirm that static fallback for GET/HEAD is intended.",
|
|
1102
|
-
"Add method coverage or static files deliberately.",
|
|
1129
|
+
nextAction("edit_request", { why: "Confirm that static fallback for GET/HEAD is intended." }),
|
|
1130
|
+
nextAction("edit_request", { why: "Add method coverage or static files deliberately." }),
|
|
1103
1131
|
],
|
|
1104
1132
|
},
|
|
1105
1133
|
WILDCARD_ROUTE_EXCLUDES_MUTATION_METHODS: {
|
|
1106
1134
|
hint: "A wildcard function route only allows GET/HEAD, so POST/PUT/PATCH/DELETE paths under that prefix will be rejected before the function runs.",
|
|
1107
1135
|
next_actions: [
|
|
1108
|
-
"Add the mutation methods the routed function supports, such as POST.",
|
|
1109
|
-
"Omit methods to allow every supported method when the route is an API surface.",
|
|
1110
|
-
"Set acknowledge_readonly: true on an intentionally read-only GET/HEAD wildcard function route.",
|
|
1111
|
-
"
|
|
1136
|
+
nextAction("edit_request", { why: "Add the mutation methods the routed function supports, such as POST." }),
|
|
1137
|
+
nextAction("edit_request", { why: "Omit methods to allow every supported method when the route is an API surface." }),
|
|
1138
|
+
nextAction("edit_request", { why: "Set acknowledge_readonly: true on an intentionally read-only GET/HEAD wildcard function route." }),
|
|
1139
|
+
retryAction("run402 deploy apply --allow-warning WILDCARD_ROUTE_EXCLUDES_MUTATION_METHODS", "Use only as a reviewed CLI escape hatch."),
|
|
1112
1140
|
],
|
|
1113
1141
|
},
|
|
1114
1142
|
ROUTE_TABLE_NEAR_LIMIT: {
|
|
1115
1143
|
hint: "The route table is near the gateway/project limit.",
|
|
1116
1144
|
next_actions: [
|
|
1117
|
-
"Consolidate prefix routes where possible.",
|
|
1118
|
-
"Remove stale route entries before adding more.",
|
|
1145
|
+
nextAction("edit_request", { why: "Consolidate prefix routes where possible." }),
|
|
1146
|
+
nextAction("edit_request", { why: "Remove stale route entries before adding more." }),
|
|
1119
1147
|
],
|
|
1120
1148
|
},
|
|
1121
1149
|
ROUTES_NOT_ENABLED: {
|
|
1122
1150
|
hint: "Deploy-v2 web routes are not enabled for this project or environment; direct /functions/v1/:name remains protected and is not a browser-route substitute.",
|
|
1123
1151
|
next_actions: [
|
|
1124
|
-
"Deploy without the routes resource, or request route enablement for this project/environment.",
|
|
1125
|
-
"Keep direct function invocation API-key protected; do not substitute it for same-origin browser routes.",
|
|
1152
|
+
nextAction("edit_request", { why: "Deploy without the routes resource, or request route enablement for this project/environment." }),
|
|
1153
|
+
nextAction("edit_request", { why: "Keep direct function invocation API-key protected; do not substitute it for same-origin browser routes." }),
|
|
1126
1154
|
],
|
|
1127
1155
|
},
|
|
1128
1156
|
STATIC_ALIAS_SHADOWS_STATIC_PATH: {
|
|
1129
1157
|
hint: "A static route target shadows a direct static path at the same public URL.",
|
|
1130
1158
|
next_actions: [
|
|
1131
|
-
"Inspect the route pattern, target.file, and direct static path.",
|
|
1132
|
-
"Confirm only when the static route target is intentional.",
|
|
1159
|
+
nextAction("edit_request", { why: "Inspect the route pattern, target.file, and direct static path." }),
|
|
1160
|
+
nextAction("edit_request", { why: "Confirm only when the static route target is intentional." }),
|
|
1133
1161
|
],
|
|
1134
1162
|
},
|
|
1135
1163
|
STATIC_ALIAS_RELATIVE_ASSET_RISK: {
|
|
1136
1164
|
hint: "Relative asset URLs inside the target HTML may resolve differently at the static route target URL.",
|
|
1137
1165
|
next_actions: [
|
|
1138
|
-
"Inspect the target HTML for relative asset references.",
|
|
1139
|
-
"Use absolute asset URLs or confirm only when the alternate URL is intentional.",
|
|
1166
|
+
nextAction("edit_request", { why: "Inspect the target HTML for relative asset references." }),
|
|
1167
|
+
nextAction("edit_request", { why: "Use absolute asset URLs or confirm only when the alternate URL is intentional." }),
|
|
1140
1168
|
],
|
|
1141
1169
|
},
|
|
1142
1170
|
STATIC_ALIAS_DUPLICATE_CANONICAL_URL: {
|
|
1143
1171
|
hint: "Both the static route target URL and the target file URL may be publicly reachable.",
|
|
1144
1172
|
next_actions: [
|
|
1145
|
-
"Decide which URL should be canonical.",
|
|
1146
|
-
"Update links/canonical tags or accept the duplicate public URL intentionally.",
|
|
1173
|
+
nextAction("edit_request", { why: "Decide which URL should be canonical." }),
|
|
1174
|
+
nextAction("edit_request", { why: "Update links/canonical tags or accept the duplicate public URL intentionally." }),
|
|
1147
1175
|
],
|
|
1148
1176
|
},
|
|
1149
1177
|
STATIC_ALIAS_EXTENSIONLESS_NON_HTML: {
|
|
1150
1178
|
hint: "An extensionless static route target points at a non-HTML file.",
|
|
1151
1179
|
next_actions: [
|
|
1152
|
-
"Check that the extensionless route is meant to serve that content type.",
|
|
1153
|
-
"Prefer extensionless static route targets for HTML pages.",
|
|
1180
|
+
nextAction("edit_request", { why: "Check that the extensionless route is meant to serve that content type." }),
|
|
1181
|
+
nextAction("edit_request", { why: "Prefer extensionless static route targets for HTML pages." }),
|
|
1154
1182
|
],
|
|
1155
1183
|
},
|
|
1156
1184
|
STATIC_ALIAS_TABLE_NEAR_LIMIT: {
|
|
1157
1185
|
hint: "Static route targets count toward the route table limit.",
|
|
1158
1186
|
next_actions: [
|
|
1159
|
-
"Consolidate manual static route targets where possible.",
|
|
1160
|
-
"Avoid one route entry per page for large sites until framework-scale Web Output support exists.",
|
|
1187
|
+
nextAction("edit_request", { why: "Consolidate manual static route targets where possible." }),
|
|
1188
|
+
nextAction("edit_request", { why: "Avoid one route entry per page for large sites until framework-scale Web Output support exists." }),
|
|
1161
1189
|
],
|
|
1162
1190
|
},
|
|
1163
1191
|
};
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
* summary. Wired into `run402 deploy` pre-flight as a deploy-failing
|
|
30
30
|
* gate for the error severities; non-blocking for warning severities.
|
|
31
31
|
*
|
|
32
|
-
* @see
|
|
32
|
+
* @see the auth-aware-ssr OpenSpec change (functions-sdk-auth-model)
|
|
33
33
|
*/
|
|
34
34
|
|
|
35
35
|
import { readdirSync, readFileSync, statSync } from "node:fs";
|
package/lib/functions.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import { findProject, API } from "./config.mjs";
|
|
|
3
3
|
import { getSdk } from "./sdk.mjs";
|
|
4
4
|
import { reportSdkError, fail } from "./sdk-errors.mjs";
|
|
5
5
|
import { assertKnownFlags, hasHelp, normalizeArgv, parseIntegerFlag, validateRegularFile } from "./argparse.mjs";
|
|
6
|
+
import { cliCommandAction } from "./next-actions.mjs";
|
|
6
7
|
|
|
7
8
|
const FUNCTION_LOG_REQUEST_ID_RE = /^req_[A-Za-z0-9_-]{4,128}$/;
|
|
8
9
|
const ISO_DATE_TIME_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?(?:Z|[+-]\d{2}:\d{2})$/;
|
|
@@ -541,7 +542,7 @@ async function rebuild(projectId, args = []) {
|
|
|
541
542
|
code: "CANNOT_REBUILD_UNLOCKED_DEPS",
|
|
542
543
|
message: err.body.error || err.body.message || "Function was deployed before dependency locking.",
|
|
543
544
|
hint: UNLOCKED_DEPS_HINT,
|
|
544
|
-
next_actions: [`run402 functions deploy ${projectId} ${name} --file <file
|
|
545
|
+
next_actions: [cliCommandAction("deploy", `run402 functions deploy ${projectId} ${name} --file <file>`, "Redeploy the function once so Run402 can lock dependencies for future rebuilds.")],
|
|
545
546
|
retryable: false,
|
|
546
547
|
safe_to_retry: false,
|
|
547
548
|
});
|
package/lib/init.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readAllowance, saveAllowance, loadKeyStore, configDir } from "./config.mjs";
|
|
1
|
+
import { readAllowance, saveAllowance, loadKeyStore, configDir, configureApiBase } from "./config.mjs";
|
|
2
2
|
import { getSdk } from "./sdk.mjs";
|
|
3
3
|
import { fail } from "./sdk-errors.mjs";
|
|
4
4
|
import { setTierAction, deployAction } from "./next-actions.mjs";
|
|
@@ -15,6 +15,9 @@ const HELP = `run402 init — Set up allowance, funding, and check tier status
|
|
|
15
15
|
|
|
16
16
|
Usage:
|
|
17
17
|
run402 init Set up with x402 (Base Sepolia) — default
|
|
18
|
+
run402 init --api-base <url>
|
|
19
|
+
Configure a Run402 Core/API target for the active
|
|
20
|
+
profile without setting up Cloud payment.
|
|
18
21
|
run402 init mpp Set up with MPP (Tempo Moderato)
|
|
19
22
|
run402 init <rail> --switch-rail
|
|
20
23
|
Switch the persisted payment rail to <rail>.
|
|
@@ -23,6 +26,9 @@ Usage:
|
|
|
23
26
|
silently flipping billing networks.
|
|
24
27
|
|
|
25
28
|
Options:
|
|
29
|
+
--api-base <url> Configure the active profile to use this API base. Use this
|
|
30
|
+
for a self-hosted Run402 Core Gateway, e.g.
|
|
31
|
+
http://my-core:4020.
|
|
26
32
|
--switch-rail Confirm switching the persisted payment rail. Re-running
|
|
27
33
|
init with the SAME rail as the existing allowance is always
|
|
28
34
|
idempotent and does not need this flag.
|
|
@@ -46,6 +52,61 @@ Run this once to get started, or again to check your setup.
|
|
|
46
52
|
|
|
47
53
|
function short(addr) { return addr.slice(0, 6) + "..." + addr.slice(-4); }
|
|
48
54
|
|
|
55
|
+
function parseApiBaseFlag(args) {
|
|
56
|
+
for (let i = 0; i < args.length; i++) {
|
|
57
|
+
const arg = args[i];
|
|
58
|
+
if (arg === "--api-base") {
|
|
59
|
+
const value = args[i + 1];
|
|
60
|
+
if (value === undefined || String(value).startsWith("--")) {
|
|
61
|
+
fail({
|
|
62
|
+
code: "BAD_USAGE",
|
|
63
|
+
message: "--api-base requires a value.",
|
|
64
|
+
details: { flag: "--api-base" },
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
return { value, args: [...args.slice(0, i), ...args.slice(i + 2)] };
|
|
68
|
+
}
|
|
69
|
+
if (typeof arg === "string" && arg.startsWith("--api-base=")) {
|
|
70
|
+
const value = arg.slice("--api-base=".length);
|
|
71
|
+
if (!value) {
|
|
72
|
+
fail({
|
|
73
|
+
code: "BAD_USAGE",
|
|
74
|
+
message: "--api-base requires a non-empty value.",
|
|
75
|
+
details: { flag: "--api-base" },
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
return { value, args: [...args.slice(0, i), ...args.slice(i + 1)] };
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return { value: null, args };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function sameOrigin(a, b) {
|
|
85
|
+
try {
|
|
86
|
+
return new URL(a).origin === new URL(b).origin;
|
|
87
|
+
} catch {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function detectTarget(apiBase) {
|
|
93
|
+
try {
|
|
94
|
+
const health = await getSdk({ apiBase, disablePaidFetch: true, authMode: "none" }).service.health();
|
|
95
|
+
const kind = health && typeof health === "object" && health.mode === "core"
|
|
96
|
+
? "core"
|
|
97
|
+
: sameOrigin(apiBase, "https://api.run402.com") ? "cloud" : "core";
|
|
98
|
+
return {
|
|
99
|
+
kind,
|
|
100
|
+
health_status: typeof health?.status === "string" ? health.status : "ok",
|
|
101
|
+
};
|
|
102
|
+
} catch (err) {
|
|
103
|
+
return {
|
|
104
|
+
kind: sameOrigin(apiBase, "https://api.run402.com") ? "cloud" : "core",
|
|
105
|
+
health_error: errorMessage(err),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
49
110
|
function errorMessage(err) {
|
|
50
111
|
if (err?.body && typeof err.body === "object") return err.body.message || err.body.error || err.message;
|
|
51
112
|
return err?.message || String(err);
|
|
@@ -67,6 +128,50 @@ export async function run(args = []) {
|
|
|
67
128
|
|
|
68
129
|
if (args.includes("--help") || args.includes("-h")) { console.log(HELP); process.exit(0); }
|
|
69
130
|
|
|
131
|
+
const parsedApiBase = parseApiBaseFlag(args);
|
|
132
|
+
if (parsedApiBase.value) {
|
|
133
|
+
if (parsedApiBase.args.some((arg) => typeof arg === "string" && !arg.startsWith("--"))) {
|
|
134
|
+
fail({
|
|
135
|
+
code: "BAD_USAGE",
|
|
136
|
+
message: "run402 init --api-base cannot be combined with a payment rail.",
|
|
137
|
+
hint: "Run `run402 init --api-base=http://my-core:4020` for Core, or `run402 init` for Run402 Cloud.",
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
const CONFIG_DIR = configDir();
|
|
141
|
+
const detected = await detectTarget(parsedApiBase.value);
|
|
142
|
+
const config = configureApiBase(parsedApiBase.value, {
|
|
143
|
+
target_kind: detected.kind,
|
|
144
|
+
...(detected.health_status ? { health_status: detected.health_status } : {}),
|
|
145
|
+
...(detected.health_error ? { health_error: detected.health_error } : {}),
|
|
146
|
+
});
|
|
147
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
148
|
+
console.error("");
|
|
149
|
+
console.error(` ${"Config".padEnd(10)} ${CONFIG_DIR}`);
|
|
150
|
+
console.error(` ${"API base".padEnd(10)} ${config.api_base}`);
|
|
151
|
+
console.error(` ${"Target".padEnd(10)} ${config.target_kind}`);
|
|
152
|
+
if (detected.health_status) console.error(` ${"Health".padEnd(10)} ${detected.health_status}`);
|
|
153
|
+
if (detected.health_error) console.error(` ${"Health".padEnd(10)} ${detected.health_error}`);
|
|
154
|
+
console.error("");
|
|
155
|
+
const summary = {
|
|
156
|
+
config_dir: CONFIG_DIR,
|
|
157
|
+
api_base: config.api_base,
|
|
158
|
+
api_base_source: "profile",
|
|
159
|
+
target: {
|
|
160
|
+
kind: config.target_kind,
|
|
161
|
+
...(config.health_status ? { health_status: config.health_status } : {}),
|
|
162
|
+
...(config.health_error ? { health_error: config.health_error } : {}),
|
|
163
|
+
},
|
|
164
|
+
payment_required: config.target_kind === "cloud",
|
|
165
|
+
next_actions: [{
|
|
166
|
+
type: "create_project",
|
|
167
|
+
command: 'run402 projects provision --name "my-app"',
|
|
168
|
+
}],
|
|
169
|
+
next_step: 'run402 projects provision --name "my-app"',
|
|
170
|
+
};
|
|
171
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
70
175
|
// Resolve once for this invocation — reflects the active wallet/profile that
|
|
71
176
|
// cli.mjs published to RUN402_WALLET before this module loaded.
|
|
72
177
|
const CONFIG_DIR = configDir();
|