@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 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
- process.env.FLAIR_KEY_DIR ? resolve(process.env.FLAIR_KEY_DIR, `${agentId}.key`) : null,
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
- this.url = (config.url ?? process.env.FLAIR_URL ?? DEFAULT_URL).replace(/\/$/, "");
32
- this.agentId = config.agentId || process.env.FLAIR_AGENT_ID || "";
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 || process.env.FLAIR_CLIENT || undefined;
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 ?? process.env.FLAIR_ADMIN_USER;
41
- const adminPass = config.adminPassword ?? process.env.FLAIR_ADMIN_PASSWORD;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tpsdev-ai/flair-client",
3
- "version": "0.44.12",
3
+ "version": "0.45.0",
4
4
  "description": "Lightweight client for Flair — identity, memory, and soul for AI agents. Zero heavy dependencies.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",