run402 3.5.4 → 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.
@@ -1,7 +1,8 @@
1
1
  import { homedir } from "node:os";
2
- import { join } from "node:path";
3
- import { existsSync, renameSync, mkdirSync, chmodSync } from "node:fs";
4
- const DEFAULT_API_BASE = "https://api.run402.com";
2
+ import { dirname, join } from "node:path";
3
+ import { existsSync, renameSync, mkdirSync, chmodSync, readFileSync, writeFileSync } from "node:fs";
4
+ import { randomBytes } from "node:crypto";
5
+ export const DEFAULT_API_BASE = "https://api.run402.com";
5
6
  /**
6
7
  * Validate a user-supplied API base URL. Throws a clear error message that
7
8
  * names the env var when the URL is malformed or uses a scheme other than
@@ -33,7 +34,12 @@ function validateApiBase(envVar, raw, fallback) {
33
34
  }
34
35
  export function getApiBase() {
35
36
  const validated = validateApiBase("RUN402_API_BASE", process.env.RUN402_API_BASE, DEFAULT_API_BASE);
36
- return validated ?? DEFAULT_API_BASE;
37
+ return validated ?? getConfiguredApiBase() ?? DEFAULT_API_BASE;
38
+ }
39
+ export function getApiBaseSource() {
40
+ if (process.env.RUN402_API_BASE !== undefined)
41
+ return "env";
42
+ return getConfiguredApiBase() ? "profile" : "default";
37
43
  }
38
44
  /**
39
45
  * API base for the deploy-v2 routes. Defaults to the same value as
@@ -115,6 +121,82 @@ export function getConfigDir() {
115
121
  export function getKeystorePath() {
116
122
  return join(getConfigDir(), "projects.json");
117
123
  }
124
+ export function getApiTargetConfigPath() {
125
+ return join(getConfigDir(), "target.json");
126
+ }
127
+ export function readApiTargetConfig(path) {
128
+ const p = path ?? getApiTargetConfigPath();
129
+ try {
130
+ const parsed = JSON.parse(readFileSync(p, "utf-8"));
131
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
132
+ return null;
133
+ const cfg = parsed;
134
+ if (cfg.api_base !== undefined && typeof cfg.api_base !== "string")
135
+ return null;
136
+ if (cfg.target_kind !== undefined &&
137
+ cfg.target_kind !== "cloud" &&
138
+ cfg.target_kind !== "core" &&
139
+ cfg.target_kind !== "unknown") {
140
+ return null;
141
+ }
142
+ return cfg;
143
+ }
144
+ catch {
145
+ return null;
146
+ }
147
+ }
148
+ function atomicWrite(path, content, mode) {
149
+ const dir = dirname(path);
150
+ mkdirSync(dir, { recursive: true });
151
+ const tmp = join(dir, `.target.${randomBytes(4).toString("hex")}.tmp`);
152
+ writeFileSync(tmp, content, { mode });
153
+ renameSync(tmp, path);
154
+ try {
155
+ chmodSync(path, mode);
156
+ }
157
+ catch {
158
+ /* best-effort on non-POSIX */
159
+ }
160
+ }
161
+ export function saveApiTargetConfig(config, path) {
162
+ const p = path ?? getApiTargetConfigPath();
163
+ atomicWrite(p, JSON.stringify(config, null, 2), 0o600);
164
+ }
165
+ export function configureApiBase(apiBase, options = {}) {
166
+ if (apiBase === "") {
167
+ throw new Error("api_base must be a non-empty http(s) URL.");
168
+ }
169
+ const validated = validateApiBase("api_base", apiBase, DEFAULT_API_BASE);
170
+ if (!validated) {
171
+ throw new Error("api_base must be a non-empty http(s) URL.");
172
+ }
173
+ const config = {
174
+ api_base: validated.replace(/\/+$/, ""),
175
+ target_kind: options.target_kind ?? "unknown",
176
+ updated_at: options.updated_at ?? new Date().toISOString(),
177
+ ...(options.health_status ? { health_status: options.health_status } : {}),
178
+ ...(options.health_error ? { health_error: options.health_error } : {}),
179
+ };
180
+ saveApiTargetConfig(config);
181
+ return config;
182
+ }
183
+ export function getConfiguredApiBase() {
184
+ const cfg = readApiTargetConfig();
185
+ if (!cfg?.api_base)
186
+ return null;
187
+ const validated = validateApiBase("api_base", cfg.api_base, DEFAULT_API_BASE);
188
+ return validated ? validated.replace(/\/+$/, "") : null;
189
+ }
190
+ export function getApiTargetKind() {
191
+ const cfg = readApiTargetConfig();
192
+ return cfg?.target_kind ?? "unknown";
193
+ }
194
+ export function isCoreApiTarget() {
195
+ const cfg = readApiTargetConfig();
196
+ if (cfg?.target_kind !== "core" || !cfg.api_base)
197
+ return false;
198
+ return getApiBase().replace(/\/+$/, "") === cfg.api_base.replace(/\/+$/, "");
199
+ }
118
200
  export function getAllowancePath() {
119
201
  if (process.env.RUN402_ALLOWANCE_PATH)
120
202
  return process.env.RUN402_ALLOWANCE_PATH;
package/lib/config.mjs CHANGED
@@ -3,7 +3,17 @@
3
3
  * Adds CLI-specific behavior: process.exit() on errors.
4
4
  */
5
5
 
6
- import { getApiBase, getConfigDir, getKeystorePath, getAllowancePath } from "../core-dist/config.js";
6
+ import {
7
+ getApiBase,
8
+ getApiBaseSource,
9
+ getApiTargetKind,
10
+ getConfigDir,
11
+ getKeystorePath,
12
+ getAllowancePath,
13
+ configureApiBase,
14
+ isCoreApiTarget,
15
+ readApiTargetConfig,
16
+ } from "../core-dist/config.js";
7
17
  import { readAllowance as coreReadAllowance, saveAllowance as coreSaveAllowance } from "../core-dist/allowance.js";
8
18
  import { loadKeyStore, getProject, saveProject, updateProject, removeProject, saveKeyStore, getActiveProjectId, setActiveProjectId } from "../core-dist/keystore.js";
9
19
  import { getAllowanceAuthHeaders as coreGetAllowanceAuthHeaders } from "../core-dist/allowance-auth.js";
@@ -16,6 +26,10 @@ import { initializeWalletAction, createProjectAction } from "./next-actions.mjs"
16
26
  export function configDir() { return getConfigDir(); }
17
27
  export function allowanceFile() { return getAllowancePath(); }
18
28
  export function projectsFile() { return getKeystorePath(); }
29
+ export function apiBase() { return getApiBase(); }
30
+ export function apiBaseSource() { return getApiBaseSource(); }
31
+ export function apiTargetKind() { return getApiTargetKind(); }
32
+ export function coreTarget() { return isCoreApiTarget(); }
19
33
 
20
34
  // Snapshot constants, retained for backward compatibility (tests, the OpenClaw
21
35
  // config re-export). These are evaluated when this module is first imported.
@@ -116,4 +130,15 @@ export function resolveProjectId(id) {
116
130
  }
117
131
 
118
132
  // Re-export core keystore functions for direct use
119
- export { loadKeyStore, saveProject, updateProject, removeProject, saveKeyStore, getActiveProjectId, setActiveProjectId };
133
+ export {
134
+ configureApiBase,
135
+ isCoreApiTarget,
136
+ readApiTargetConfig,
137
+ loadKeyStore,
138
+ saveProject,
139
+ updateProject,
140
+ removeProject,
141
+ saveKeyStore,
142
+ getActiveProjectId,
143
+ setActiveProjectId,
144
+ };
package/lib/deploy-v2.mjs CHANGED
@@ -32,7 +32,7 @@ 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";
@@ -124,6 +124,11 @@ Routes:
124
124
  Routes activate atomically with the release. Direct /functions/v1/:name remains API-key protected.
125
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.
126
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
+
127
132
  Internationalization (routed functions):
128
133
  "i18n": { "defaultLocale": "en", "locales": ["en", "es", "fr"], "detect": ["cookie:wl_locale", "accept-language"] }
129
134
  Omit i18n to carry forward from base release; pass "i18n": null to clear the slice on the new release.
@@ -664,7 +669,16 @@ async function mergeAstroReleaseSlice(spec, dirArg) {
664
669
  * the manifest normalizer is the authority on shape validity, not this
665
670
  * best-effort extractor.
666
671
  */
667
- function collectManifestSourceFiles(spec, baseDir) {
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) {
668
682
  const out = new Set();
669
683
  if (!spec || typeof spec !== "object") return [];
670
684
 
@@ -689,13 +703,20 @@ function collectManifestSourceFiles(spec, baseDir) {
689
703
  if (!map || typeof map !== "object") return;
690
704
  for (const entry of Object.values(map)) resolveEntryPath(entry);
691
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
+ };
692
713
 
693
714
  const fns = spec.functions;
694
715
  if (fns && typeof fns === "object") {
695
- eachValue(fns.replace);
716
+ eachFunctionValue(fns.replace);
696
717
  if (fns.patch && typeof fns.patch === "object") {
697
- eachValue(fns.patch.put);
698
- eachValue(fns.patch.set);
718
+ eachFunctionValue(fns.patch.put);
719
+ eachFunctionValue(fns.patch.set);
699
720
  }
700
721
  }
701
722
 
@@ -896,9 +917,10 @@ async function applyCmd(args) {
896
917
  credentials: githubActionsCredentials({ projectId: releaseSpec.project, apiBase: API }),
897
918
  disablePaidFetch: true,
898
919
  };
899
- } else if (!loadLiveControlPlaneSession()) {
920
+ } else if (!isCoreApiTarget() && !loadLiveControlPlaneSession()) {
900
921
  // Aggressive early exit when no allowance is configured — unless a
901
- // 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.
902
924
  allowanceAuthHeaders("/apply/v1/plans");
903
925
  }
904
926
 
@@ -909,6 +931,7 @@ async function applyCmd(args) {
909
931
  idempotencyKey,
910
932
  allowWarnings: opts.allowWarnings,
911
933
  allowWarningCodes: opts.allowWarningCodes,
934
+ target: isCoreApiTarget() ? "core" : "cloud",
912
935
  }),
913
936
  );
914
937
  console.log(JSON.stringify(result, null, 2));
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();
package/lib/projects.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import { readFileSync } from "fs";
2
- import { loadKeyStore, API, allowanceAuthHeaders, resolveProjectId, getActiveProjectId } from "./config.mjs";
2
+ import { loadKeyStore, API, allowanceAuthHeaders, resolveProjectId, getActiveProjectId, isCoreApiTarget } from "./config.mjs";
3
3
  import { loadLiveOperatorSession } from "../core-dist/operator-session.js";
4
4
  import { loadLiveControlPlaneSession } from "../core-dist/control-plane-session.js";
5
5
  import { withAutoApprove } from "./operator.mjs";
@@ -14,7 +14,7 @@ Usage:
14
14
 
15
15
  Subcommands:
16
16
  quote Show pricing tiers
17
- provision [--tier <tier>] [--name <n>] [--org <id>] Provision a new Postgres project (pays via x402)
17
+ provision [--tier <tier>] [--name <n>] [--org <id>] Provision a new Postgres project
18
18
  use <id> Set the active project (used as default for other commands)
19
19
  list [--org <id>] [--all] List your projects from the server (name, site_url, custom domains, org_id, active marker)
20
20
  rename <id> --name <label> Rename a project (fix an auto-generated name)
@@ -79,7 +79,9 @@ Notes:
79
79
  project:write grant) on the owning org; it works even if the project was
80
80
  never provisioned from this machine.
81
81
  - 'rest' uses PostgREST query syntax (table name + optional query string)
82
- - 'provision' requires a funded allowance payment is automatic via x402
82
+ - 'provision' requires a funded allowance on Run402 Cloud. Against a
83
+ configured Run402 Core target, it creates a local Core project without
84
+ payment.
83
85
  - 'apply-expose' declares the full authorization surface (tables, views, RPCs)
84
86
  in one convergent call. Tables not listed with expose:true are dark by
85
87
  default. Schema: https://run402.com/schemas/manifest.v1.json. Sample:
@@ -161,7 +163,9 @@ Options:
161
163
  from --name when omitted; an unnamed provision stays un-keyed.
162
164
 
163
165
  Notes:
164
- - Payment is automatic via x402; requires a funded allowance
166
+ - Payment is automatic via x402 on Run402 Cloud; requires a funded allowance.
167
+ Against a configured Run402 Core target, no Cloud tier/allowance/payment is
168
+ required.
165
169
  - The new project becomes the active project after provisioning
166
170
 
167
171
  Examples:
@@ -320,7 +324,7 @@ async function provision(args) {
320
324
  // Aggressive early exit when no agent allowance is configured — but only when
321
325
  // there's also no operator (control-plane) session, since a wallet-less human
322
326
  // provisions into an org via their operator approval instead of a wallet.
323
- if (!loadLiveControlPlaneSession()) allowanceAuthHeaders("/projects/v1");
327
+ if (!isCoreApiTarget() && !loadLiveControlPlaneSession()) allowanceAuthHeaders("/projects/v1");
324
328
 
325
329
  const activeBefore = getActiveProjectId();
326
330
  try {
@@ -744,7 +748,10 @@ async function deleteProject(projectId, args = []) {
744
748
  }
745
749
 
746
750
  const FLAGS_BY_SUB = {
747
- provision: { known: ["--tier", "--name", "--org"], values: ["--tier", "--name", "--org"] },
751
+ provision: {
752
+ known: ["--tier", "--name", "--org", "--idempotency-key"],
753
+ values: ["--tier", "--name", "--org", "--idempotency-key"],
754
+ },
748
755
  list: { known: ["--org", "--all"], values: ["--org"] },
749
756
  rename: { known: ["--name"], values: ["--name"] },
750
757
  sql: { known: ["--file", "--params"], values: ["--file", "--params"] },
package/lib/status.mjs CHANGED
@@ -1,4 +1,11 @@
1
- import { readAllowance, loadKeyStore, getActiveProjectId } from "./config.mjs";
1
+ import {
2
+ readAllowance,
3
+ loadKeyStore,
4
+ getActiveProjectId,
5
+ apiBase,
6
+ apiBaseSource,
7
+ apiTargetKind,
8
+ } from "./config.mjs";
2
9
  import { getSdk } from "./sdk.mjs";
3
10
  import { assertKnownFlags, hasHelp, normalizeArgv } from "./argparse.mjs";
4
11
  import { getActiveProfile } from "../core-dist/config.js";
@@ -16,8 +23,10 @@ Displays:
16
23
  - Tier subscription (name, status, expiry)
17
24
  - Projects (from server, with fallback to local keystore)
18
25
  - Active project ID
26
+ - Active API target
19
27
 
20
- Output is JSON. Requires an existing allowance (run 'run402 init' first).
28
+ Output is JSON. Run402 Cloud status requires an allowance; Core target status
29
+ can still report local project state without one.
21
30
  `;
22
31
 
23
32
  // USDC / pathUSD constants (match allowance.mjs)
@@ -82,8 +91,20 @@ export async function run(args = []) {
82
91
  if (hasHelp(args)) { console.log(HELP); process.exit(0); }
83
92
  assertKnownFlags(args, ["--help", "-h"]);
84
93
  const allowance = readAllowance();
94
+ const target = {
95
+ api_base: apiBase(),
96
+ api_base_source: apiBaseSource(),
97
+ kind: apiTargetKind(),
98
+ };
85
99
  if (!allowance) {
86
- console.log(JSON.stringify({ wallet: null, hint: "Run: run402 init" }));
100
+ const store = loadKeyStore();
101
+ console.log(JSON.stringify({
102
+ wallet: null,
103
+ target,
104
+ projects: Object.keys(store.projects).map(id => ({ project_id: id })),
105
+ active_project: getActiveProjectId() || null,
106
+ hint: target.kind === "core" ? "Run: run402 projects provision --name my-app" : "Run: run402 init",
107
+ }));
87
108
  return;
88
109
  }
89
110
 
@@ -145,6 +166,7 @@ export async function run(args = []) {
145
166
  lease_perpetual: tier?.lease_perpetual ?? null,
146
167
  projects,
147
168
  active_project: activeId || null,
169
+ target,
148
170
  };
149
171
 
150
172
  console.log(JSON.stringify(result, null, 2));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "run402",
3
- "version": "3.5.4",
3
+ "version": "3.5.5",
4
4
  "description": "CLI for Run402 — provision Postgres databases, deploy static sites, generate images, and manage wallets via x402 and MPP micropayments.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,7 +1,8 @@
1
1
  import { homedir } from "node:os";
2
- import { join } from "node:path";
3
- import { existsSync, renameSync, mkdirSync, chmodSync } from "node:fs";
4
- const DEFAULT_API_BASE = "https://api.run402.com";
2
+ import { dirname, join } from "node:path";
3
+ import { existsSync, renameSync, mkdirSync, chmodSync, readFileSync, writeFileSync } from "node:fs";
4
+ import { randomBytes } from "node:crypto";
5
+ export const DEFAULT_API_BASE = "https://api.run402.com";
5
6
  /**
6
7
  * Validate a user-supplied API base URL. Throws a clear error message that
7
8
  * names the env var when the URL is malformed or uses a scheme other than
@@ -33,7 +34,12 @@ function validateApiBase(envVar, raw, fallback) {
33
34
  }
34
35
  export function getApiBase() {
35
36
  const validated = validateApiBase("RUN402_API_BASE", process.env.RUN402_API_BASE, DEFAULT_API_BASE);
36
- return validated ?? DEFAULT_API_BASE;
37
+ return validated ?? getConfiguredApiBase() ?? DEFAULT_API_BASE;
38
+ }
39
+ export function getApiBaseSource() {
40
+ if (process.env.RUN402_API_BASE !== undefined)
41
+ return "env";
42
+ return getConfiguredApiBase() ? "profile" : "default";
37
43
  }
38
44
  /**
39
45
  * API base for the deploy-v2 routes. Defaults to the same value as
@@ -115,6 +121,82 @@ export function getConfigDir() {
115
121
  export function getKeystorePath() {
116
122
  return join(getConfigDir(), "projects.json");
117
123
  }
124
+ export function getApiTargetConfigPath() {
125
+ return join(getConfigDir(), "target.json");
126
+ }
127
+ export function readApiTargetConfig(path) {
128
+ const p = path ?? getApiTargetConfigPath();
129
+ try {
130
+ const parsed = JSON.parse(readFileSync(p, "utf-8"));
131
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
132
+ return null;
133
+ const cfg = parsed;
134
+ if (cfg.api_base !== undefined && typeof cfg.api_base !== "string")
135
+ return null;
136
+ if (cfg.target_kind !== undefined &&
137
+ cfg.target_kind !== "cloud" &&
138
+ cfg.target_kind !== "core" &&
139
+ cfg.target_kind !== "unknown") {
140
+ return null;
141
+ }
142
+ return cfg;
143
+ }
144
+ catch {
145
+ return null;
146
+ }
147
+ }
148
+ function atomicWrite(path, content, mode) {
149
+ const dir = dirname(path);
150
+ mkdirSync(dir, { recursive: true });
151
+ const tmp = join(dir, `.target.${randomBytes(4).toString("hex")}.tmp`);
152
+ writeFileSync(tmp, content, { mode });
153
+ renameSync(tmp, path);
154
+ try {
155
+ chmodSync(path, mode);
156
+ }
157
+ catch {
158
+ /* best-effort on non-POSIX */
159
+ }
160
+ }
161
+ export function saveApiTargetConfig(config, path) {
162
+ const p = path ?? getApiTargetConfigPath();
163
+ atomicWrite(p, JSON.stringify(config, null, 2), 0o600);
164
+ }
165
+ export function configureApiBase(apiBase, options = {}) {
166
+ if (apiBase === "") {
167
+ throw new Error("api_base must be a non-empty http(s) URL.");
168
+ }
169
+ const validated = validateApiBase("api_base", apiBase, DEFAULT_API_BASE);
170
+ if (!validated) {
171
+ throw new Error("api_base must be a non-empty http(s) URL.");
172
+ }
173
+ const config = {
174
+ api_base: validated.replace(/\/+$/, ""),
175
+ target_kind: options.target_kind ?? "unknown",
176
+ updated_at: options.updated_at ?? new Date().toISOString(),
177
+ ...(options.health_status ? { health_status: options.health_status } : {}),
178
+ ...(options.health_error ? { health_error: options.health_error } : {}),
179
+ };
180
+ saveApiTargetConfig(config);
181
+ return config;
182
+ }
183
+ export function getConfiguredApiBase() {
184
+ const cfg = readApiTargetConfig();
185
+ if (!cfg?.api_base)
186
+ return null;
187
+ const validated = validateApiBase("api_base", cfg.api_base, DEFAULT_API_BASE);
188
+ return validated ? validated.replace(/\/+$/, "") : null;
189
+ }
190
+ export function getApiTargetKind() {
191
+ const cfg = readApiTargetConfig();
192
+ return cfg?.target_kind ?? "unknown";
193
+ }
194
+ export function isCoreApiTarget() {
195
+ const cfg = readApiTargetConfig();
196
+ if (cfg?.target_kind !== "core" || !cfg.api_base)
197
+ return false;
198
+ return getApiBase().replace(/\/+$/, "") === cfg.api_base.replace(/\/+$/, "");
199
+ }
118
200
  export function getAllowancePath() {
119
201
  if (process.env.RUN402_ALLOWANCE_PATH)
120
202
  return process.env.RUN402_ALLOWANCE_PATH;
@@ -1 +1 @@
1
- {"version":3,"file":"deploy.d.ts","sourceRoot":"","sources":["../../src/namespaces/deploy.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"deploy.d.ts","sourceRoot":"","sources":["../../src/namespaces/deploy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAkB3C,OAAO,KAAK,EACV,YAAY,EACZ,sBAAsB,EAQtB,WAAW,EACX,oBAAoB,EAEpB,iBAAiB,EACjB,kBAAkB,EAClB,eAAe,EACf,YAAY,EACZ,oBAAoB,EACpB,qBAAqB,EAarB,iBAAiB,EAGjB,YAAY,EACZ,cAAc,EACd,aAAa,EACb,kBAAkB,EAClB,gBAAgB,EAChB,2BAA2B,EAC3B,uBAAuB,EACvB,WAAW,EACX,oBAAoB,EACpB,YAAY,EAEb,MAAM,mBAAmB,CAAC;AAgF3B,qBAAa,MAAM;IACL,OAAO,CAAC,QAAQ,CAAC,MAAM;gBAAN,MAAM,EAAE,MAAM;IAE3C;;;;OAIG;IACG,KAAK,CAAC,IAAI,EAAE,WAAW,EAAE,IAAI,GAAE,YAAiB,GAAG,OAAO,CAAC,YAAY,CAAC;IA4C9E;;;OAGG;IACH,KAAK,CAAC,IAAI,EAAE,WAAW,EAAE,IAAI,GAAE,YAAiB,GAAG,OAAO,CAAC,eAAe,CAAC;IAI3E;;;;OAIG;IACG,IAAI,CACR,IAAI,EAAE,WAAW,EACjB,IAAI,GAAE;QAAE,cAAc,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,OAAO,CAAA;KAAO,GACvD,OAAO,CAAC;QAAE,IAAI,EAAE,YAAY,CAAC;QAAC,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,CAAA;KAAE,CAAC;IAIxE;;;;;OAKG;IACG,MAAM,CACV,IAAI,EAAE,YAAY,EAClB,IAAI,EAAE;QACJ,OAAO,EAAE,MAAM,CAAC;QAChB,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;QACrC,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,WAAW,KAAK,IAAI,CAAC;KACxC,GACA,OAAO,CAAC,IAAI,CAAC;IAWhB;;;;;OAKG;IACG,MAAM,CACV,MAAM,EAAE,MAAM,EACd,IAAI,GAAE;QACJ,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,WAAW,KAAK,IAAI,CAAC;QACvC,cAAc,CAAC,EAAE,MAAM,CAAC;QACxB,OAAO,CAAC,EAAE,MAAM,CAAC;KACb,GACL,OAAO,CAAC,YAAY,CAAC;IASxB;;;;;;;;;OASG;IACG,MAAM,CACV,WAAW,EAAE,MAAM,EACnB,IAAI,GAAE;QAAE,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,WAAW,KAAK,IAAI,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAO,GACtE,OAAO,CAAC,YAAY,CAAC;IAqBxB;;;;;;;;;;;;;;;;;;;;;;;;;;;OA2BG;IACG,OAAO,CACX,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,EACjB,IAAI,GAAE,cAAmB,GACxB,OAAO,CAAC,aAAa,CAAC;IA2CzB;;;;OAIG;IACG,MAAM,CACV,WAAW,EAAE,MAAM,EACnB,IAAI,GAAE;QAAE,OAAO,CAAC,EAAE,MAAM,CAAA;KAAO,GAC9B,OAAO,CAAC,iBAAiB,CAAC;IAmB7B;;;;;;OAMG;IACG,IAAI,CACR,IAAI,EAAE,MAAM,GAAG,iBAAiB,GAC/B,OAAO,CAAC,kBAAkB,CAAC;IA6B9B;;;;;;;;OAQG;IACG,MAAM,CACV,WAAW,EAAE,MAAM,EACnB,IAAI,EAAE;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,GACxB,OAAO,CAAC,oBAAoB,CAAC;IAmBhC;;;;OAIG;IACG,UAAU,CAAC,IAAI,EAAE,2BAA2B,GAAG,OAAO,CAAC,gBAAgB,CAAC;IA2B9E;;;;OAIG;IACG,gBAAgB,CACpB,IAAI,EAAE,uBAAuB,GAC5B,OAAO,CAAC,sBAAsB,CAAC;IAqBlC;;;;OAIG;IACG,IAAI,CAAC,IAAI,EAAE,kBAAkB,GAAG,OAAO,CAAC,oBAAoB,CAAC;IA+BnE;;;;OAIG;IACG,OAAO,CAAC,IAAI,EAAE,oBAAoB,GAAG,OAAO,CAAC,qBAAqB,CAAC;CAW1E;AA2oDD;;;;;GAKG;AACH,MAAM,WAAW,UAAU;IACzB,IAAI,OAAO,CAAC,UAAU,CAAC,CAAC;IACxB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;iEAG6D;IAC7D,KAAK,CAAC,EAAE,SAAS,GAAG,OAAO,GAAG,OAAO,CAAC;CACvC"}