@memorax/memorax-code 0.1.5 → 0.1.7
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/README.md +7 -18
- package/bin/memorax-code-npm-preinstall.mjs +28 -39
- package/bin/memorax-code-plugin-postinstall.mjs +23 -1537
- package/bin/memorax-code-setup.mjs +1615 -0
- package/bin/memorax-code.mjs +289 -5
- package/docs/configuration.md +66 -17
- package/docs/troubleshooting.md +123 -7
- package/lib/memorax-code-adapter-common/src/config-utils.d.mts +5 -1
- package/lib/memorax-code-adapter-common/src/config-utils.mjs +36 -3
- package/lib/memorax-code-adapter-common/src/credentials/linux-secret-service.mjs +110 -0
- package/lib/memorax-code-adapter-common/src/credentials/macos-keychain.mjs +224 -0
- package/lib/memorax-code-adapter-common/src/credentials/secure-command.mjs +340 -0
- package/lib/memorax-code-adapter-common/src/credentials/trial-credential-record.d.mts +82 -0
- package/lib/memorax-code-adapter-common/src/credentials/trial-credential-record.mjs +183 -0
- package/lib/memorax-code-adapter-common/src/credentials/trial-credential-store.d.mts +82 -0
- package/lib/memorax-code-adapter-common/src/credentials/trial-credential-store.mjs +271 -0
- package/lib/memorax-code-adapter-common/src/credentials/windows-dpapi.mjs +440 -0
- package/lib/memorax-code-adapter-common/src/hooks/memory-skill-reminder-hook.mjs +7 -4
- package/lib/memorax-code-adapter-common/src/setup-completion.d.mts +81 -0
- package/lib/memorax-code-adapter-common/src/setup-completion.mjs +154 -0
- package/lib/memorax-code-backend/dist/clients/claude/memory-hook-runtime.js +21 -2
- package/lib/memorax-code-backend/dist/clients/codex/memory-hook-runtime.js +23 -3
- package/lib/memorax-code-backend/dist/clients/opencode/memory-hook-runtime.js +19 -1
- package/lib/memorax-code-backend/dist/entrypoints/backend-cli.js +17 -16
- package/lib/memorax-code-backend/dist/lifecycle/backend/service.js +1 -0
- package/lib/memorax-code-backend/dist/lifecycle/orchestrator.js +81 -22
- package/lib/memorax-code-backend/dist/memorax-cli.js +3 -0
- package/lib/memorax-code-backend/dist/memory/automatic-retrieval.js +8 -2
- package/lib/memorax-code-backend/dist/memory/automatic-writeback.js +13 -1
- package/lib/memorax-code-backend/dist/memory/cli.js +10 -0
- package/lib/memorax-code-backend/dist/memory/quota-notice.js +208 -0
- package/lib/memorax-code-backend/dist/memory/service.js +11 -0
- package/lib/memorax-code-backend/dist/provider/memorax/adapter.js +4 -2
- package/lib/memorax-code-backend/dist/provider/memorax/http.js +10 -3
- package/lib/memorax-code-backend/dist/provider/memorax/quota.js +26 -0
- package/lib/memorax-code-backend/package.json +1 -1
- package/lib/memorax-code-claude-adapter/.claude-plugin/plugin.json +1 -1
- package/lib/memorax-code-claude-adapter/hooks/runtime-shell.json +1 -1
- package/lib/memorax-code-claude-adapter/package.json +1 -1
- package/lib/memorax-code-claude-adapter/runtime-hooks/memory-turn.mjs +5 -3
- package/lib/memorax-code-claude-adapter/skills/memorax-code/references/memorax-add.md +2 -0
- package/lib/memorax-code-claude-adapter/skills/memorax-code/references/memorax-search.md +4 -2
- package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/.claude-plugin/plugin.json +1 -1
- package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/hooks/runtime-shell.json +1 -1
- package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/memorax-code-adapter-common/src/config-utils.d.mts +5 -1
- package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/memorax-code-adapter-common/src/config-utils.mjs +36 -3
- package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/memorax-code-adapter-common/src/credentials/linux-secret-service.mjs +110 -0
- package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/memorax-code-adapter-common/src/credentials/macos-keychain.mjs +224 -0
- package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/memorax-code-adapter-common/src/credentials/secure-command.mjs +340 -0
- package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/memorax-code-adapter-common/src/credentials/trial-credential-record.d.mts +82 -0
- package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/memorax-code-adapter-common/src/credentials/trial-credential-record.mjs +183 -0
- package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/memorax-code-adapter-common/src/credentials/trial-credential-store.d.mts +82 -0
- package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/memorax-code-adapter-common/src/credentials/trial-credential-store.mjs +271 -0
- package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/memorax-code-adapter-common/src/credentials/windows-dpapi.mjs +440 -0
- package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/memorax-code-adapter-common/src/hooks/memory-skill-reminder-hook.mjs +7 -4
- package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/memorax-code-adapter-common/src/setup-completion.d.mts +81 -0
- package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/memorax-code-adapter-common/src/setup-completion.mjs +154 -0
- package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/package.json +1 -1
- package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/runtime-hooks/memory-turn.mjs +5 -3
- package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/skills/memorax-code/references/memorax-add.md +2 -0
- package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/skills/memorax-code/references/memorax-search.md +4 -2
- package/lib/memorax-code-codex-adapter/.codex-plugin/plugin.json +1 -1
- package/lib/memorax-code-codex-adapter/hooks/runtime-shell.json +1 -1
- package/lib/memorax-code-codex-adapter/package.json +1 -1
- package/lib/memorax-code-codex-adapter/runtime-hooks/memory-skill-reminder.mjs +2 -0
- package/lib/memorax-code-codex-adapter/skills/memorax-code/references/memorax-add.md +2 -0
- package/lib/memorax-code-codex-adapter/skills/memorax-code/references/memorax-search.md +4 -2
- package/lib/memorax-code-dsh-adapter/hooks/repo-memory-job.mjs +5 -2
- package/lib/memorax-code-dsh-adapter/package.json +1 -1
- package/lib/memorax-code-dsh-adapter/skills/memorax-code/references/memorax-add.md +2 -0
- package/lib/memorax-code-dsh-adapter/skills/memorax-code/references/memorax-search.md +4 -2
- package/lib/memorax-code-dsh-adapter/src/profile-lifecycle.mjs +158 -7
- package/lib/memorax-code-dsh-adapter/src/runtime-state.mjs +21 -0
- package/lib/memorax-code-opencode-adapter/package.json +1 -1
- package/lib/memorax-code-opencode-adapter/skills/memorax-code/references/memorax-add.md +2 -0
- package/lib/memorax-code-opencode-adapter/skills/memorax-code/references/memorax-search.md +4 -2
- package/lib/memorax-code-opencode-adapter/src/plugin.mjs +20 -0
- package/lib/package-transition.mjs +312 -0
- package/lib/resolve-codex-command.mjs +32 -6
- package/lib/setup-memory-preferences.mjs +113 -0
- package/lib/setup-reconcile.mjs +128 -0
- package/lib/trial-plugin-mark.mjs +171 -0
- package/lib/trial-provision-client.mjs +307 -0
- package/lib/trial-provision-flow.mjs +215 -0
- package/lib/trial-setup.mjs +53 -0
- 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
|
|
24
|
-
validateMemoraxEnvelope(
|
|
25
|
-
|
|
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
|
+
}
|
|
@@ -35,12 +35,14 @@ try {
|
|
|
35
35
|
pluginRoot: process.env.CLAUDE_PLUGIN_ROOT,
|
|
36
36
|
});
|
|
37
37
|
const additionalContext = stringValue(response?.additionalContext);
|
|
38
|
-
|
|
38
|
+
const systemMessage = stringValue(response?.userNotice);
|
|
39
|
+
if (additionalContext || systemMessage) {
|
|
39
40
|
process.stdout.write(`${JSON.stringify({
|
|
40
|
-
|
|
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
|
|
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
|
|
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.
|
|
@@ -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?:
|
|
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
|
-
|
|
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
|
+
}
|