@stigmer/cli 3.12.9 → 3.14.0-dev.20260910084630
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/auth/login.d.ts.map +1 -1
- package/auth/login.js +30 -5
- package/auth/login.js.map +1 -1
- package/auth/token.d.ts +10 -5
- package/auth/token.d.ts.map +1 -1
- package/auth/token.js +30 -21
- package/auth/token.js.map +1 -1
- package/commands/apikey/index.d.ts.map +1 -1
- package/commands/apikey/index.js +11 -5
- package/commands/apikey/index.js.map +1 -1
- package/commands/auth/index.d.ts.map +1 -1
- package/commands/auth/index.js +19 -8
- package/commands/auth/index.js.map +1 -1
- package/commands/config/backend.d.ts.map +1 -1
- package/commands/config/backend.js +134 -12
- package/commands/config/backend.js.map +1 -1
- package/commands/connect.d.ts.map +1 -1
- package/commands/connect.js +6 -3
- package/commands/connect.js.map +1 -1
- package/commands/share.d.ts.map +1 -1
- package/commands/share.js +8 -6
- package/commands/share.js.map +1 -1
- package/commands/up.d.ts.map +1 -1
- package/commands/up.js +22 -6
- package/commands/up.js.map +1 -1
- package/config/config.d.ts +56 -7
- package/config/config.d.ts.map +1 -1
- package/config/config.js +115 -16
- package/config/config.js.map +1 -1
- package/config/index.d.ts +1 -1
- package/config/index.d.ts.map +1 -1
- package/config/index.js +1 -1
- package/config/index.js.map +1 -1
- package/config/keys.d.ts +1 -1
- package/config/keys.d.ts.map +1 -1
- package/config/keys.js +29 -6
- package/config/keys.js.map +1 -1
- package/config/resolve.d.ts +32 -21
- package/config/resolve.d.ts.map +1 -1
- package/config/resolve.js +80 -39
- package/config/resolve.js.map +1 -1
- package/local/constants.d.ts +8 -3
- package/local/constants.d.ts.map +1 -1
- package/local/constants.js +8 -3
- package/local/constants.js.map +1 -1
- package/local/daemon/components.d.ts.map +1 -1
- package/local/daemon/components.js +4 -2
- package/local/daemon/components.js.map +1 -1
- package/local/daemon/env.d.ts +16 -3
- package/local/daemon/env.d.ts.map +1 -1
- package/local/daemon/env.js +16 -3
- package/local/daemon/env.js.map +1 -1
- package/local/daemon/launch.d.ts.map +1 -1
- package/local/daemon/launch.js +5 -4
- package/local/daemon/launch.js.map +1 -1
- package/local/daemon/process.d.ts.map +1 -1
- package/local/daemon/process.js +8 -5
- package/local/daemon/process.js.map +1 -1
- package/local/runtime/index.d.ts +2 -2
- package/local/runtime/index.d.ts.map +1 -1
- package/local/runtime/index.js +4 -2
- package/local/runtime/index.js.map +1 -1
- package/local/runtime/node.d.ts +8 -0
- package/local/runtime/node.d.ts.map +1 -1
- package/local/runtime/node.js +74 -23
- package/local/runtime/node.js.map +1 -1
- package/local/runtime/runner.d.ts +2 -1
- package/local/runtime/runner.d.ts.map +1 -1
- package/local/runtime/runner.js +4 -31
- package/local/runtime/runner.js.map +1 -1
- package/local/runtime/runtimes-install.d.ts +13 -0
- package/local/runtime/runtimes-install.d.ts.map +1 -0
- package/local/runtime/runtimes-install.js +60 -0
- package/local/runtime/runtimes-install.js.map +1 -0
- package/local/runtime/server.d.ts +26 -30
- package/local/runtime/server.d.ts.map +1 -1
- package/local/runtime/server.js +106 -108
- package/local/runtime/server.js.map +1 -1
- package/local/seedpack/content.d.ts +3 -3
- package/local/seedpack/content.d.ts.map +1 -1
- package/local/seedpack/content.js +19 -13
- package/local/seedpack/content.js.map +1 -1
- package/local/status.d.ts.map +1 -1
- package/local/status.js +4 -2
- package/local/status.js.map +1 -1
- package/local/webconsole/index.d.ts +2 -3
- package/local/webconsole/index.d.ts.map +1 -1
- package/local/webconsole/index.js +24 -10
- package/local/webconsole/index.js.map +1 -1
- package/package.json +5 -5
- package/resources/connect/connect.d.ts +4 -3
- package/resources/connect/connect.d.ts.map +1 -1
- package/resources/connect/connect.js +17 -5
- package/resources/connect/connect.js.map +1 -1
- package/resources/connect/oauth.d.ts +5 -2
- package/resources/connect/oauth.d.ts.map +1 -1
- package/resources/connect/oauth.js +5 -4
- package/resources/connect/oauth.js.map +1 -1
- package/src/auth/login.ts +54 -9
- package/src/auth/token.test.ts +69 -13
- package/src/auth/token.ts +46 -22
- package/src/client/client.test.ts +10 -2
- package/src/commands/apikey/index.ts +42 -11
- package/src/commands/auth/index.ts +52 -14
- package/src/commands/config/backend.test.ts +122 -0
- package/src/commands/config/backend.ts +195 -14
- package/src/commands/connect.test.ts +22 -4
- package/src/commands/connect.ts +37 -13
- package/src/commands/share.ts +36 -16
- package/src/commands/up.ts +24 -6
- package/src/config/config.test.ts +62 -6
- package/src/config/config.ts +165 -24
- package/src/config/index.ts +6 -0
- package/src/config/keys.test.ts +27 -4
- package/src/config/keys.ts +46 -9
- package/src/config/resolve.test.ts +135 -34
- package/src/config/resolve.ts +88 -38
- package/src/local/constants.ts +8 -4
- package/src/local/daemon/components.test.ts +9 -1
- package/src/local/daemon/components.ts +4 -2
- package/src/local/daemon/daemon.integration.test.ts +1 -1
- package/src/local/daemon/env.test.ts +15 -4
- package/src/local/daemon/env.ts +33 -5
- package/src/local/daemon/launch.ts +5 -4
- package/src/local/daemon/process.ts +7 -4
- package/src/local/runtime/index.ts +6 -5
- package/src/local/runtime/node.ts +99 -23
- package/src/local/runtime/runner.ts +5 -36
- package/src/local/runtime/runtime.test.ts +58 -14
- package/src/local/runtime/runtimes-install.test.ts +56 -0
- package/src/local/runtime/runtimes-install.ts +72 -0
- package/src/local/runtime/server.test.ts +178 -114
- package/src/local/runtime/server.ts +149 -129
- package/src/local/seedpack/content.ts +52 -21
- package/src/local/status.ts +4 -2
- package/src/local/webconsole/index.ts +27 -10
- package/src/resources/connect/connect.integration.test.ts +79 -26
- package/src/resources/connect/connect.ts +72 -19
- package/src/resources/connect/oauth.test.ts +38 -12
- package/src/resources/connect/oauth.ts +27 -11
- package/src/resources/share.test.ts +2 -2
package/src/config/keys.ts
CHANGED
|
@@ -3,7 +3,12 @@
|
|
|
3
3
|
// managed by their own (later) commands and preserved opaquely on save.
|
|
4
4
|
|
|
5
5
|
import { UsageError } from "../errors/usage-error.js";
|
|
6
|
-
import
|
|
6
|
+
import {
|
|
7
|
+
CLOUD_BACKEND_NAME,
|
|
8
|
+
type Config,
|
|
9
|
+
LOCAL_BACKEND_NAME,
|
|
10
|
+
activeBackendName,
|
|
11
|
+
} from "./config.js";
|
|
7
12
|
|
|
8
13
|
interface ConfigKey {
|
|
9
14
|
/** Stored (non-resolved) value, "" when unset. */
|
|
@@ -12,26 +17,54 @@ interface ConfigKey {
|
|
|
12
17
|
set(config: Config, value: string): void;
|
|
13
18
|
}
|
|
14
19
|
|
|
20
|
+
// The backend.cloud.* keys predate named backends and remain the documented
|
|
21
|
+
// kv surface for the reserved "cloud" entry; `current_backend` is the named
|
|
22
|
+
// model's own key. `backend.type` stays the simple local/cloud switch
|
|
23
|
+
// (named backends switch through `config backend use`).
|
|
15
24
|
const KEYS: Record<string, ConfigKey> = {
|
|
16
25
|
"backend.type": {
|
|
17
|
-
get: (config) =>
|
|
26
|
+
get: (config) =>
|
|
27
|
+
activeBackendName(config) === LOCAL_BACKEND_NAME ? "local" : "cloud",
|
|
18
28
|
set: (config, value) => {
|
|
19
29
|
if (value !== "local" && value !== "cloud") {
|
|
20
|
-
throw new UsageError(
|
|
30
|
+
throw new UsageError(
|
|
31
|
+
`invalid backend.type "${value}" (expected: local, cloud)`,
|
|
32
|
+
);
|
|
21
33
|
}
|
|
22
|
-
|
|
34
|
+
if (value === "cloud") {
|
|
35
|
+
(config.backends ??= {})[CLOUD_BACKEND_NAME] ??= { type: "cloud" };
|
|
36
|
+
config.current_backend = CLOUD_BACKEND_NAME;
|
|
37
|
+
} else {
|
|
38
|
+
config.current_backend = LOCAL_BACKEND_NAME;
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
current_backend: {
|
|
43
|
+
get: (config) => activeBackendName(config),
|
|
44
|
+
set: (config, value) => {
|
|
45
|
+
if (
|
|
46
|
+
value !== LOCAL_BACKEND_NAME &&
|
|
47
|
+
config.backends?.[value] === undefined
|
|
48
|
+
) {
|
|
49
|
+
throw new UsageError(
|
|
50
|
+
`unknown backend "${value}" (add it first: stigmer config backend add)`,
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
config.current_backend = value;
|
|
23
54
|
},
|
|
24
55
|
},
|
|
25
56
|
"backend.cloud.endpoint": {
|
|
26
|
-
get: (config) => config.
|
|
57
|
+
get: (config) => config.backends?.[CLOUD_BACKEND_NAME]?.endpoint ?? "",
|
|
27
58
|
set: (config, value) => {
|
|
28
|
-
(config.
|
|
59
|
+
const backends = (config.backends ??= {});
|
|
60
|
+
(backends[CLOUD_BACKEND_NAME] ??= { type: "cloud" }).endpoint = value;
|
|
29
61
|
},
|
|
30
62
|
},
|
|
31
63
|
"backend.cloud.org_id": {
|
|
32
|
-
get: (config) => config.
|
|
64
|
+
get: (config) => config.backends?.[CLOUD_BACKEND_NAME]?.org_id ?? "",
|
|
33
65
|
set: (config, value) => {
|
|
34
|
-
(config.
|
|
66
|
+
const backends = (config.backends ??= {});
|
|
67
|
+
(backends[CLOUD_BACKEND_NAME] ??= { type: "cloud" }).org_id = value;
|
|
35
68
|
},
|
|
36
69
|
},
|
|
37
70
|
"context.organization": {
|
|
@@ -53,7 +86,11 @@ export function getConfigValue(config: Config, key: string): string {
|
|
|
53
86
|
return entry.get(config);
|
|
54
87
|
}
|
|
55
88
|
|
|
56
|
-
export function setConfigValue(
|
|
89
|
+
export function setConfigValue(
|
|
90
|
+
config: Config,
|
|
91
|
+
key: string,
|
|
92
|
+
value: string,
|
|
93
|
+
): void {
|
|
57
94
|
const entry = KEYS[key];
|
|
58
95
|
if (entry === undefined) throw new UsageError(unknownKeyMessage(key));
|
|
59
96
|
entry.set(config, value);
|
|
@@ -1,6 +1,11 @@
|
|
|
1
|
+
// Pins the effective-value resolvers over the named-backend model (O3):
|
|
2
|
+
// env > active entry > defaults for endpoint/token/org/console-URL, the
|
|
3
|
+
// selfhost credential lane (api_key), and the auth gate's three postures
|
|
4
|
+
// (local never, selfhost never client-side, cloud requires a credential).
|
|
5
|
+
|
|
1
6
|
import { afterEach, describe, expect, it } from "vitest";
|
|
2
7
|
import { CliExitError } from "../errors/index.js";
|
|
3
|
-
import type { Config } from "./config.js";
|
|
8
|
+
import type { Config, NamedBackendConfig } from "./config.js";
|
|
4
9
|
import {
|
|
5
10
|
DEFAULT_CLOUD_CONSOLE_URL,
|
|
6
11
|
ensureAuthenticated,
|
|
@@ -11,29 +16,78 @@ import {
|
|
|
11
16
|
resolveToken,
|
|
12
17
|
} from "./resolve.js";
|
|
13
18
|
|
|
14
|
-
const ENV_KEYS = [
|
|
19
|
+
const ENV_KEYS = [
|
|
20
|
+
"STIGMER_SERVER_ADDRESS",
|
|
21
|
+
"STIGMER_API_KEY",
|
|
22
|
+
"STIGMER_ORG_ID",
|
|
23
|
+
] as const;
|
|
15
24
|
|
|
16
25
|
afterEach(() => {
|
|
17
26
|
for (const key of ENV_KEYS) delete process.env[key];
|
|
18
27
|
});
|
|
19
28
|
|
|
20
|
-
function
|
|
21
|
-
return {
|
|
29
|
+
function localConfig(context?: Config["context"]): Config {
|
|
30
|
+
return {
|
|
31
|
+
backend: { type: "local" },
|
|
32
|
+
backends: {},
|
|
33
|
+
current_backend: "local",
|
|
34
|
+
context,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function cloudConfig(
|
|
39
|
+
overrides: Partial<NamedBackendConfig> = {},
|
|
40
|
+
context?: Config["context"],
|
|
41
|
+
): Config {
|
|
42
|
+
return {
|
|
43
|
+
backend: { type: "cloud" },
|
|
44
|
+
backends: {
|
|
45
|
+
cloud: { type: "cloud", endpoint: "cloud.example:443", ...overrides },
|
|
46
|
+
},
|
|
47
|
+
current_backend: "cloud",
|
|
48
|
+
context,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function selfhostConfig(overrides: Partial<NamedBackendConfig> = {}): Config {
|
|
53
|
+
return {
|
|
54
|
+
backend: { type: "cloud" },
|
|
55
|
+
backends: {
|
|
56
|
+
staging: {
|
|
57
|
+
type: "selfhost",
|
|
58
|
+
endpoint: "stigmer.example.com:7234",
|
|
59
|
+
...overrides,
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
current_backend: "staging",
|
|
63
|
+
};
|
|
22
64
|
}
|
|
23
65
|
|
|
24
66
|
describe("resolveEndpoint", () => {
|
|
25
67
|
it("defaults to the local daemon address in local mode", () => {
|
|
26
|
-
expect(resolveEndpoint(
|
|
68
|
+
expect(resolveEndpoint(localConfig())).toBe("localhost:7234");
|
|
27
69
|
});
|
|
28
70
|
|
|
29
71
|
it("uses the cloud default when no endpoint is configured", () => {
|
|
30
|
-
expect(resolveEndpoint({
|
|
72
|
+
expect(resolveEndpoint(cloudConfig({ endpoint: undefined }))).toBe(
|
|
73
|
+
"api.stigmer.ai:443",
|
|
74
|
+
);
|
|
31
75
|
});
|
|
32
76
|
|
|
33
77
|
it("prefers the configured cloud endpoint", () => {
|
|
34
78
|
expect(resolveEndpoint(cloudConfig())).toBe("cloud.example:443");
|
|
35
79
|
});
|
|
36
80
|
|
|
81
|
+
it("uses the selfhost entry's endpoint", () => {
|
|
82
|
+
expect(resolveEndpoint(selfhostConfig())).toBe("stigmer.example.com:7234");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("refuses a selfhost entry without an endpoint (hand-edited config)", () => {
|
|
86
|
+
expect(() =>
|
|
87
|
+
resolveEndpoint(selfhostConfig({ endpoint: undefined })),
|
|
88
|
+
).toThrow(CliExitError);
|
|
89
|
+
});
|
|
90
|
+
|
|
37
91
|
it("lets STIGMER_SERVER_ADDRESS override everything", () => {
|
|
38
92
|
process.env.STIGMER_SERVER_ADDRESS = "override:7234";
|
|
39
93
|
expect(resolveEndpoint(cloudConfig())).toBe("override:7234");
|
|
@@ -43,15 +97,27 @@ describe("resolveEndpoint", () => {
|
|
|
43
97
|
describe("resolveToken", () => {
|
|
44
98
|
it("prefers STIGMER_API_KEY over the config token", () => {
|
|
45
99
|
process.env.STIGMER_API_KEY = "env-key";
|
|
46
|
-
expect(resolveToken(cloudConfig({ token: "config-token" }))).toBe(
|
|
100
|
+
expect(resolveToken(cloudConfig({ token: "config-token" }))).toBe(
|
|
101
|
+
"env-key",
|
|
102
|
+
);
|
|
47
103
|
});
|
|
48
104
|
|
|
49
|
-
it("falls back to the
|
|
50
|
-
expect(resolveToken(cloudConfig({ token: "config-token" }))).toBe(
|
|
105
|
+
it("falls back to the cloud entry's login token", () => {
|
|
106
|
+
expect(resolveToken(cloudConfig({ token: "config-token" }))).toBe(
|
|
107
|
+
"config-token",
|
|
108
|
+
);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("uses the selfhost entry's api_key", () => {
|
|
112
|
+
expect(resolveToken(selfhostConfig({ api_key: "stk_stored" }))).toBe(
|
|
113
|
+
"stk_stored",
|
|
114
|
+
);
|
|
51
115
|
});
|
|
52
116
|
|
|
53
117
|
it("returns empty string when unauthenticated", () => {
|
|
54
|
-
expect(resolveToken({
|
|
118
|
+
expect(resolveToken(cloudConfig({ endpoint: undefined }))).toBe("");
|
|
119
|
+
expect(resolveToken(selfhostConfig())).toBe("");
|
|
120
|
+
expect(resolveToken(localConfig())).toBe("");
|
|
55
121
|
});
|
|
56
122
|
});
|
|
57
123
|
|
|
@@ -62,64 +128,95 @@ describe("resolveOrganization", () => {
|
|
|
62
128
|
expect(resolveOrganization(config, "flag-org")).toBe("flag-org");
|
|
63
129
|
});
|
|
64
130
|
|
|
65
|
-
it("falls back to STIGMER_ORG_ID, then context, then
|
|
131
|
+
it("falls back to STIGMER_ORG_ID, then context, then the entry's org_id", () => {
|
|
66
132
|
process.env.STIGMER_ORG_ID = "env-org";
|
|
67
|
-
expect(resolveOrganization(cloudConfig({ org_id: "config-org" }))).toBe(
|
|
133
|
+
expect(resolveOrganization(cloudConfig({ org_id: "config-org" }))).toBe(
|
|
134
|
+
"env-org",
|
|
135
|
+
);
|
|
68
136
|
});
|
|
69
137
|
|
|
70
138
|
it("uses context organization when no flag/env is set", () => {
|
|
71
|
-
const config
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
139
|
+
const config = cloudConfig(
|
|
140
|
+
{ org_id: "legacy" },
|
|
141
|
+
{ organization: "ctx-org" },
|
|
142
|
+
);
|
|
75
143
|
expect(resolveOrganization(config)).toBe("ctx-org");
|
|
76
144
|
expect(resolveContextOrganization(config)).toBe("ctx-org");
|
|
77
145
|
});
|
|
78
146
|
|
|
79
|
-
it("falls back to
|
|
80
|
-
expect(resolveContextOrganization(cloudConfig({ org_id: "legacy" }))).toBe(
|
|
147
|
+
it("falls back to the entry's org_id", () => {
|
|
148
|
+
expect(resolveContextOrganization(cloudConfig({ org_id: "legacy" }))).toBe(
|
|
149
|
+
"legacy",
|
|
150
|
+
);
|
|
81
151
|
});
|
|
82
152
|
|
|
83
153
|
it("defaults to the seedpack org in local mode when nothing is configured", () => {
|
|
84
|
-
expect(resolveOrganization(
|
|
154
|
+
expect(resolveOrganization(localConfig())).toBe("stigmer");
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("defaults to the seedpack org on a selfhost backend (it IS the OSS server)", () => {
|
|
158
|
+
expect(resolveOrganization(selfhostConfig())).toBe("stigmer");
|
|
85
159
|
});
|
|
86
160
|
|
|
87
161
|
it("prefers an explicit context org over the local default", () => {
|
|
88
|
-
|
|
89
|
-
|
|
162
|
+
expect(
|
|
163
|
+
resolveOrganization(localConfig({ organization: "my-local-org" })),
|
|
164
|
+
).toBe("my-local-org");
|
|
90
165
|
});
|
|
91
166
|
|
|
92
167
|
it("does not apply the local default in cloud mode (org stays empty)", () => {
|
|
93
|
-
expect(resolveOrganization({
|
|
168
|
+
expect(resolveOrganization(cloudConfig({ endpoint: undefined }))).toBe("");
|
|
94
169
|
});
|
|
95
170
|
|
|
96
171
|
it("reports an unset local org as empty via resolveContextOrganization (no implicit default)", () => {
|
|
97
172
|
// The implicit local default lives only in resolveOrganization (effective
|
|
98
173
|
// org for operations); the context query must stay faithful so config/context
|
|
99
174
|
// display can show "(not set)".
|
|
100
|
-
expect(resolveContextOrganization(
|
|
175
|
+
expect(resolveContextOrganization(localConfig())).toBe("");
|
|
101
176
|
});
|
|
102
177
|
});
|
|
103
178
|
|
|
104
179
|
describe("resolveConsoleURL", () => {
|
|
105
180
|
it("prefers the STIGMER_CONSOLE_URL override", () => {
|
|
106
|
-
expect(
|
|
107
|
-
|
|
108
|
-
|
|
181
|
+
expect(
|
|
182
|
+
resolveConsoleURL(cloudConfig(), {
|
|
183
|
+
STIGMER_CONSOLE_URL: "https://console.example",
|
|
184
|
+
} as NodeJS.ProcessEnv),
|
|
185
|
+
).toBe("https://console.example");
|
|
109
186
|
});
|
|
110
187
|
|
|
111
188
|
it("uses the local web-console port for the local backend", () => {
|
|
112
|
-
expect(resolveConsoleURL(
|
|
189
|
+
expect(resolveConsoleURL(localConfig(), {} as NodeJS.ProcessEnv)).toBe(
|
|
190
|
+
"http://localhost:7234",
|
|
191
|
+
);
|
|
113
192
|
});
|
|
114
193
|
|
|
115
194
|
it("uses the cloud console URL for the cloud backend", () => {
|
|
116
|
-
expect(resolveConsoleURL(
|
|
195
|
+
expect(resolveConsoleURL(cloudConfig(), {} as NodeJS.ProcessEnv)).toBe(
|
|
196
|
+
DEFAULT_CLOUD_CONSOLE_URL,
|
|
197
|
+
);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("derives the selfhost console from the entry's endpoint (one-origin rule)", () => {
|
|
201
|
+
expect(resolveConsoleURL(selfhostConfig(), {} as NodeJS.ProcessEnv)).toBe(
|
|
202
|
+
"http://stigmer.example.com:7234",
|
|
203
|
+
);
|
|
204
|
+
expect(
|
|
205
|
+
resolveConsoleURL(
|
|
206
|
+
selfhostConfig({ endpoint: "stigmer.example.com:443" }),
|
|
207
|
+
{} as NodeJS.ProcessEnv,
|
|
208
|
+
),
|
|
209
|
+
).toBe("https://stigmer.example.com");
|
|
117
210
|
});
|
|
118
211
|
});
|
|
119
212
|
|
|
120
213
|
describe("ensureAuthenticated", () => {
|
|
121
214
|
it("never requires auth in local mode", () => {
|
|
122
|
-
expect(() => ensureAuthenticated(
|
|
215
|
+
expect(() => ensureAuthenticated(localConfig())).not.toThrow();
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it("never gates selfhost client-side (the server owns that decision)", () => {
|
|
219
|
+
expect(() => ensureAuthenticated(selfhostConfig())).not.toThrow();
|
|
123
220
|
});
|
|
124
221
|
|
|
125
222
|
it("throws in cloud mode with no credentials", () => {
|
|
@@ -127,15 +224,19 @@ describe("ensureAuthenticated", () => {
|
|
|
127
224
|
});
|
|
128
225
|
|
|
129
226
|
it("passes with a stored access token", () => {
|
|
130
|
-
expect(() =>
|
|
227
|
+
expect(() =>
|
|
228
|
+
ensureAuthenticated(cloudConfig({ token: "t" })),
|
|
229
|
+
).not.toThrow();
|
|
131
230
|
});
|
|
132
231
|
|
|
133
|
-
it("passes with only a refresh token", () => {
|
|
134
|
-
expect(() =>
|
|
232
|
+
it("passes with only a refresh token (silent refresh will mint one)", () => {
|
|
233
|
+
expect(() =>
|
|
234
|
+
ensureAuthenticated(cloudConfig({ refresh_token: "r" })),
|
|
235
|
+
).not.toThrow();
|
|
135
236
|
});
|
|
136
237
|
|
|
137
|
-
it("passes with STIGMER_API_KEY
|
|
138
|
-
process.env.STIGMER_API_KEY = "
|
|
238
|
+
it("passes with STIGMER_API_KEY in the environment", () => {
|
|
239
|
+
process.env.STIGMER_API_KEY = "stk_env";
|
|
139
240
|
expect(() => ensureAuthenticated(cloudConfig())).not.toThrow();
|
|
140
241
|
});
|
|
141
242
|
});
|
package/src/config/resolve.ts
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
// Effective-value resolution with env > config > default precedence. These are
|
|
2
2
|
// the settings the backend client and read verbs depend on; centralizing the
|
|
3
3
|
// precedence keeps every call site consistent (mirrors the Go CLI's Resolve*).
|
|
4
|
+
//
|
|
5
|
+
// All resolvers read the ACTIVE named backend (config.ts): local is the
|
|
6
|
+
// managed daemon (fixed endpoint, never requires auth), cloud entries carry
|
|
7
|
+
// the OAuth token set, selfhost entries carry an endpoint + api_key.
|
|
4
8
|
|
|
5
9
|
import { CliExitError, ExitCode } from "../errors/index.js";
|
|
6
|
-
import {
|
|
7
|
-
import { type
|
|
10
|
+
import { SERVER_PORT } from "../local/constants.js";
|
|
11
|
+
import { type Config, activeBackend, isCloudMode } from "./config.js";
|
|
8
12
|
|
|
9
13
|
const DEFAULT_CLOUD_ENDPOINT = "api.stigmer.ai:443";
|
|
10
14
|
const DEFAULT_LOCAL_ENDPOINT = "localhost:7234";
|
|
@@ -15,45 +19,68 @@ export const DEFAULT_CLOUD_CONSOLE_URL = "https://app.stigmer.ai";
|
|
|
15
19
|
// Local mode is single-tenant: the seedpack bootstraps resources into the
|
|
16
20
|
// "stigmer" org (see client-apps/cli/src/local/seedpack/apply.ts DEFAULT_ORG).
|
|
17
21
|
// Org-scoped getByReference now requires an org, so bare-slug `get`/`run`/`apply`
|
|
18
|
-
// in local mode fall back to this default when nothing else is configured.
|
|
19
|
-
//
|
|
22
|
+
// in local mode fall back to this default when nothing else is configured. The
|
|
23
|
+
// same holds for selfhost backends — they ARE the OSS server. Cloud mode has no
|
|
24
|
+
// implicit org — the caller must select one (flag/env/context/login).
|
|
20
25
|
const DEFAULT_LOCAL_ORG = "stigmer";
|
|
21
26
|
|
|
22
27
|
/**
|
|
23
28
|
* Resolve the server endpoint.
|
|
24
|
-
* STIGMER_SERVER_ADDRESS
|
|
25
|
-
* >
|
|
26
|
-
* >
|
|
29
|
+
* STIGMER_SERVER_ADDRESS (explicit override, any backend)
|
|
30
|
+
* > the active entry's endpoint (cloud falls back to api.stigmer.ai:443)
|
|
31
|
+
* > localhost:7234 (the local daemon)
|
|
32
|
+
*
|
|
33
|
+
* A selfhost entry without an endpoint is a loud configuration error —
|
|
34
|
+
* `config backend add` requires one, so this only happens to hand-edited
|
|
35
|
+
* files, and guessing an address for a self-hosted server helps nobody.
|
|
27
36
|
*/
|
|
28
37
|
export function resolveEndpoint(config: Config): string {
|
|
29
38
|
const override = process.env.STIGMER_SERVER_ADDRESS;
|
|
30
39
|
if (override !== undefined && override !== "") {
|
|
31
40
|
return override;
|
|
32
41
|
}
|
|
33
|
-
|
|
34
|
-
|
|
42
|
+
const { name, entry } = activeBackend(config);
|
|
43
|
+
if (entry === undefined) {
|
|
44
|
+
return DEFAULT_LOCAL_ENDPOINT;
|
|
45
|
+
}
|
|
46
|
+
if (entry.endpoint !== undefined && entry.endpoint !== "") {
|
|
47
|
+
return entry.endpoint;
|
|
48
|
+
}
|
|
49
|
+
if (entry.type === "cloud") {
|
|
50
|
+
return DEFAULT_CLOUD_ENDPOINT;
|
|
35
51
|
}
|
|
36
|
-
|
|
52
|
+
throw new CliExitError(
|
|
53
|
+
`backend "${name}" has no endpoint configured`,
|
|
54
|
+
ExitCode.Usage,
|
|
55
|
+
[
|
|
56
|
+
`Re-add it with one:`,
|
|
57
|
+
` stigmer config backend add ${name} --endpoint <host:port>`,
|
|
58
|
+
],
|
|
59
|
+
);
|
|
37
60
|
}
|
|
38
61
|
|
|
39
62
|
/**
|
|
40
63
|
* Resolve the auth token.
|
|
41
|
-
* STIGMER_API_KEY
|
|
42
|
-
* > cloud
|
|
43
|
-
* > ""
|
|
64
|
+
* STIGMER_API_KEY (env, also where the --api-key global is bridged)
|
|
65
|
+
* > the active entry's credential (cloud: login token; selfhost: api_key)
|
|
66
|
+
* > "" (unauthenticated)
|
|
44
67
|
*/
|
|
45
68
|
export function resolveToken(config: Config): string {
|
|
46
69
|
const env = process.env.STIGMER_API_KEY;
|
|
47
70
|
if (env !== undefined && env !== "") {
|
|
48
71
|
return env;
|
|
49
72
|
}
|
|
50
|
-
|
|
73
|
+
const { entry } = activeBackend(config);
|
|
74
|
+
if (entry === undefined) {
|
|
75
|
+
return "";
|
|
76
|
+
}
|
|
77
|
+
return (entry.type === "cloud" ? entry.token : entry.api_key) ?? "";
|
|
51
78
|
}
|
|
52
79
|
|
|
53
80
|
/**
|
|
54
81
|
* Resolve the active organization.
|
|
55
82
|
* --org flag override > STIGMER_ORG_ID env > context.organization
|
|
56
|
-
* >
|
|
83
|
+
* > the active entry's org_id (legacy fallback)
|
|
57
84
|
*/
|
|
58
85
|
export function resolveOrganization(config: Config, flagOrg?: string): string {
|
|
59
86
|
if (flagOrg !== undefined && flagOrg !== "") {
|
|
@@ -67,53 +94,76 @@ export function resolveOrganization(config: Config, flagOrg?: string): string {
|
|
|
67
94
|
if (context !== "") {
|
|
68
95
|
return context;
|
|
69
96
|
}
|
|
70
|
-
// Nothing configured: local
|
|
71
|
-
// so bare-slug operations resolve without ceremony; cloud
|
|
72
|
-
// caller must select an org, e.g. apply raises a clear
|
|
97
|
+
// Nothing configured: local and selfhost fall back to the single-tenant
|
|
98
|
+
// default org so bare-slug operations resolve without ceremony; cloud
|
|
99
|
+
// stays empty (the caller must select an org, e.g. apply raises a clear
|
|
100
|
+
// "organization not set").
|
|
73
101
|
return isCloudMode(config) ? "" : DEFAULT_LOCAL_ORG;
|
|
74
102
|
}
|
|
75
103
|
|
|
76
104
|
/**
|
|
77
105
|
* Resolve the web console URL:
|
|
78
106
|
* 1. `STIGMER_CONSOLE_URL` (explicit override)
|
|
79
|
-
* 2. local
|
|
80
|
-
*
|
|
107
|
+
* 2. local daemon → `http://localhost:{SERVER_PORT}` — the server's own
|
|
108
|
+
* unified port serves the console since DD-012 (one origin for UI and
|
|
109
|
+
* API; the Go-era separate 8234 listener is retired)
|
|
110
|
+
* 3. selfhost → the entry's endpoint on the same one-origin rule
|
|
111
|
+
* (https for :443, http otherwise)
|
|
112
|
+
* 4. cloud → {@link DEFAULT_CLOUD_CONSOLE_URL}
|
|
81
113
|
*
|
|
82
114
|
* The console origin is also the app origin that serves the hosted chat
|
|
83
115
|
* page (`/chat/<org>/<slug>`) and the embed loader (`/embed.js`), so
|
|
84
|
-
* share-link builders use this same resolver
|
|
116
|
+
* share-link builders use this same resolver — both routes live in the
|
|
117
|
+
* same static export the server serves.
|
|
85
118
|
*/
|
|
86
|
-
export function resolveConsoleURL(
|
|
119
|
+
export function resolveConsoleURL(
|
|
120
|
+
config: Config,
|
|
121
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
122
|
+
): string {
|
|
87
123
|
const override = env.STIGMER_CONSOLE_URL;
|
|
88
124
|
if (override !== undefined && override !== "") return override;
|
|
89
|
-
|
|
125
|
+
const { entry } = activeBackend(config);
|
|
126
|
+
if (entry === undefined) return `http://localhost:${SERVER_PORT}`;
|
|
127
|
+
if (entry.type === "selfhost") {
|
|
128
|
+
const endpoint = entry.endpoint ?? "";
|
|
129
|
+
const scheme = endpoint.endsWith(":443") ? "https" : "http";
|
|
130
|
+
return `${scheme}://${endpoint.replace(/:443$/, "")}`;
|
|
131
|
+
}
|
|
90
132
|
return DEFAULT_CLOUD_CONSOLE_URL;
|
|
91
133
|
}
|
|
92
134
|
|
|
93
135
|
/**
|
|
94
|
-
* The configured organization, with the
|
|
95
|
-
* only what is explicitly set (no implicit local default) so
|
|
96
|
-
* display can faithfully show "(not set)".
|
|
136
|
+
* The configured organization, with the active entry's org_id fallback.
|
|
137
|
+
* Reports only what is explicitly set (no implicit local default) so
|
|
138
|
+
* config/context display can faithfully show "(not set)".
|
|
97
139
|
*/
|
|
98
140
|
export function resolveContextOrganization(config: Config): string {
|
|
99
|
-
if (
|
|
141
|
+
if (
|
|
142
|
+
config.context?.organization !== undefined &&
|
|
143
|
+
config.context.organization !== ""
|
|
144
|
+
) {
|
|
100
145
|
return config.context.organization;
|
|
101
146
|
}
|
|
102
|
-
return config.
|
|
147
|
+
return activeBackend(config).entry?.org_id ?? "";
|
|
103
148
|
}
|
|
104
149
|
|
|
105
150
|
/**
|
|
106
|
-
* Fail fast with a clear "please log in" error when a
|
|
107
|
-
* usable credential.
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
151
|
+
* Fail fast with a clear "please log in" error when a CLOUD command has no
|
|
152
|
+
* usable credential. The local daemon never requires auth. Selfhost
|
|
153
|
+
* backends are deliberately not gated client-side: an unauthenticated
|
|
154
|
+
* self-host (no issuer configured) is a legitimate posture, and an
|
|
155
|
+
* authenticated one answers UNAUTHENTICATED with a clear message — the
|
|
156
|
+
* server owns that decision. A credential is any of: the STIGMER_API_KEY
|
|
157
|
+
* env (or bridged --api-key), a persisted access token, or a refresh token
|
|
158
|
+
* (which the token provider can exchange for a fresh access token).
|
|
112
159
|
*/
|
|
113
160
|
export function ensureAuthenticated(config: Config): void {
|
|
114
|
-
|
|
161
|
+
const { entry } = activeBackend(config);
|
|
162
|
+
if (entry?.type !== "cloud") return;
|
|
115
163
|
if (process.env.STIGMER_API_KEY) return;
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
164
|
+
if (entry.token || entry.refresh_token) return;
|
|
165
|
+
throw new CliExitError("Not authenticated", ExitCode.Auth, [
|
|
166
|
+
"Please sign in:",
|
|
167
|
+
" stigmer auth login",
|
|
168
|
+
]);
|
|
119
169
|
}
|
package/src/local/constants.ts
CHANGED
|
@@ -7,12 +7,16 @@
|
|
|
7
7
|
// runner-poll can never drift (see the T05 plan's "Critical correctness
|
|
8
8
|
// finding").
|
|
9
9
|
|
|
10
|
-
/**
|
|
10
|
+
/**
|
|
11
|
+
* Port `stigmer-server` listens on (gRPC). Matches the Go CLI's `DaemonPort`.
|
|
12
|
+
*
|
|
13
|
+
* This is also the web console's origin: since DD-012 the server serves the
|
|
14
|
+
* console's static export on this same unified port (lane 4), so the Go-era
|
|
15
|
+
* WEB_CONSOLE_PORT (8234, a separate CLI-embedded listener) is retired —
|
|
16
|
+
* one process, one origin, no CORS hop.
|
|
17
|
+
*/
|
|
11
18
|
export const SERVER_PORT = 7234;
|
|
12
19
|
|
|
13
|
-
/** Port the embedded web console is served on (7xxx=API, 8xxx=UI convention). */
|
|
14
|
-
export const WEB_CONSOLE_PORT = 8234;
|
|
15
|
-
|
|
16
20
|
/** Temporal frontend gRPC port (dev server default). */
|
|
17
21
|
export const TEMPORAL_PORT = 7233;
|
|
18
22
|
|
|
@@ -11,7 +11,7 @@ const config: DaemonConfig = {
|
|
|
11
11
|
temporalAddress: "127.0.0.1:7233",
|
|
12
12
|
serverOnly: false,
|
|
13
13
|
noWeb: false,
|
|
14
|
-
|
|
14
|
+
server: { nodeBin: "/bin/node", entryPath: "/repo/server/dist/main.js", appDir: "/repo/server" },
|
|
15
15
|
runner: { nodeBin: "/bin/node", entryPath: "/repo/runner/dist/main.js", appDir: "/repo/runner" },
|
|
16
16
|
};
|
|
17
17
|
|
|
@@ -133,6 +133,14 @@ describe("buildComponents", () => {
|
|
|
133
133
|
expect(runner.resolve().args).toEqual(["/repo/runner/dist/main.js"]);
|
|
134
134
|
});
|
|
135
135
|
|
|
136
|
+
// The TS server child is `node <entry>` — the runner's launch shape.
|
|
137
|
+
it("spawns the node-shape server as node + entry", () => {
|
|
138
|
+
const [server] = buildComponents(config, {});
|
|
139
|
+
const spawn = server.resolve();
|
|
140
|
+
expect(spawn.command).toBe("/bin/node");
|
|
141
|
+
expect(spawn.args).toEqual(["/repo/server/dist/main.js"]);
|
|
142
|
+
});
|
|
143
|
+
|
|
136
144
|
it("omits the runner in server-only mode", () => {
|
|
137
145
|
const components = buildComponents({ ...config, serverOnly: true }, {});
|
|
138
146
|
expect(components.map((c) => c.name)).toEqual(["stigmer-server"]);
|
|
@@ -91,9 +91,11 @@ export function buildComponents(config: DaemonConfig, base: NodeJS.ProcessEnv =
|
|
|
91
91
|
name: "stigmer-server",
|
|
92
92
|
pidFile: join(config.dataDir, SERVER_PID_FILE),
|
|
93
93
|
critical: true,
|
|
94
|
+
// The server is `node <entry>` — the runner's launch shape, one child
|
|
95
|
+
// contract for both components.
|
|
94
96
|
resolve: () => ({
|
|
95
|
-
command: config.
|
|
96
|
-
args: [],
|
|
97
|
+
command: config.server.nodeBin,
|
|
98
|
+
args: [config.server.entryPath],
|
|
97
99
|
env: buildServerEnv(config, base),
|
|
98
100
|
logFile: join(config.logDir, "stigmer-server.log"),
|
|
99
101
|
}),
|
|
@@ -128,7 +128,7 @@ describe("runInternalDaemon (full body with fakes)", () => {
|
|
|
128
128
|
temporalAddress: "127.0.0.1:7233",
|
|
129
129
|
serverOnly: false,
|
|
130
130
|
noWeb: true,
|
|
131
|
-
|
|
131
|
+
server: { nodeBin: "node-cmd", entryPath: join(data, "server-entry.js"), appDir: data },
|
|
132
132
|
runner: { nodeBin: "node-cmd", entryPath: join(data, "entry.js"), appDir: data },
|
|
133
133
|
};
|
|
134
134
|
|