@tpsdev-ai/flair-client 0.44.12 → 0.45.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auth.js +6 -1
- package/dist/client.js +11 -5
- package/dist/env-guard.d.ts +41 -0
- package/dist/env-guard.js +48 -0
- package/package.json +1 -1
package/dist/auth.js
CHANGED
|
@@ -8,6 +8,7 @@ import { randomUUID, sign as ed25519Sign, createPrivateKey } from "node:crypto";
|
|
|
8
8
|
import { readFileSync, existsSync } from "node:fs";
|
|
9
9
|
import { resolve } from "node:path";
|
|
10
10
|
import { homedir } from "node:os";
|
|
11
|
+
import { readEnvOrUnset } from "./env-guard.js";
|
|
11
12
|
const PKCS8_ED25519_PREFIX = Buffer.from("302e020100300506032b657004220420", "hex");
|
|
12
13
|
/** Resolve an Ed25519 private key from a file (base64 PKCS8 DER or raw 32-byte seed). */
|
|
13
14
|
export function loadPrivateKey(path) {
|
|
@@ -25,8 +26,12 @@ export function resolveKeyPath(agentId, keyPath) {
|
|
|
25
26
|
const resolved = resolve(keyPath.replace(/^~/, homedir()));
|
|
26
27
|
return existsSync(resolved) ? resolved : null;
|
|
27
28
|
}
|
|
29
|
+
// flair#1254: an unsubstituted `${FLAIR_KEY_DIR}` literal reads as unset, so
|
|
30
|
+
// key resolution falls through to the standard locations below instead of
|
|
31
|
+
// probing a directory literally named "${FLAIR_KEY_DIR}".
|
|
32
|
+
const keyDir = readEnvOrUnset("FLAIR_KEY_DIR");
|
|
28
33
|
const candidates = [
|
|
29
|
-
|
|
34
|
+
keyDir ? resolve(keyDir, `${agentId}.key`) : null,
|
|
30
35
|
resolve(homedir(), ".flair", "keys", `${agentId}.key`),
|
|
31
36
|
resolve(homedir(), ".tps", "secrets", "flair", `${agentId}-priv.key`),
|
|
32
37
|
].filter(Boolean);
|
package/dist/client.js
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { createHash, createPrivateKey } from "node:crypto";
|
|
11
11
|
import { loadPrivateKey, resolveKeyPath, signRequest } from "./auth.js";
|
|
12
|
+
import { readEnvOrUnset } from "./env-guard.js";
|
|
12
13
|
const DEFAULT_URL = "http://localhost:19926";
|
|
13
14
|
const DEFAULT_TIMEOUT = 30_000;
|
|
14
15
|
export class FlairClient {
|
|
@@ -28,17 +29,22 @@ export class FlairClient {
|
|
|
28
29
|
timeoutMs;
|
|
29
30
|
basicAuth = null;
|
|
30
31
|
constructor(config) {
|
|
31
|
-
|
|
32
|
-
|
|
32
|
+
// Every env fallback below goes through readEnvOrUnset (flair#1254): a
|
|
33
|
+
// wholesale unsubstituted `${...}` interpolation literal — the shape an
|
|
34
|
+
// MCP-host config template leaves behind when it fails to substitute —
|
|
35
|
+
// reads as UNSET, so the existing defaults apply instead of the literal
|
|
36
|
+
// winning the `??`/`||` chain and poisoning the connection.
|
|
37
|
+
this.url = (config.url ?? readEnvOrUnset("FLAIR_URL") ?? DEFAULT_URL).replace(/\/$/, "");
|
|
38
|
+
this.agentId = config.agentId || readEnvOrUnset("FLAIR_AGENT_ID") || "";
|
|
33
39
|
this.keyPath = config.keyPath;
|
|
34
40
|
if (config.privateKey !== undefined) {
|
|
35
41
|
this.rawPrivateKey = config.privateKey;
|
|
36
42
|
}
|
|
37
|
-
this.claimedClient = config.claimedClient ||
|
|
43
|
+
this.claimedClient = config.claimedClient || readEnvOrUnset("FLAIR_CLIENT") || undefined;
|
|
38
44
|
this.timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT;
|
|
39
45
|
// Basic auth fallback for standalone deployments without Ed25519 keys
|
|
40
|
-
const adminUser = config.adminUser ??
|
|
41
|
-
const adminPass = config.adminPassword ??
|
|
46
|
+
const adminUser = config.adminUser ?? readEnvOrUnset("FLAIR_ADMIN_USER");
|
|
47
|
+
const adminPass = config.adminPassword ?? readEnvOrUnset("FLAIR_ADMIN_PASSWORD");
|
|
42
48
|
if (adminUser && adminPass) {
|
|
43
49
|
this.basicAuth = `Basic ${Buffer.from(`${adminUser}:${adminPass}`).toString("base64")}`;
|
|
44
50
|
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interpolation-literal env guard (flair#1254, generalizing flair#1250/#1253).
|
|
3
|
+
*
|
|
4
|
+
* An MCP/agent host config template often forwards env like
|
|
5
|
+
* "env": { "FLAIR_URL": "${FLAIR_URL}" }
|
|
6
|
+
* expecting the host to substitute `${FLAIR_URL}` with the real value. When
|
|
7
|
+
* the variable is unset and the host does NOT substitute, the consumer process
|
|
8
|
+
* is handed the LITERAL string `"${FLAIR_URL}"`. That literal is truthy, so it
|
|
9
|
+
* wins every `value ?? default` / `value || default` chain and the sensible
|
|
10
|
+
* default never applies — the connection then fails pointing nowhere near the
|
|
11
|
+
* real cause.
|
|
12
|
+
*
|
|
13
|
+
* flair#1253 stripped such literals at flair-mcp's process boundary, but the
|
|
14
|
+
* root exposure is HERE: FlairClient's constructor independently re-reads
|
|
15
|
+
* `process.env.FLAIR_*` as its own fallback, so every consumer of this client
|
|
16
|
+
* (CLI, adk, langgraph, n8n, ...) had the identical exposure and would each
|
|
17
|
+
* have needed its own boundary strip. This guard makes the client's own env
|
|
18
|
+
* fallbacks treat a wholesale `${...}` literal as absent, so the existing
|
|
19
|
+
* defaults (DEFAULT_URL, key-path derivation, no-basic-auth) apply instead —
|
|
20
|
+
* no new hardcoded defaults are introduced. flair-mcp's boundary strip stays
|
|
21
|
+
* as defense-in-depth.
|
|
22
|
+
*
|
|
23
|
+
* Semantics intentionally identical to packages/flair-mcp/src/env-guard.ts.
|
|
24
|
+
* Duplicated (not imported) because the dependency points the other way:
|
|
25
|
+
* flair-mcp depends on flair-client, and flair-client is zero-dep.
|
|
26
|
+
*/
|
|
27
|
+
/**
|
|
28
|
+
* True iff `value` is an unsubstituted interpolation literal like `${FLAIR_URL}`
|
|
29
|
+
* — i.e. the whole (trimmed) value is a single `${...}` placeholder the host
|
|
30
|
+
* never expanded. A value that merely CONTAINS a `${...}` (e.g. a real URL with
|
|
31
|
+
* a fragment) is left alone: only a wholesale placeholder is treated as unset.
|
|
32
|
+
*/
|
|
33
|
+
export declare function isUnsubstitutedInterpolation(value: string): boolean;
|
|
34
|
+
/**
|
|
35
|
+
* Read an env var, treating an unsubstituted `${...}` interpolation literal as
|
|
36
|
+
* unset: returns `undefined` for such a literal (and for a genuinely absent
|
|
37
|
+
* var) so a downstream default applies instead of the literal being used
|
|
38
|
+
* verbatim. A real value is returned unchanged (NOT trimmed — only the
|
|
39
|
+
* placeholder check trims).
|
|
40
|
+
*/
|
|
41
|
+
export declare function readEnvOrUnset(name: string, env?: NodeJS.ProcessEnv): string | undefined;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interpolation-literal env guard (flair#1254, generalizing flair#1250/#1253).
|
|
3
|
+
*
|
|
4
|
+
* An MCP/agent host config template often forwards env like
|
|
5
|
+
* "env": { "FLAIR_URL": "${FLAIR_URL}" }
|
|
6
|
+
* expecting the host to substitute `${FLAIR_URL}` with the real value. When
|
|
7
|
+
* the variable is unset and the host does NOT substitute, the consumer process
|
|
8
|
+
* is handed the LITERAL string `"${FLAIR_URL}"`. That literal is truthy, so it
|
|
9
|
+
* wins every `value ?? default` / `value || default` chain and the sensible
|
|
10
|
+
* default never applies — the connection then fails pointing nowhere near the
|
|
11
|
+
* real cause.
|
|
12
|
+
*
|
|
13
|
+
* flair#1253 stripped such literals at flair-mcp's process boundary, but the
|
|
14
|
+
* root exposure is HERE: FlairClient's constructor independently re-reads
|
|
15
|
+
* `process.env.FLAIR_*` as its own fallback, so every consumer of this client
|
|
16
|
+
* (CLI, adk, langgraph, n8n, ...) had the identical exposure and would each
|
|
17
|
+
* have needed its own boundary strip. This guard makes the client's own env
|
|
18
|
+
* fallbacks treat a wholesale `${...}` literal as absent, so the existing
|
|
19
|
+
* defaults (DEFAULT_URL, key-path derivation, no-basic-auth) apply instead —
|
|
20
|
+
* no new hardcoded defaults are introduced. flair-mcp's boundary strip stays
|
|
21
|
+
* as defense-in-depth.
|
|
22
|
+
*
|
|
23
|
+
* Semantics intentionally identical to packages/flair-mcp/src/env-guard.ts.
|
|
24
|
+
* Duplicated (not imported) because the dependency points the other way:
|
|
25
|
+
* flair-mcp depends on flair-client, and flair-client is zero-dep.
|
|
26
|
+
*/
|
|
27
|
+
/**
|
|
28
|
+
* True iff `value` is an unsubstituted interpolation literal like `${FLAIR_URL}`
|
|
29
|
+
* — i.e. the whole (trimmed) value is a single `${...}` placeholder the host
|
|
30
|
+
* never expanded. A value that merely CONTAINS a `${...}` (e.g. a real URL with
|
|
31
|
+
* a fragment) is left alone: only a wholesale placeholder is treated as unset.
|
|
32
|
+
*/
|
|
33
|
+
export function isUnsubstitutedInterpolation(value) {
|
|
34
|
+
return /^\$\{.*\}$/.test(value.trim());
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Read an env var, treating an unsubstituted `${...}` interpolation literal as
|
|
38
|
+
* unset: returns `undefined` for such a literal (and for a genuinely absent
|
|
39
|
+
* var) so a downstream default applies instead of the literal being used
|
|
40
|
+
* verbatim. A real value is returned unchanged (NOT trimmed — only the
|
|
41
|
+
* placeholder check trims).
|
|
42
|
+
*/
|
|
43
|
+
export function readEnvOrUnset(name, env = process.env) {
|
|
44
|
+
const v = env[name];
|
|
45
|
+
if (v === undefined)
|
|
46
|
+
return undefined;
|
|
47
|
+
return isUnsubstitutedInterpolation(v) ? undefined : v;
|
|
48
|
+
}
|
package/package.json
CHANGED