@memorax/memorax-code 0.1.5 → 0.1.6

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.
Files changed (85) hide show
  1. package/README.md +7 -18
  2. package/bin/memorax-code-npm-preinstall.mjs +28 -39
  3. package/bin/memorax-code-plugin-postinstall.mjs +23 -1537
  4. package/bin/memorax-code-setup.mjs +1615 -0
  5. package/bin/memorax-code.mjs +289 -5
  6. package/docs/configuration.md +66 -17
  7. package/docs/troubleshooting.md +105 -7
  8. package/lib/memorax-code-adapter-common/src/config-utils.d.mts +5 -1
  9. package/lib/memorax-code-adapter-common/src/config-utils.mjs +36 -3
  10. package/lib/memorax-code-adapter-common/src/credentials/linux-secret-service.mjs +110 -0
  11. package/lib/memorax-code-adapter-common/src/credentials/macos-keychain.mjs +224 -0
  12. package/lib/memorax-code-adapter-common/src/credentials/secure-command.mjs +340 -0
  13. package/lib/memorax-code-adapter-common/src/credentials/trial-credential-record.d.mts +82 -0
  14. package/lib/memorax-code-adapter-common/src/credentials/trial-credential-record.mjs +183 -0
  15. package/lib/memorax-code-adapter-common/src/credentials/trial-credential-store.d.mts +82 -0
  16. package/lib/memorax-code-adapter-common/src/credentials/trial-credential-store.mjs +271 -0
  17. package/lib/memorax-code-adapter-common/src/credentials/windows-dpapi.mjs +440 -0
  18. package/lib/memorax-code-adapter-common/src/hooks/memory-skill-reminder-hook.mjs +7 -4
  19. package/lib/memorax-code-adapter-common/src/setup-completion.d.mts +81 -0
  20. package/lib/memorax-code-adapter-common/src/setup-completion.mjs +154 -0
  21. package/lib/memorax-code-backend/dist/clients/claude/memory-hook-runtime.js +21 -2
  22. package/lib/memorax-code-backend/dist/clients/codex/memory-hook-runtime.js +23 -3
  23. package/lib/memorax-code-backend/dist/clients/opencode/memory-hook-runtime.js +19 -1
  24. package/lib/memorax-code-backend/dist/entrypoints/backend-cli.js +17 -16
  25. package/lib/memorax-code-backend/dist/lifecycle/orchestrator.js +81 -22
  26. package/lib/memorax-code-backend/dist/memorax-cli.js +3 -0
  27. package/lib/memorax-code-backend/dist/memory/automatic-retrieval.js +8 -2
  28. package/lib/memorax-code-backend/dist/memory/automatic-writeback.js +13 -1
  29. package/lib/memorax-code-backend/dist/memory/cli.js +10 -0
  30. package/lib/memorax-code-backend/dist/memory/quota-notice.js +208 -0
  31. package/lib/memorax-code-backend/dist/memory/service.js +11 -0
  32. package/lib/memorax-code-backend/dist/provider/memorax/adapter.js +4 -2
  33. package/lib/memorax-code-backend/dist/provider/memorax/http.js +10 -3
  34. package/lib/memorax-code-backend/dist/provider/memorax/quota.js +26 -0
  35. package/lib/memorax-code-backend/package.json +1 -1
  36. package/lib/memorax-code-claude-adapter/.claude-plugin/plugin.json +1 -1
  37. package/lib/memorax-code-claude-adapter/hooks/runtime-shell.json +1 -1
  38. package/lib/memorax-code-claude-adapter/package.json +1 -1
  39. package/lib/memorax-code-claude-adapter/runtime-hooks/memory-turn.mjs +5 -3
  40. package/lib/memorax-code-claude-adapter/skills/memorax-code/references/memorax-add.md +2 -0
  41. package/lib/memorax-code-claude-adapter/skills/memorax-code/references/memorax-search.md +4 -2
  42. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/.claude-plugin/plugin.json +1 -1
  43. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/hooks/runtime-shell.json +1 -1
  44. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/memorax-code-adapter-common/src/config-utils.d.mts +5 -1
  45. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/memorax-code-adapter-common/src/config-utils.mjs +36 -3
  46. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/memorax-code-adapter-common/src/credentials/linux-secret-service.mjs +110 -0
  47. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/memorax-code-adapter-common/src/credentials/macos-keychain.mjs +224 -0
  48. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/memorax-code-adapter-common/src/credentials/secure-command.mjs +340 -0
  49. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/memorax-code-adapter-common/src/credentials/trial-credential-record.d.mts +82 -0
  50. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/memorax-code-adapter-common/src/credentials/trial-credential-record.mjs +183 -0
  51. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/memorax-code-adapter-common/src/credentials/trial-credential-store.d.mts +82 -0
  52. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/memorax-code-adapter-common/src/credentials/trial-credential-store.mjs +271 -0
  53. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/memorax-code-adapter-common/src/credentials/windows-dpapi.mjs +440 -0
  54. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/memorax-code-adapter-common/src/hooks/memory-skill-reminder-hook.mjs +7 -4
  55. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/memorax-code-adapter-common/src/setup-completion.d.mts +81 -0
  56. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/memorax-code-adapter-common/src/setup-completion.mjs +154 -0
  57. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/package.json +1 -1
  58. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/runtime-hooks/memory-turn.mjs +5 -3
  59. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/skills/memorax-code/references/memorax-add.md +2 -0
  60. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/skills/memorax-code/references/memorax-search.md +4 -2
  61. package/lib/memorax-code-codex-adapter/.codex-plugin/plugin.json +1 -1
  62. package/lib/memorax-code-codex-adapter/hooks/runtime-shell.json +1 -1
  63. package/lib/memorax-code-codex-adapter/package.json +1 -1
  64. package/lib/memorax-code-codex-adapter/runtime-hooks/memory-skill-reminder.mjs +2 -0
  65. package/lib/memorax-code-codex-adapter/skills/memorax-code/references/memorax-add.md +2 -0
  66. package/lib/memorax-code-codex-adapter/skills/memorax-code/references/memorax-search.md +4 -2
  67. package/lib/memorax-code-dsh-adapter/hooks/repo-memory-job.mjs +5 -2
  68. package/lib/memorax-code-dsh-adapter/package.json +1 -1
  69. package/lib/memorax-code-dsh-adapter/skills/memorax-code/references/memorax-add.md +2 -0
  70. package/lib/memorax-code-dsh-adapter/skills/memorax-code/references/memorax-search.md +4 -2
  71. package/lib/memorax-code-dsh-adapter/src/profile-lifecycle.mjs +158 -7
  72. package/lib/memorax-code-dsh-adapter/src/runtime-state.mjs +21 -0
  73. package/lib/memorax-code-opencode-adapter/package.json +1 -1
  74. package/lib/memorax-code-opencode-adapter/skills/memorax-code/references/memorax-add.md +2 -0
  75. package/lib/memorax-code-opencode-adapter/skills/memorax-code/references/memorax-search.md +4 -2
  76. package/lib/memorax-code-opencode-adapter/src/plugin.mjs +20 -0
  77. package/lib/package-transition.mjs +311 -0
  78. package/lib/resolve-codex-command.mjs +32 -6
  79. package/lib/setup-memory-preferences.mjs +113 -0
  80. package/lib/setup-reconcile.mjs +128 -0
  81. package/lib/trial-plugin-mark.mjs +171 -0
  82. package/lib/trial-provision-client.mjs +307 -0
  83. package/lib/trial-provision-flow.mjs +215 -0
  84. package/lib/trial-setup.mjs +53 -0
  85. package/package.json +1 -1
@@ -1,4 +1,5 @@
1
1
  import { isRecord } from "../../shared/record.js";
2
+ import { memoraxQuotaFromResponse, } from "./quota.js";
2
3
  export async function postMemoraxJson(config, path, payload, fetchImpl) {
3
4
  const controller = new AbortController();
4
5
  const timeout = setTimeout(() => controller.abort(), config.timeoutMs);
@@ -20,9 +21,15 @@ export async function postMemoraxJson(config, path, payload, fetchImpl) {
20
21
  retryAfterMs,
21
22
  });
22
23
  }
23
- const raw = await response.json().catch(() => null);
24
- validateMemoraxEnvelope(raw);
25
- return raw;
24
+ const body = await response.json().catch(() => null);
25
+ validateMemoraxEnvelope(body);
26
+ const featureCode = path === "/v1/memories/add"
27
+ ? "memory_write"
28
+ : path === "/v1/memories/search"
29
+ ? "memory_search"
30
+ : undefined;
31
+ const quota = featureCode ? memoraxQuotaFromResponse(body, featureCode) : undefined;
32
+ return { body, ...(quota ? { quota } : {}) };
26
33
  }
27
34
  catch (error) {
28
35
  throw normalizeMemoraxRequestError(error, controller.signal.aborted);
@@ -0,0 +1,26 @@
1
+ export function memoraxQuotaFromResponse(body, featureCode) {
2
+ if (!isRecord(body) || !isRecord(body.data) || !Array.isArray(body.data.balances)) {
3
+ return undefined;
4
+ }
5
+ for (const balance of body.data.balances) {
6
+ if (!isRecord(balance)
7
+ || balance.product_code !== "memory_api"
8
+ || balance.feature_code !== featureCode
9
+ || balance.spec_key !== "calls"
10
+ || balance.quota_unit !== "times") {
11
+ continue;
12
+ }
13
+ const remaining = nonNegativeSafeInteger(balance.remaining);
14
+ const limit = nonNegativeSafeInteger(balance.quota_limit);
15
+ if (remaining !== undefined && limit !== undefined && remaining <= limit) {
16
+ return { featureCode, remaining, limit };
17
+ }
18
+ }
19
+ return undefined;
20
+ }
21
+ function nonNegativeSafeInteger(value) {
22
+ return Number.isSafeInteger(value) && Number(value) >= 0 ? Number(value) : undefined;
23
+ }
24
+ function isRecord(value) {
25
+ return typeof value === "object" && value !== null && !Array.isArray(value);
26
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@memorax-code/backend",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "description": "Local memory integration, lifecycle, trace, and diagnostics for MemoraX Code.",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memorax-code-claude-adapter",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "description": "Memory skill and local hooks for MemoraX Code in Claude Code.",
5
5
  "author": {
6
6
  "name": "MemoraX Code Maintainers"
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "version": 1,
3
- "shellVersion": "0.1.5",
3
+ "shellVersion": "0.1.6",
4
4
  "runtimeAbi": 1
5
5
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@memorax-code/claude-adapter",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "description": "Claude Code Hook adapter for MemoraX Code memory services.",
@@ -35,12 +35,14 @@ try {
35
35
  pluginRoot: process.env.CLAUDE_PLUGIN_ROOT,
36
36
  });
37
37
  const additionalContext = stringValue(response?.additionalContext);
38
- if (additionalContext) {
38
+ const systemMessage = stringValue(response?.userNotice);
39
+ if (additionalContext || systemMessage) {
39
40
  process.stdout.write(`${JSON.stringify({
40
- hookSpecificOutput: {
41
+ ...(systemMessage ? { systemMessage } : {}),
42
+ ...(additionalContext ? { hookSpecificOutput: {
41
43
  hookEventName: "UserPromptSubmit",
42
44
  additionalContext,
43
- },
45
+ } } : {}),
44
46
  })}\n`);
45
47
  }
46
48
  } else if (event === "Stop") {
@@ -83,4 +83,6 @@ Do not add speculation, assistant-only praise, or a current instruction merely b
83
83
 
84
84
  ## Output
85
85
 
86
+ If a successful Add returns `quotaNotice`, or prints it as a quota-reminder line in the default CLI output, present the complete reminder once and prominently before the normal result summary. Do not reduce it to only a percentage or omit its account URL or conditional local Mark ID retrieval instructions. Never run `memorax-code account --show-mark-id` for the user, ask for its output, or reproduce a Mark ID in chat. Treat the reminder as user-facing operational output, not coding memory content, and continue the current task.
87
+
86
88
  After an add request is accepted, confirm briefly that it was submitted for processing and identify the reusable coding lesson at a high level. If add is disabled or fails, report the issue and continue without bypassing the CLI.
@@ -6,11 +6,11 @@ Use these instructions only to search reusable coding memory through `memorax-cl
6
6
 
7
7
  Run the CLI from the active task workspace. The installed Hook and session binding supply the authoritative workspace root; do not run `git rev-parse`, infer the root from Git metadata, or substitute an unrelated working directory. Do not make `memorax-cli status` a mandatory preflight; use it only to diagnose a configuration or scope failure.
8
8
 
9
- Coding memory uses `<MemoraX base user ID>@<normalized repository name>` for Git workspaces and the normalized folder name for genuine non-Git directories. MemoraX Code resolves `.git`, `gitdir`, and `commondir` without executing Git. Linked worktrees share one repository scope; another clone, repository, or non-Git directory retains a different local session key even when its readable name matches.
9
+ Coding memory uses `<MemoraX base username>@<normalized repository name>` for Git workspaces and the normalized folder name for genuine non-Git directories. MemoraX Code resolves `.git`, `gitdir`, and `commondir` without executing Git. Linked worktrees share one repository scope; another clone, repository, or non-Git directory retains a different local session key even when its readable name matches.
10
10
 
11
11
  If a successful Search result reports `workspaceScopeFallbackReason: git_metadata_invalid`, malformed or incomplete metadata inside a direct `.git` directory was downgraded to the normalized local folder scope. Search has already run with the reported `effectiveUserId`. Present its `userNotice` once without pausing the current task or asking the user to repair Git first, then continue with the returned memory and live evidence. After the repository or `.git` metadata is repaired, later Search, Add, and automatic writeback in the same client session automatically use the restored Git repository scope.
12
12
 
13
- Require a readable active workspace binding. A CLI command from a linked worktree of the bound repository is valid. If `memorax-cli search` reports `workspace_scope_mismatch` or `workspace_scope_unavailable`, do not bypass the scope or fall back to an unscoped user id. Do not change the CLI working directory and retry. Tell the user that memory search was not executed and no request was sent to MemoraX, then present the CLI's `userAction` in natural language. Continue the current task using only live code and documentation.
13
+ Require a readable active workspace binding. A CLI command from a linked worktree of the bound repository is valid. If `memorax-cli search` reports `workspace_scope_mismatch` or `workspace_scope_unavailable`, do not bypass the scope or fall back to an unscoped username. Do not change the CLI working directory and retry. Tell the user that memory search was not executed and no request was sent to MemoraX, then present the CLI's `userAction` in natural language. Continue the current task using only live code and documentation.
14
14
 
15
15
  If `memorax-cli` is not on `PATH`, or memory is disabled, unconfigured, or unavailable, report that briefly and continue with live code or documentation. Authenticate through MemoraX Code configuration; never recover credentials from shell history or place tokens in prompts. Treat injected memory as a hypothesis and verify it against the current checkout.
16
16
 
@@ -88,4 +88,6 @@ Do not apply this retry to `memorax-cli add`, authentication or configuration fa
88
88
 
89
89
  ## Output
90
90
 
91
+ If a successful Search returns `quotaNotice`, or prints it as a quota-reminder line in the default CLI output, present the complete reminder once and prominently before the normal result summary. Do not reduce it to only a percentage or omit its account URL or conditional local Mark ID retrieval instructions. Never run `memorax-code account --show-mark-id` for the user, ask for its output, or reproduce a Mark ID in chat. Treat the reminder as user-facing operational output, not recalled memory, and continue the current task.
92
+
91
93
  Mention only an invariant, pitfall, convention, or validation idea that materially affects the answer. Ground claims about current implementation behavior in live code and checks.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memorax-code-claude-adapter",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "description": "Memory skill and local hooks for MemoraX Code in Claude Code.",
5
5
  "author": {
6
6
  "name": "MemoraX Code Maintainers"
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "version": 1,
3
- "shellVersion": "0.1.5",
3
+ "shellVersion": "0.1.6",
4
4
  "runtimeAbi": 1
5
5
  }
@@ -4,6 +4,10 @@ export type JsonFileLockOptions = Readonly<{
4
4
  retryMs?: number;
5
5
  }>;
6
6
 
7
+ export type AsyncJsonFileLockOptions = JsonFileLockOptions & Readonly<{
8
+ signal?: AbortSignal;
9
+ }>;
10
+
7
11
  export function readAdapterState(path: string): any;
8
12
  export function readJsonFile(path: string): any;
9
13
  export function readJsonValue(path: string): any;
@@ -23,5 +27,5 @@ export function withJsonFileLock<T>(
23
27
  export function withJsonFileLockAsync<T>(
24
28
  path: string,
25
29
  operation: () => T | Promise<T>,
26
- options?: JsonFileLockOptions,
30
+ options?: AsyncJsonFileLockOptions,
27
31
  ): Promise<T>;
@@ -127,14 +127,17 @@ export async function withJsonFileLockAsync(path, operation, options = {}) {
127
127
  const timeoutMs = positiveInteger(options.timeoutMs, JSON_FILE_LOCK_TIMEOUT_MS);
128
128
  const staleMs = positiveInteger(options.staleMs, JSON_FILE_LOCK_STALE_MS);
129
129
  const retryMs = positiveInteger(options.retryMs, JSON_FILE_LOCK_RETRY_MS);
130
+ const signal = options.signal;
130
131
  const lockPath = `${path}.lock`;
131
132
  const ownerId = `${process.pid}:${randomUUID()}`;
132
133
  const deadline = Date.now() + timeoutMs;
133
134
  const observedProcessStarts = new Map();
134
135
  const directory = dirname(path);
136
+ throwIfJsonFileLockAborted(signal, path, lockPath);
135
137
  ensurePrivateDirectory(directory, { durableBoundary: directory });
136
138
 
137
139
  while (!tryAcquireJsonFileLock(lockPath, ownerId)) {
140
+ throwIfJsonFileLockAborted(signal, path, lockPath);
138
141
  if (removeStaleJsonFileLock(lockPath, staleMs, observedProcessStarts)) continue;
139
142
  const remainingMs = deadline - Date.now();
140
143
  if (remainingMs <= 0) {
@@ -144,10 +147,11 @@ export async function withJsonFileLockAsync(path, operation, options = {}) {
144
147
  error.lockPath = lockPath;
145
148
  throw error;
146
149
  }
147
- await sleep(Math.min(retryMs, remainingMs));
150
+ await sleep(Math.min(retryMs, remainingMs), signal, path, lockPath);
148
151
  }
149
152
 
150
153
  try {
154
+ throwIfJsonFileLockAborted(signal, path, lockPath);
151
155
  return await operation();
152
156
  } finally {
153
157
  releaseJsonFileLock(lockPath, ownerId);
@@ -381,8 +385,37 @@ function sleepSync(milliseconds) {
381
385
  Atomics.wait(LOCK_SLEEP_BUFFER, 0, 0, Math.max(1, Math.trunc(milliseconds)));
382
386
  }
383
387
 
384
- function sleep(milliseconds) {
385
- return new Promise((resolve) => setTimeout(resolve, Math.max(1, Math.trunc(milliseconds))));
388
+ function sleep(milliseconds, signal, path, lockPath) {
389
+ const delayMs = Math.max(1, Math.trunc(milliseconds));
390
+ if (!signal) return new Promise((resolve) => setTimeout(resolve, delayMs));
391
+ throwIfJsonFileLockAborted(signal, path, lockPath);
392
+ return new Promise((resolve, reject) => {
393
+ let settled = false;
394
+ const finish = (error) => {
395
+ if (settled) return;
396
+ settled = true;
397
+ clearTimeout(timeout);
398
+ signal.removeEventListener?.("abort", onAbort);
399
+ if (error) reject(error);
400
+ else resolve();
401
+ };
402
+ const timeout = setTimeout(() => finish(), delayMs);
403
+ const onAbort = () => finish(jsonFileLockAbortError(path, lockPath));
404
+ signal.addEventListener?.("abort", onAbort, { once: true });
405
+ if (signal.aborted) onAbort();
406
+ });
407
+ }
408
+
409
+ function throwIfJsonFileLockAborted(signal, path, lockPath) {
410
+ if (signal?.aborted) throw jsonFileLockAbortError(path, lockPath);
411
+ }
412
+
413
+ function jsonFileLockAbortError(path, lockPath) {
414
+ const error = new Error(`aborted while waiting for JSON state lock: ${lockPath}`);
415
+ error.code = "JSON_FILE_LOCK_ABORTED";
416
+ error.path = path;
417
+ error.lockPath = lockPath;
418
+ return error;
386
419
  }
387
420
 
388
421
  function positiveInteger(value, fallback) {
@@ -0,0 +1,110 @@
1
+ import {
2
+ DEFAULT_SECURE_COMMAND_OUTPUT_BYTES,
3
+ DEFAULT_SECURE_COMMAND_TIMEOUT_MS,
4
+ decodeSecureCredential,
5
+ encodeSecureCredential,
6
+ executeSecureCommand,
7
+ minimalEnvironment,
8
+ runSecureCommand,
9
+ secureCredentialBackendError,
10
+ validateSecureCredentialNamespace,
11
+ wipeSecureCommandResult,
12
+ } from "./secure-command.mjs";
13
+
14
+ const BACKEND = "linux-secret-service";
15
+ const SECRET_TOOL = "/usr/bin/secret-tool";
16
+ const LINUX_ENVIRONMENT_NAMES = Object.freeze([
17
+ "DBUS_SESSION_BUS_ADDRESS",
18
+ "XDG_RUNTIME_DIR",
19
+ "DISPLAY",
20
+ "WAYLAND_DISPLAY",
21
+ "HOME",
22
+ "LANG",
23
+ "LC_ALL",
24
+ ]);
25
+
26
+ export function createLinuxSecretServiceBackend(options) {
27
+ const namespace = validateSecureCredentialNamespace(options?.namespace, BACKEND);
28
+ const runner = options?.runner ?? runSecureCommand;
29
+ const environment = minimalEnvironment(options?.environment ?? process.env, LINUX_ENVIRONMENT_NAMES);
30
+ const timeoutMs = options?.timeoutMs ?? DEFAULT_SECURE_COMMAND_TIMEOUT_MS;
31
+ const maxOutputBytes = options?.maxOutputBytes ?? DEFAULT_SECURE_COMMAND_OUTPUT_BYTES;
32
+ const attributes = Object.freeze([
33
+ "application",
34
+ "memorax-code",
35
+ "record",
36
+ "trial-credentials",
37
+ "namespace",
38
+ namespace,
39
+ ]);
40
+
41
+ async function invoke(operation, args, input = undefined) {
42
+ return executeSecureCommand(runner, {
43
+ backend: BACKEND,
44
+ operation,
45
+ command: SECRET_TOOL,
46
+ args,
47
+ input,
48
+ env: environment,
49
+ timeoutMs,
50
+ maxOutputBytes,
51
+ });
52
+ }
53
+
54
+ return Object.freeze({
55
+ async load() {
56
+ const result = await invoke("load", ["lookup", "--", ...attributes]);
57
+ try {
58
+ if (result.status === 1
59
+ && result.stdout.length === 0
60
+ && result.stderr.length === 0) return null;
61
+ if (result.status !== 0 || result.signal !== null) {
62
+ throw secureCredentialBackendError(BACKEND, "load", "command_failed");
63
+ }
64
+ return decodeSecureCredential(result.stdout, BACKEND, "load");
65
+ } finally {
66
+ wipeSecureCommandResult(result);
67
+ }
68
+ },
69
+
70
+ async save(serialized) {
71
+ const input = encodeSecureCredential(serialized, BACKEND, "save");
72
+ try {
73
+ const result = await invoke(
74
+ "save",
75
+ [
76
+ "store",
77
+ "--label=MemoraX Code trial credentials",
78
+ "--",
79
+ ...attributes,
80
+ ],
81
+ input,
82
+ );
83
+ try {
84
+ if (result.status !== 0 || result.signal !== null) {
85
+ throw secureCredentialBackendError(BACKEND, "save", "command_failed");
86
+ }
87
+ } finally {
88
+ wipeSecureCommandResult(result);
89
+ }
90
+ } finally {
91
+ input.fill(0);
92
+ }
93
+ },
94
+
95
+ async delete() {
96
+ const result = await invoke("delete", ["clear", "--", ...attributes]);
97
+ try {
98
+ if (result.status === 1
99
+ && result.stdout.length === 0
100
+ && result.stderr.length === 0) return false;
101
+ if (result.status !== 0 || result.signal !== null) {
102
+ throw secureCredentialBackendError(BACKEND, "delete", "command_failed");
103
+ }
104
+ return true;
105
+ } finally {
106
+ wipeSecureCommandResult(result);
107
+ }
108
+ },
109
+ });
110
+ }
@@ -0,0 +1,224 @@
1
+ import {
2
+ DEFAULT_SECURE_COMMAND_OUTPUT_BYTES,
3
+ DEFAULT_SECURE_COMMAND_TIMEOUT_MS,
4
+ decodeSecureCredential,
5
+ encodeSecureCredential,
6
+ executeSecureCommand,
7
+ minimalEnvironment,
8
+ runSecureCommand,
9
+ secureCredentialBackendError,
10
+ validateSecureCredentialNamespace,
11
+ wipeSecureCommandResult,
12
+ } from "./secure-command.mjs";
13
+
14
+ const BACKEND = "macos-keychain";
15
+ const OSASCRIPT = "/usr/bin/osascript";
16
+ const KEYCHAIN_SERVICE = "net.memorax.memorax-code.trial";
17
+ const ITEM_NOT_FOUND_EXIT = 44;
18
+ const INVALID_SECRET_EXIT = 65;
19
+ const MACOS_ENVIRONMENT_NAMES = Object.freeze([
20
+ "HOME",
21
+ "LANG",
22
+ "LC_ALL",
23
+ "LOGNAME",
24
+ "TMPDIR",
25
+ "USER",
26
+ ]);
27
+
28
+ const JXA_PREFIX = String.raw`
29
+ ObjC.import("Foundation");
30
+ ObjC.import("Security");
31
+ ObjC.bindFunction("exit", ["void", ["int"]]);
32
+ const errSecItemNotFound = -25300;
33
+ const errSecDuplicateItem = -25299;
34
+ const maxSecretBytes = 4096;
35
+ function quit(code) { $.exit(code); }
36
+ function utf8Length(value) {
37
+ return Number(value.lengthOfBytesUsingEncoding($.NSUTF8StringEncoding));
38
+ }
39
+ function itemIdentity(argv) {
40
+ if (!argv || argv.length !== 2) quit(70);
41
+ const service = $(argv[0]);
42
+ const account = $(argv[1]);
43
+ if (utf8Length(service) < 1 || utf8Length(account) < 1) quit(70);
44
+ return { service, account };
45
+ }
46
+ function findItem(identity, passwordLength, passwordData, item) {
47
+ return $.SecKeychainFindGenericPassword(
48
+ $.nil,
49
+ utf8Length(identity.service),
50
+ identity.service.UTF8String,
51
+ utf8Length(identity.account),
52
+ identity.account.UTF8String,
53
+ passwordLength,
54
+ passwordData,
55
+ item
56
+ );
57
+ }
58
+ `;
59
+
60
+ const LOAD_SCRIPT = `${JXA_PREFIX}
61
+ function run(argv) {
62
+ const identity = itemIdentity(argv);
63
+ const passwordLength = Ref();
64
+ const passwordData = Ref();
65
+ const item = Ref();
66
+ const code = findItem(identity, passwordLength, passwordData, item);
67
+ if (code === errSecItemNotFound) quit(${ITEM_NOT_FOUND_EXIT});
68
+ if (code !== 0) quit(70);
69
+ let invalidSecret = false;
70
+ try {
71
+ const length = Number(passwordLength[0]);
72
+ if (length < 1 || length > maxSecretBytes) {
73
+ invalidSecret = true;
74
+ } else {
75
+ const secret = $.NSData.alloc.initWithBytesLength(passwordData[0], length);
76
+ $.NSFileHandle.fileHandleWithStandardOutput.writeData(secret);
77
+ }
78
+ } finally {
79
+ $.SecKeychainItemFreeContent($.nil, passwordData[0]);
80
+ }
81
+ if (invalidSecret) quit(${INVALID_SECRET_EXIT});
82
+ }
83
+ `;
84
+
85
+ const SAVE_SCRIPT = `${JXA_PREFIX}
86
+ function run(argv) {
87
+ const identity = itemIdentity(argv);
88
+ const secret = $.NSFileHandle.fileHandleWithStandardInput.readDataToEndOfFile;
89
+ const length = Number(secret.length);
90
+ if (length < 1 || length > maxSecretBytes) quit(${INVALID_SECRET_EXIT});
91
+ const item = Ref();
92
+ let code = findItem(identity, null, null, item);
93
+ if (code === 0) {
94
+ code = $.SecKeychainItemModifyAttributesAndData(
95
+ item[0],
96
+ $.nil,
97
+ length,
98
+ secret.bytes
99
+ );
100
+ } else if (code === errSecItemNotFound) {
101
+ code = $.SecKeychainAddGenericPassword(
102
+ $.nil,
103
+ utf8Length(identity.service),
104
+ identity.service.UTF8String,
105
+ utf8Length(identity.account),
106
+ identity.account.UTF8String,
107
+ length,
108
+ secret.bytes,
109
+ item
110
+ );
111
+ if (code === errSecDuplicateItem) {
112
+ code = findItem(identity, null, null, item);
113
+ if (code === 0) {
114
+ code = $.SecKeychainItemModifyAttributesAndData(
115
+ item[0],
116
+ $.nil,
117
+ length,
118
+ secret.bytes
119
+ );
120
+ }
121
+ }
122
+ }
123
+ if (code !== 0) quit(70);
124
+ }
125
+ `;
126
+
127
+ const DELETE_SCRIPT = `${JXA_PREFIX}
128
+ function run(argv) {
129
+ const identity = itemIdentity(argv);
130
+ const item = Ref();
131
+ const found = findItem(identity, null, null, item);
132
+ if (found === errSecItemNotFound) quit(${ITEM_NOT_FOUND_EXIT});
133
+ if (found !== 0) quit(70);
134
+ const deleted = $.SecKeychainItemDelete(item[0]);
135
+ if (deleted !== 0) quit(70);
136
+ }
137
+ `;
138
+
139
+ export function createMacosKeychainBackend(options) {
140
+ const namespace = validateSecureCredentialNamespace(options?.namespace, BACKEND);
141
+ const runner = options?.runner ?? runSecureCommand;
142
+ const environment = minimalEnvironment(
143
+ options?.environment ?? process.env,
144
+ MACOS_ENVIRONMENT_NAMES,
145
+ );
146
+ const timeoutMs = options?.timeoutMs ?? DEFAULT_SECURE_COMMAND_TIMEOUT_MS;
147
+ const maxOutputBytes = options?.maxOutputBytes ?? DEFAULT_SECURE_COMMAND_OUTPUT_BYTES;
148
+ const account = `credential-v1:${namespace}`;
149
+
150
+ async function invoke(operation, script, input = undefined) {
151
+ return executeSecureCommand(runner, {
152
+ backend: BACKEND,
153
+ operation,
154
+ command: OSASCRIPT,
155
+ args: ["-l", "JavaScript", "-e", script, "--", KEYCHAIN_SERVICE, account],
156
+ input,
157
+ env: environment,
158
+ timeoutMs,
159
+ maxOutputBytes,
160
+ });
161
+ }
162
+
163
+ async function load() {
164
+ const result = await invoke("load", LOAD_SCRIPT);
165
+ try {
166
+ if (isEmptyExit(result, ITEM_NOT_FOUND_EXIT)) return null;
167
+ if (result.status === INVALID_SECRET_EXIT) {
168
+ throw secureCredentialBackendError(BACKEND, "load", "invalid_response");
169
+ }
170
+ if (result.status !== 0 || result.signal !== null || result.stderr.length !== 0) {
171
+ throw secureCredentialBackendError(BACKEND, "load", "command_failed");
172
+ }
173
+ return decodeSecureCredential(result.stdout, BACKEND, "load");
174
+ } finally {
175
+ wipeSecureCommandResult(result);
176
+ }
177
+ }
178
+
179
+ return Object.freeze({
180
+ load,
181
+
182
+ async save(serialized) {
183
+ const input = encodeSecureCredential(serialized, BACKEND, "save");
184
+ try {
185
+ const result = await invoke("save", SAVE_SCRIPT, input);
186
+ try {
187
+ if (result.status !== 0
188
+ || result.signal !== null
189
+ || result.stdout.length !== 0
190
+ || result.stderr.length !== 0) {
191
+ throw secureCredentialBackendError(BACKEND, "save", "command_failed");
192
+ }
193
+ } finally {
194
+ wipeSecureCommandResult(result);
195
+ }
196
+ } finally {
197
+ input.fill(0);
198
+ }
199
+ },
200
+
201
+ async delete() {
202
+ const result = await invoke("delete", DELETE_SCRIPT);
203
+ try {
204
+ if (isEmptyExit(result, ITEM_NOT_FOUND_EXIT)) return false;
205
+ if (result.status !== 0
206
+ || result.signal !== null
207
+ || result.stdout.length !== 0
208
+ || result.stderr.length !== 0) {
209
+ throw secureCredentialBackendError(BACKEND, "delete", "command_failed");
210
+ }
211
+ } finally {
212
+ wipeSecureCommandResult(result);
213
+ }
214
+ return true;
215
+ },
216
+ });
217
+ }
218
+
219
+ function isEmptyExit(result, status) {
220
+ return result.status === status
221
+ && result.signal === null
222
+ && result.stdout.length === 0
223
+ && result.stderr.length === 0;
224
+ }