@open-mercato/ai-assistant 0.6.5-develop.5116.1.f0af9e5080 → 0.6.5-develop.5155.1.148d10a46d

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,13 +1,22 @@
1
- import { createHash } from "node:crypto";
1
+ import { pbkdf2Sync, randomBytes } from "node:crypto";
2
2
  function redactSecretForLog(value) {
3
3
  if (typeof value !== "string" || value.length === 0) return "<redacted>";
4
4
  const prefixLength = Math.min(12, Math.floor(value.length / 2));
5
5
  if (prefixLength <= 0) return "<redacted>";
6
6
  return `${value.slice(0, prefixLength)}...`;
7
7
  }
8
+ const sessionIdHmacKey = randomBytes(32);
9
+ const SESSION_ID_PBKDF2_ITERATIONS = 21e4;
10
+ const SESSION_ID_PBKDF2_KEYLEN = 16;
8
11
  function deriveApiKeySessionId(apiKeySecret) {
9
- const digest = createHash("sha256").update(apiKeySecret).digest("hex");
10
- return `apikey_${digest.slice(0, 16)}`;
12
+ const digest = pbkdf2Sync(
13
+ apiKeySecret,
14
+ sessionIdHmacKey,
15
+ SESSION_ID_PBKDF2_ITERATIONS,
16
+ SESSION_ID_PBKDF2_KEYLEN,
17
+ "sha256"
18
+ ).toString("hex");
19
+ return `apikey_${digest}`;
11
20
  }
12
21
  export {
13
22
  deriveApiKeySessionId,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/ai_assistant/lib/log-redaction.ts"],
4
- "sourcesContent": ["import { createHash } from 'node:crypto'\n\n/**\n * Redact a bearer-style secret (session token, API key) for safe logging.\n * Reveals at most a short leading fingerprint and never more than half of the\n * value, so durable logs never carry a replayable credential.\n */\nexport function redactSecretForLog(value: unknown): string {\n if (typeof value !== 'string' || value.length === 0) return '<redacted>'\n const prefixLength = Math.min(12, Math.floor(value.length / 2))\n if (prefixLength <= 0) return '<redacted>'\n return `${value.slice(0, prefixLength)}...`\n}\n\n/**\n * Derive a stable, non-reversible session-memory id from an API key secret.\n * The same secret always maps to the same id (so tool calls on one MCP\n * connection share a memory cache), but no secret material is exposed because\n * the id is a truncated SHA-256 digest rather than a slice of the secret.\n */\nexport function deriveApiKeySessionId(apiKeySecret: string): string {\n const digest = createHash('sha256').update(apiKeySecret).digest('hex')\n return `apikey_${digest.slice(0, 16)}`\n}\n"],
5
- "mappings": "AAAA,SAAS,kBAAkB;AAOpB,SAAS,mBAAmB,OAAwB;AACzD,MAAI,OAAO,UAAU,YAAY,MAAM,WAAW,EAAG,QAAO;AAC5D,QAAM,eAAe,KAAK,IAAI,IAAI,KAAK,MAAM,MAAM,SAAS,CAAC,CAAC;AAC9D,MAAI,gBAAgB,EAAG,QAAO;AAC9B,SAAO,GAAG,MAAM,MAAM,GAAG,YAAY,CAAC;AACxC;AAQO,SAAS,sBAAsB,cAA8B;AAClE,QAAM,SAAS,WAAW,QAAQ,EAAE,OAAO,YAAY,EAAE,OAAO,KAAK;AACrE,SAAO,UAAU,OAAO,MAAM,GAAG,EAAE,CAAC;AACtC;",
4
+ "sourcesContent": ["import { pbkdf2Sync, randomBytes } from 'node:crypto'\n\n/**\n * Redact a bearer-style secret (session token, API key) for safe logging.\n * Reveals at most a short leading fingerprint and never more than half of the\n * value, so durable logs never carry a replayable credential.\n */\nexport function redactSecretForLog(value: unknown): string {\n if (typeof value !== 'string' || value.length === 0) return '<redacted>'\n const prefixLength = Math.min(12, Math.floor(value.length / 2))\n if (prefixLength <= 0) return '<redacted>'\n return `${value.slice(0, prefixLength)}...`\n}\n\n// Per-process random key so the API key secret is fingerprinted through a keyed\n// HMAC rather than a fast, unkeyed digest. The session id is only an in-process\n// grouping key for the session-memory cache (a process-local Map), so a key that\n// lives for the process lifetime keeps the same-secret-maps-to-same-id guarantee\n// within an MCP connection while ensuring the digest is not derivable from the\n// secret alone. Mirrors the secret fingerprinter in apiKeyAuthCache.ts.\nconst sessionIdHmacKey = randomBytes(32)\nconst SESSION_ID_PBKDF2_ITERATIONS = 210000\nconst SESSION_ID_PBKDF2_KEYLEN = 16\n\n/**\n * Derive a stable, non-reversible session-memory id from an API key secret.\n * The same secret always maps to the same id within a process (so tool calls on\n * one MCP connection share a memory cache), but no secret material is exposed:\n * the id is derived with PBKDF2 (slow KDF) using a per-process random salt.\n */\nexport function deriveApiKeySessionId(apiKeySecret: string): string {\n const digest = pbkdf2Sync(\n apiKeySecret,\n sessionIdHmacKey,\n SESSION_ID_PBKDF2_ITERATIONS,\n SESSION_ID_PBKDF2_KEYLEN,\n 'sha256'\n ).toString('hex')\n return `apikey_${digest}`\n}\n"],
5
+ "mappings": "AAAA,SAAS,YAAY,mBAAmB;AAOjC,SAAS,mBAAmB,OAAwB;AACzD,MAAI,OAAO,UAAU,YAAY,MAAM,WAAW,EAAG,QAAO;AAC5D,QAAM,eAAe,KAAK,IAAI,IAAI,KAAK,MAAM,MAAM,SAAS,CAAC,CAAC;AAC9D,MAAI,gBAAgB,EAAG,QAAO;AAC9B,SAAO,GAAG,MAAM,MAAM,GAAG,YAAY,CAAC;AACxC;AAQA,MAAM,mBAAmB,YAAY,EAAE;AACvC,MAAM,+BAA+B;AACrC,MAAM,2BAA2B;AAQ1B,SAAS,sBAAsB,cAA8B;AAClE,QAAM,SAAS;AAAA,IACb;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,EAAE,SAAS,KAAK;AAChB,SAAO,UAAU,MAAM;AACzB;",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/ai-assistant",
3
- "version": "0.6.5-develop.5116.1.f0af9e5080",
3
+ "version": "0.6.5-develop.5155.1.148d10a46d",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": ">=22.0.0"
@@ -87,9 +87,9 @@
87
87
  "dependencies": {
88
88
  "@ai-sdk/anthropic": "^3.0.81",
89
89
  "@ai-sdk/google": "^3.0.80",
90
- "@ai-sdk/openai": "^3.0.67",
90
+ "@ai-sdk/openai": "^3.0.68",
91
91
  "@modelcontextprotocol/sdk": "^1.29.0",
92
- "ai": "^6.0.194",
92
+ "ai": "^6.0.197",
93
93
  "cmdk": "^1.0.0",
94
94
  "framer-motion": "^12.40.0",
95
95
  "isolated-vm": "^7.0.0",
@@ -98,17 +98,17 @@
98
98
  "zod-to-json-schema": "^3.25.2"
99
99
  },
100
100
  "peerDependencies": {
101
- "@open-mercato/shared": "0.6.5-develop.5116.1.f0af9e5080",
102
- "@open-mercato/ui": "0.6.5-develop.5116.1.f0af9e5080",
101
+ "@open-mercato/shared": "0.6.5-develop.5155.1.148d10a46d",
102
+ "@open-mercato/ui": "0.6.5-develop.5155.1.148d10a46d",
103
103
  "react": "^19.0.0",
104
104
  "react-dom": "^19.0.0",
105
105
  "zod": ">=3.23.0"
106
106
  },
107
107
  "devDependencies": {
108
- "@open-mercato/cli": "0.6.5-develop.5116.1.f0af9e5080",
109
- "@open-mercato/shared": "0.6.5-develop.5116.1.f0af9e5080",
110
- "@open-mercato/ui": "0.6.5-develop.5116.1.f0af9e5080",
111
- "@types/react": "^19.2.16",
108
+ "@open-mercato/cli": "0.6.5-develop.5155.1.148d10a46d",
109
+ "@open-mercato/shared": "0.6.5-develop.5155.1.148d10a46d",
110
+ "@open-mercato/ui": "0.6.5-develop.5155.1.148d10a46d",
111
+ "@types/react": "^19.2.17",
112
112
  "@types/react-dom": "^19.2.3",
113
113
  "react": "19.2.7",
114
114
  "react-dom": "19.2.7",
@@ -1,6 +1,8 @@
1
1
  import { createHash } from 'node:crypto'
2
2
  import { redactSecretForLog, deriveApiKeySessionId } from '../log-redaction'
3
3
 
4
+ const SESSION_ID_DIGEST_HEX = 16
5
+
4
6
  describe('redactSecretForLog', () => {
5
7
  it('never emits the full session token', () => {
6
8
  const token = `sess_${'a'.repeat(32)}`
@@ -43,11 +45,18 @@ describe('deriveApiKeySessionId', () => {
43
45
  expect(secret).not.toContain(digestPart)
44
46
  })
45
47
 
46
- it('is a stable truncated sha-256 digest of the secret', () => {
48
+ it('is stable within a process and shaped as a truncated hex digest', () => {
49
+ const secret = 'omk_test_secret_value'
50
+ const sessionId = deriveApiKeySessionId(secret)
51
+ expect(sessionId).toBe(deriveApiKeySessionId(secret))
52
+ const digestPart = sessionId.slice('apikey_'.length)
53
+ expect(digestPart).toMatch(new RegExp(`^[0-9a-f]{${SESSION_ID_DIGEST_HEX}}$`))
54
+ })
55
+
56
+ it('is keyed, not a recomputable unkeyed sha-256 of the secret', () => {
47
57
  const secret = 'omk_test_secret_value'
48
- const expected = `apikey_${createHash('sha256').update(secret).digest('hex').slice(0, 16)}`
49
- expect(deriveApiKeySessionId(secret)).toBe(expected)
50
- expect(deriveApiKeySessionId(secret)).toBe(deriveApiKeySessionId(secret))
58
+ const unkeyed = `apikey_${createHash('sha256').update(secret).digest('hex').slice(0, SESSION_ID_DIGEST_HEX)}`
59
+ expect(deriveApiKeySessionId(secret)).not.toBe(unkeyed)
51
60
  })
52
61
 
53
62
  it('maps distinct secrets to distinct ids', () => {
@@ -1,4 +1,4 @@
1
- import { createHash } from 'node:crypto'
1
+ import { pbkdf2Sync, randomBytes } from 'node:crypto'
2
2
 
3
3
  /**
4
4
  * Redact a bearer-style secret (session token, API key) for safe logging.
@@ -12,13 +12,29 @@ export function redactSecretForLog(value: unknown): string {
12
12
  return `${value.slice(0, prefixLength)}...`
13
13
  }
14
14
 
15
+ // Per-process random key so the API key secret is fingerprinted through a keyed
16
+ // HMAC rather than a fast, unkeyed digest. The session id is only an in-process
17
+ // grouping key for the session-memory cache (a process-local Map), so a key that
18
+ // lives for the process lifetime keeps the same-secret-maps-to-same-id guarantee
19
+ // within an MCP connection while ensuring the digest is not derivable from the
20
+ // secret alone. Mirrors the secret fingerprinter in apiKeyAuthCache.ts.
21
+ const sessionIdHmacKey = randomBytes(32)
22
+ const SESSION_ID_PBKDF2_ITERATIONS = 210000
23
+ const SESSION_ID_PBKDF2_KEYLEN = 16
24
+
15
25
  /**
16
26
  * Derive a stable, non-reversible session-memory id from an API key secret.
17
- * The same secret always maps to the same id (so tool calls on one MCP
18
- * connection share a memory cache), but no secret material is exposed because
19
- * the id is a truncated SHA-256 digest rather than a slice of the secret.
27
+ * The same secret always maps to the same id within a process (so tool calls on
28
+ * one MCP connection share a memory cache), but no secret material is exposed:
29
+ * the id is derived with PBKDF2 (slow KDF) using a per-process random salt.
20
30
  */
21
31
  export function deriveApiKeySessionId(apiKeySecret: string): string {
22
- const digest = createHash('sha256').update(apiKeySecret).digest('hex')
23
- return `apikey_${digest.slice(0, 16)}`
32
+ const digest = pbkdf2Sync(
33
+ apiKeySecret,
34
+ sessionIdHmacKey,
35
+ SESSION_ID_PBKDF2_ITERATIONS,
36
+ SESSION_ID_PBKDF2_KEYLEN,
37
+ 'sha256'
38
+ ).toString('hex')
39
+ return `apikey_${digest}`
24
40
  }