@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.
- 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 +105 -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/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 +311 -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
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { TextDecoder } from "node:util";
|
|
3
|
+
|
|
4
|
+
export const MAX_SECURE_CREDENTIAL_BYTES = 4096;
|
|
5
|
+
export const DEFAULT_SECURE_COMMAND_TIMEOUT_MS = 30_000;
|
|
6
|
+
export const DEFAULT_SECURE_COMMAND_OUTPUT_BYTES = 8192;
|
|
7
|
+
|
|
8
|
+
const UTF8_DECODER = new TextDecoder("utf-8", { fatal: true });
|
|
9
|
+
const NAMESPACE_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
10
|
+
const ERROR_REASONS = new Set([
|
|
11
|
+
"backend_unavailable",
|
|
12
|
+
"command_failed",
|
|
13
|
+
"command_timeout",
|
|
14
|
+
"invalid_namespace",
|
|
15
|
+
"invalid_response",
|
|
16
|
+
"invalid_secret",
|
|
17
|
+
"output_limit",
|
|
18
|
+
"secret_too_large",
|
|
19
|
+
"storage_failed",
|
|
20
|
+
"unsafe_path",
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
export class SecureCredentialBackendError extends Error {
|
|
24
|
+
constructor({ backend, operation, reason }) {
|
|
25
|
+
super("Secure credential backend operation failed.");
|
|
26
|
+
this.name = "SecureCredentialBackendError";
|
|
27
|
+
this.code = "TRIAL_CREDENTIAL_BACKEND_ERROR";
|
|
28
|
+
this.backend = safeIdentifier(backend, "unknown");
|
|
29
|
+
this.operation = safeIdentifier(operation, "unknown");
|
|
30
|
+
this.reason = ERROR_REASONS.has(reason) ? reason : "command_failed";
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function secureCredentialBackendError(backend, operation, reason) {
|
|
35
|
+
return new SecureCredentialBackendError({ backend, operation, reason });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function validateSecureCredentialNamespace(namespace, backend, operation = "initialize") {
|
|
39
|
+
if (typeof namespace !== "string" || !NAMESPACE_PATTERN.test(namespace)) {
|
|
40
|
+
throw secureCredentialBackendError(backend, operation, "invalid_namespace");
|
|
41
|
+
}
|
|
42
|
+
return namespace;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function encodeSecureCredential(serialized, backend, operation = "save") {
|
|
46
|
+
if (typeof serialized !== "string" || serialized.length === 0 || serialized.includes("\0")) {
|
|
47
|
+
throw secureCredentialBackendError(backend, operation, "invalid_secret");
|
|
48
|
+
}
|
|
49
|
+
const encoded = Buffer.from(serialized, "utf8");
|
|
50
|
+
if (encoded.length > MAX_SECURE_CREDENTIAL_BYTES) {
|
|
51
|
+
encoded.fill(0);
|
|
52
|
+
throw secureCredentialBackendError(backend, operation, "secret_too_large");
|
|
53
|
+
}
|
|
54
|
+
return encoded;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function decodeSecureCredential(encoded, backend, operation = "load") {
|
|
58
|
+
const bytes = bufferFrom(encoded);
|
|
59
|
+
try {
|
|
60
|
+
if (bytes.length === 0 || bytes.length > MAX_SECURE_CREDENTIAL_BYTES) {
|
|
61
|
+
throw secureCredentialBackendError(
|
|
62
|
+
backend,
|
|
63
|
+
operation,
|
|
64
|
+
bytes.length > MAX_SECURE_CREDENTIAL_BYTES ? "secret_too_large" : "invalid_response",
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
let decoded;
|
|
68
|
+
try {
|
|
69
|
+
decoded = UTF8_DECODER.decode(bytes);
|
|
70
|
+
} catch {
|
|
71
|
+
throw secureCredentialBackendError(backend, operation, "invalid_response");
|
|
72
|
+
}
|
|
73
|
+
if (!decoded || decoded.includes("\0")) {
|
|
74
|
+
throw secureCredentialBackendError(backend, operation, "invalid_response");
|
|
75
|
+
}
|
|
76
|
+
return decoded;
|
|
77
|
+
} finally {
|
|
78
|
+
bytes.fill(0);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export async function executeSecureCommand(runner, specification) {
|
|
83
|
+
const backend = safeIdentifier(specification?.backend, "unknown");
|
|
84
|
+
const operation = safeIdentifier(specification?.operation, "unknown");
|
|
85
|
+
let result;
|
|
86
|
+
try {
|
|
87
|
+
result = await runner(specification);
|
|
88
|
+
} catch (error) {
|
|
89
|
+
if (error instanceof SecureCredentialBackendError) {
|
|
90
|
+
throw secureCredentialBackendError(backend, operation, error.reason);
|
|
91
|
+
}
|
|
92
|
+
throw secureCredentialBackendError(backend, operation, "command_failed");
|
|
93
|
+
}
|
|
94
|
+
if (!result || typeof result !== "object") {
|
|
95
|
+
throw secureCredentialBackendError(backend, operation, "invalid_response");
|
|
96
|
+
}
|
|
97
|
+
let rawStdout;
|
|
98
|
+
let rawStderr;
|
|
99
|
+
let stdout;
|
|
100
|
+
let stderr;
|
|
101
|
+
let returned = false;
|
|
102
|
+
try {
|
|
103
|
+
rawStdout = result.stdout;
|
|
104
|
+
rawStderr = result.stderr;
|
|
105
|
+
const rawStatus = result.status;
|
|
106
|
+
const status = rawStatus === null || Number.isInteger(rawStatus)
|
|
107
|
+
? rawStatus
|
|
108
|
+
: undefined;
|
|
109
|
+
if (status === undefined) {
|
|
110
|
+
throw secureCredentialBackendError(backend, operation, "invalid_response");
|
|
111
|
+
}
|
|
112
|
+
stdout = resultBuffer(rawStdout);
|
|
113
|
+
stderr = resultBuffer(rawStderr);
|
|
114
|
+
if (stdout === undefined || stderr === undefined) {
|
|
115
|
+
throw secureCredentialBackendError(backend, operation, "invalid_response");
|
|
116
|
+
}
|
|
117
|
+
const outputLimit = positiveInteger(
|
|
118
|
+
specification.maxOutputBytes,
|
|
119
|
+
DEFAULT_SECURE_COMMAND_OUTPUT_BYTES,
|
|
120
|
+
);
|
|
121
|
+
if (stdout.length > outputLimit || stderr.length > outputLimit) {
|
|
122
|
+
throw secureCredentialBackendError(backend, operation, "output_limit");
|
|
123
|
+
}
|
|
124
|
+
const signal = result.signal;
|
|
125
|
+
returned = true;
|
|
126
|
+
return {
|
|
127
|
+
status,
|
|
128
|
+
signal: typeof signal === "string" ? signal : null,
|
|
129
|
+
stdout,
|
|
130
|
+
stderr,
|
|
131
|
+
};
|
|
132
|
+
} catch (error) {
|
|
133
|
+
if (error instanceof SecureCredentialBackendError) {
|
|
134
|
+
throw secureCredentialBackendError(backend, operation, error.reason);
|
|
135
|
+
}
|
|
136
|
+
throw secureCredentialBackendError(backend, operation, "invalid_response");
|
|
137
|
+
} finally {
|
|
138
|
+
wipeByteArray(rawStdout);
|
|
139
|
+
wipeByteArray(rawStderr);
|
|
140
|
+
if (!returned) {
|
|
141
|
+
stdout?.fill(0);
|
|
142
|
+
stderr?.fill(0);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function wipeSecureCommandResult(result) {
|
|
148
|
+
if (!result || typeof result !== "object") return;
|
|
149
|
+
try {
|
|
150
|
+
wipeByteArray(result.stdout);
|
|
151
|
+
} catch {
|
|
152
|
+
// Best-effort cleanup must not replace the operation result.
|
|
153
|
+
}
|
|
154
|
+
try {
|
|
155
|
+
wipeByteArray(result.stderr);
|
|
156
|
+
} catch {
|
|
157
|
+
// Best-effort cleanup must not replace the operation result.
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function runSecureCommand(specification) {
|
|
162
|
+
const backend = safeIdentifier(specification?.backend, "unknown");
|
|
163
|
+
const operation = safeIdentifier(specification?.operation, "unknown");
|
|
164
|
+
const timeoutMs = positiveInteger(
|
|
165
|
+
specification?.timeoutMs,
|
|
166
|
+
DEFAULT_SECURE_COMMAND_TIMEOUT_MS,
|
|
167
|
+
);
|
|
168
|
+
const maxOutputBytes = positiveInteger(
|
|
169
|
+
specification?.maxOutputBytes,
|
|
170
|
+
DEFAULT_SECURE_COMMAND_OUTPUT_BYTES,
|
|
171
|
+
);
|
|
172
|
+
const input = bufferFrom(specification?.input);
|
|
173
|
+
let child;
|
|
174
|
+
try {
|
|
175
|
+
child = spawn(specification.command, specification.args ?? [], {
|
|
176
|
+
env: specification.env ?? Object.create(null),
|
|
177
|
+
shell: false,
|
|
178
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
179
|
+
windowsHide: true,
|
|
180
|
+
});
|
|
181
|
+
} catch {
|
|
182
|
+
input.fill(0);
|
|
183
|
+
return Promise.reject(secureCredentialBackendError(
|
|
184
|
+
backend,
|
|
185
|
+
operation,
|
|
186
|
+
"backend_unavailable",
|
|
187
|
+
));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return new Promise((resolve, reject) => {
|
|
191
|
+
const stdout = [];
|
|
192
|
+
const stderr = [];
|
|
193
|
+
let stdoutBytes = 0;
|
|
194
|
+
let stderrBytes = 0;
|
|
195
|
+
let settled = false;
|
|
196
|
+
let timer;
|
|
197
|
+
|
|
198
|
+
const wipeInput = () => input.fill(0);
|
|
199
|
+
const wipeOutput = () => {
|
|
200
|
+
for (const chunk of stdout) chunk.fill(0);
|
|
201
|
+
for (const chunk of stderr) chunk.fill(0);
|
|
202
|
+
};
|
|
203
|
+
const closePipes = () => {
|
|
204
|
+
child.stdin.destroy();
|
|
205
|
+
child.stdout.destroy();
|
|
206
|
+
child.stderr.destroy();
|
|
207
|
+
};
|
|
208
|
+
const finishError = (reason, { terminate = false } = {}) => {
|
|
209
|
+
if (settled) return;
|
|
210
|
+
settled = true;
|
|
211
|
+
clearTimeout(timer);
|
|
212
|
+
if (terminate) stopChild();
|
|
213
|
+
closePipes();
|
|
214
|
+
child.unref?.();
|
|
215
|
+
wipeInput();
|
|
216
|
+
wipeOutput();
|
|
217
|
+
reject(secureCredentialBackendError(backend, operation, reason));
|
|
218
|
+
};
|
|
219
|
+
const stopChild = () => {
|
|
220
|
+
try {
|
|
221
|
+
child.kill("SIGKILL");
|
|
222
|
+
} catch {
|
|
223
|
+
// Best effort; the caller still receives the bounded, redacted failure.
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
const collect = (chunk, chunks, currentBytes, setBytes) => {
|
|
227
|
+
const bytes = Buffer.from(chunk);
|
|
228
|
+
if (settled) {
|
|
229
|
+
bytes.fill(0);
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
if (currentBytes + bytes.length > maxOutputBytes) {
|
|
233
|
+
bytes.fill(0);
|
|
234
|
+
finishError("output_limit", { terminate: true });
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
chunks.push(bytes);
|
|
238
|
+
setBytes(currentBytes + bytes.length);
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
timer = setTimeout(
|
|
242
|
+
() => finishError("command_timeout", { terminate: true }),
|
|
243
|
+
timeoutMs,
|
|
244
|
+
);
|
|
245
|
+
timer.unref?.();
|
|
246
|
+
|
|
247
|
+
child.stdout.on("data", (chunk) => collect(
|
|
248
|
+
chunk,
|
|
249
|
+
stdout,
|
|
250
|
+
stdoutBytes,
|
|
251
|
+
(value) => { stdoutBytes = value; },
|
|
252
|
+
));
|
|
253
|
+
child.stderr.on("data", (chunk) => collect(
|
|
254
|
+
chunk,
|
|
255
|
+
stderr,
|
|
256
|
+
stderrBytes,
|
|
257
|
+
(value) => { stderrBytes = value; },
|
|
258
|
+
));
|
|
259
|
+
child.stdout.on("error", () => finishError("command_failed", { terminate: true }));
|
|
260
|
+
child.stderr.on("error", () => finishError("command_failed", { terminate: true }));
|
|
261
|
+
child.stdin.on("error", () => {
|
|
262
|
+
// A non-zero child result remains the authority for a closed stdin.
|
|
263
|
+
});
|
|
264
|
+
child.once("error", () => finishError("backend_unavailable"));
|
|
265
|
+
child.once("close", (status, signal) => {
|
|
266
|
+
if (settled) return;
|
|
267
|
+
settled = true;
|
|
268
|
+
clearTimeout(timer);
|
|
269
|
+
wipeInput();
|
|
270
|
+
const collectedStdout = Buffer.concat(stdout, stdoutBytes);
|
|
271
|
+
const collectedStderr = Buffer.concat(stderr, stderrBytes);
|
|
272
|
+
wipeOutput();
|
|
273
|
+
resolve({
|
|
274
|
+
status: Number.isInteger(status) ? status : null,
|
|
275
|
+
signal: typeof signal === "string" ? signal : null,
|
|
276
|
+
stdout: collectedStdout,
|
|
277
|
+
stderr: collectedStderr,
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
child.stdin.end(input);
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export function minimalEnvironment(source, names, overrides = undefined) {
|
|
285
|
+
const environment = Object.create(null);
|
|
286
|
+
for (const name of names) {
|
|
287
|
+
const value = environmentValue(source, name);
|
|
288
|
+
if (value !== undefined) environment[name] = value;
|
|
289
|
+
}
|
|
290
|
+
if (overrides) {
|
|
291
|
+
for (const [name, value] of Object.entries(overrides)) {
|
|
292
|
+
if (typeof value === "string" && value) environment[name] = value;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return environment;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export function environmentValue(source, requestedName) {
|
|
299
|
+
if (!source || typeof source !== "object") return undefined;
|
|
300
|
+
const exact = source[requestedName];
|
|
301
|
+
if (typeof exact === "string" && exact) return exact;
|
|
302
|
+
const requested = requestedName.toLowerCase();
|
|
303
|
+
const matchingName = Object.keys(source).find((name) => name.toLowerCase() === requested);
|
|
304
|
+
const matching = matchingName === undefined ? undefined : source[matchingName];
|
|
305
|
+
return typeof matching === "string" && matching ? matching : undefined;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function bufferFrom(value) {
|
|
309
|
+
if (value === undefined || value === null) return Buffer.alloc(0);
|
|
310
|
+
if (Buffer.isBuffer(value)) return Buffer.from(value);
|
|
311
|
+
if (value instanceof Uint8Array) {
|
|
312
|
+
return Buffer.from(value);
|
|
313
|
+
}
|
|
314
|
+
if (typeof value === "string") return Buffer.from(value, "utf8");
|
|
315
|
+
return Buffer.alloc(0);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function resultBuffer(value) {
|
|
319
|
+
if (value === undefined || value === null) return Buffer.alloc(0);
|
|
320
|
+
if (Buffer.isBuffer(value)) return Buffer.from(value);
|
|
321
|
+
if (value instanceof Uint8Array) {
|
|
322
|
+
return Buffer.from(value);
|
|
323
|
+
}
|
|
324
|
+
if (typeof value === "string") return Buffer.from(value, "utf8");
|
|
325
|
+
return undefined;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function wipeByteArray(value) {
|
|
329
|
+
if (Buffer.isBuffer(value) || value instanceof Uint8Array) value.fill(0);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function positiveInteger(value, fallback) {
|
|
333
|
+
return Number.isSafeInteger(value) && value > 0 ? value : fallback;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function safeIdentifier(value, fallback) {
|
|
337
|
+
return typeof value === "string" && /^[a-z0-9_-]{1,64}$/.test(value)
|
|
338
|
+
? value
|
|
339
|
+
: fallback;
|
|
340
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
export const TRIAL_CREDENTIAL_RECORD_VERSION: 1;
|
|
2
|
+
|
|
3
|
+
export type TrialCredentialRecordState = "provisioning" | "ready";
|
|
4
|
+
|
|
5
|
+
type TrialCredentialIdentity = Readonly<{
|
|
6
|
+
version: 1;
|
|
7
|
+
mark_id: string;
|
|
8
|
+
mark_version: 1;
|
|
9
|
+
app_salt: "memorax-plugin-v1";
|
|
10
|
+
machine_id: string;
|
|
11
|
+
hostname: string;
|
|
12
|
+
platform: "windows" | "linux" | "macos";
|
|
13
|
+
arch: "x86_64" | "arm64";
|
|
14
|
+
mac_hash: string;
|
|
15
|
+
}>;
|
|
16
|
+
|
|
17
|
+
export type TrialCredentialRecord = TrialCredentialIdentity & (
|
|
18
|
+
| Readonly<{
|
|
19
|
+
state: "provisioning";
|
|
20
|
+
api_key: null;
|
|
21
|
+
account_id: null;
|
|
22
|
+
project_id: null;
|
|
23
|
+
}>
|
|
24
|
+
| Readonly<{
|
|
25
|
+
state: "ready";
|
|
26
|
+
api_key: string;
|
|
27
|
+
account_id: string;
|
|
28
|
+
project_id: string;
|
|
29
|
+
}>
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
export type TrialCredentialRecordInvalidReason =
|
|
33
|
+
| "malformed_json"
|
|
34
|
+
| "invalid_record"
|
|
35
|
+
| "unknown_fields"
|
|
36
|
+
| "missing_fields"
|
|
37
|
+
| "unsupported_version"
|
|
38
|
+
| "invalid_version"
|
|
39
|
+
| "invalid_state"
|
|
40
|
+
| "invalid_mark_id"
|
|
41
|
+
| "invalid_mark_version"
|
|
42
|
+
| "invalid_app_salt"
|
|
43
|
+
| "invalid_machine_id"
|
|
44
|
+
| "invalid_hostname"
|
|
45
|
+
| "invalid_platform"
|
|
46
|
+
| "invalid_arch"
|
|
47
|
+
| "invalid_mac_hash"
|
|
48
|
+
| "invalid_api_key"
|
|
49
|
+
| "invalid_shape"
|
|
50
|
+
| "invalid_account_id"
|
|
51
|
+
| "invalid_project_id"
|
|
52
|
+
| "invalid_transition";
|
|
53
|
+
|
|
54
|
+
export class TrialCredentialRecordError extends Error {
|
|
55
|
+
constructor(reason: TrialCredentialRecordInvalidReason);
|
|
56
|
+
readonly code: "TRIAL_CREDENTIAL_RECORD_INVALID";
|
|
57
|
+
readonly reason: TrialCredentialRecordInvalidReason;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function parseTrialCredentialRecord(text: string): TrialCredentialRecord;
|
|
61
|
+
export function validateTrialCredentialRecord(value: unknown): TrialCredentialRecord;
|
|
62
|
+
export function serializeTrialCredentialRecord(value: unknown): string;
|
|
63
|
+
|
|
64
|
+
export function createInitialTrialCredentialRecord(options: Readonly<{
|
|
65
|
+
markId: string;
|
|
66
|
+
markVersion: 1;
|
|
67
|
+
appSalt: "memorax-plugin-v1";
|
|
68
|
+
machineId: string;
|
|
69
|
+
hostname: string;
|
|
70
|
+
platform: "windows" | "linux" | "macos";
|
|
71
|
+
arch: "x86_64" | "arm64";
|
|
72
|
+
macHash: string;
|
|
73
|
+
}>): TrialCredentialRecord;
|
|
74
|
+
|
|
75
|
+
export function completeTrialCredentialProvisioning(
|
|
76
|
+
value: unknown,
|
|
77
|
+
metadata: Readonly<{
|
|
78
|
+
accountId: string;
|
|
79
|
+
projectId: string;
|
|
80
|
+
apiKey: string;
|
|
81
|
+
}>,
|
|
82
|
+
): TrialCredentialRecord;
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
export const TRIAL_CREDENTIAL_RECORD_VERSION = 1;
|
|
2
|
+
|
|
3
|
+
const RECORD_KEYS = Object.freeze([
|
|
4
|
+
"version",
|
|
5
|
+
"state",
|
|
6
|
+
"mark_id",
|
|
7
|
+
"mark_version",
|
|
8
|
+
"app_salt",
|
|
9
|
+
"machine_id",
|
|
10
|
+
"hostname",
|
|
11
|
+
"platform",
|
|
12
|
+
"arch",
|
|
13
|
+
"mac_hash",
|
|
14
|
+
"api_key",
|
|
15
|
+
"account_id",
|
|
16
|
+
"project_id",
|
|
17
|
+
]);
|
|
18
|
+
const RECORD_KEY_SET = new Set(RECORD_KEYS);
|
|
19
|
+
const MARK_ID_PATTERN = /^mk_[0-9a-f]{64}$/;
|
|
20
|
+
const MACHINE_ID_PATTERN = /^[A-Za-z0-9._:-]{1,128}$/;
|
|
21
|
+
const SHA256_HEX_PATTERN = /^[0-9a-f]{64}$/;
|
|
22
|
+
const API_KEY_PATTERN = /^sk_[A-Za-z0-9_-]{43}$/;
|
|
23
|
+
const DECIMAL_PUBLIC_ID_PATTERN = /^[0-9]+$/;
|
|
24
|
+
const ERROR_REASONS = new Set([
|
|
25
|
+
"malformed_json",
|
|
26
|
+
"invalid_record",
|
|
27
|
+
"unknown_fields",
|
|
28
|
+
"missing_fields",
|
|
29
|
+
"unsupported_version",
|
|
30
|
+
"invalid_version",
|
|
31
|
+
"invalid_state",
|
|
32
|
+
"invalid_mark_id",
|
|
33
|
+
"invalid_mark_version",
|
|
34
|
+
"invalid_app_salt",
|
|
35
|
+
"invalid_machine_id",
|
|
36
|
+
"invalid_hostname",
|
|
37
|
+
"invalid_platform",
|
|
38
|
+
"invalid_arch",
|
|
39
|
+
"invalid_mac_hash",
|
|
40
|
+
"invalid_api_key",
|
|
41
|
+
"invalid_shape",
|
|
42
|
+
"invalid_account_id",
|
|
43
|
+
"invalid_project_id",
|
|
44
|
+
"invalid_transition",
|
|
45
|
+
]);
|
|
46
|
+
|
|
47
|
+
export class TrialCredentialRecordError extends Error {
|
|
48
|
+
constructor(reason) {
|
|
49
|
+
const safeReason = ERROR_REASONS.has(reason) ? reason : "invalid_record";
|
|
50
|
+
super(`Trial credential record is invalid (${safeReason})`);
|
|
51
|
+
this.name = "TrialCredentialRecordError";
|
|
52
|
+
this.code = "TRIAL_CREDENTIAL_RECORD_INVALID";
|
|
53
|
+
this.reason = safeReason;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function parseTrialCredentialRecord(text) {
|
|
58
|
+
if (typeof text !== "string") {
|
|
59
|
+
throw new TypeError("Trial credential record input must be a string");
|
|
60
|
+
}
|
|
61
|
+
let value;
|
|
62
|
+
try {
|
|
63
|
+
value = JSON.parse(text);
|
|
64
|
+
} catch {
|
|
65
|
+
fail("malformed_json");
|
|
66
|
+
}
|
|
67
|
+
return validateRecord(value);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function validateTrialCredentialRecord(value) {
|
|
71
|
+
return validateRecord(value);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function serializeTrialCredentialRecord(value) {
|
|
75
|
+
const record = validateTrialCredentialRecord(value);
|
|
76
|
+
return `${JSON.stringify(record, null, 2)}\n`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function createInitialTrialCredentialRecord(options) {
|
|
80
|
+
return validateRecord({
|
|
81
|
+
version: TRIAL_CREDENTIAL_RECORD_VERSION,
|
|
82
|
+
state: "provisioning",
|
|
83
|
+
mark_id: options?.markId,
|
|
84
|
+
mark_version: options?.markVersion,
|
|
85
|
+
app_salt: options?.appSalt,
|
|
86
|
+
machine_id: options?.machineId,
|
|
87
|
+
hostname: options?.hostname,
|
|
88
|
+
platform: options?.platform,
|
|
89
|
+
arch: options?.arch,
|
|
90
|
+
mac_hash: options?.macHash,
|
|
91
|
+
api_key: null,
|
|
92
|
+
account_id: null,
|
|
93
|
+
project_id: null,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function completeTrialCredentialProvisioning(value, metadata) {
|
|
98
|
+
const record = validateRecord(value);
|
|
99
|
+
if (record.state !== "provisioning") fail("invalid_transition");
|
|
100
|
+
return validateRecord({
|
|
101
|
+
...record,
|
|
102
|
+
state: "ready",
|
|
103
|
+
api_key: metadata?.apiKey,
|
|
104
|
+
account_id: metadata?.accountId,
|
|
105
|
+
project_id: metadata?.projectId,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function validateRecord(value) {
|
|
110
|
+
if (!isRecord(value)) fail("invalid_record");
|
|
111
|
+
const keys = Reflect.ownKeys(value);
|
|
112
|
+
if (keys.some((key) => typeof key !== "string" || !RECORD_KEY_SET.has(key))) {
|
|
113
|
+
fail("unknown_fields");
|
|
114
|
+
}
|
|
115
|
+
if (RECORD_KEYS.some((key) => !Object.hasOwn(value, key))) fail("missing_fields");
|
|
116
|
+
const snapshot = Object.fromEntries(RECORD_KEYS.map((key) => [key, value[key]]));
|
|
117
|
+
|
|
118
|
+
if (snapshot.version !== TRIAL_CREDENTIAL_RECORD_VERSION) {
|
|
119
|
+
if (Number.isSafeInteger(snapshot.version) && snapshot.version > 0) {
|
|
120
|
+
fail("unsupported_version");
|
|
121
|
+
}
|
|
122
|
+
fail("invalid_version");
|
|
123
|
+
}
|
|
124
|
+
if (snapshot.state !== "provisioning" && snapshot.state !== "ready") {
|
|
125
|
+
fail("invalid_state");
|
|
126
|
+
}
|
|
127
|
+
if (typeof snapshot.mark_id !== "string" || !MARK_ID_PATTERN.test(snapshot.mark_id)) {
|
|
128
|
+
fail("invalid_mark_id");
|
|
129
|
+
}
|
|
130
|
+
if (snapshot.mark_version !== 1) fail("invalid_mark_version");
|
|
131
|
+
if (snapshot.app_salt !== "memorax-plugin-v1") fail("invalid_app_salt");
|
|
132
|
+
if (typeof snapshot.machine_id !== "string"
|
|
133
|
+
|| !MACHINE_ID_PATTERN.test(snapshot.machine_id)) {
|
|
134
|
+
fail("invalid_machine_id");
|
|
135
|
+
}
|
|
136
|
+
if (!validBoundedText(snapshot.hostname, 120)) fail("invalid_hostname");
|
|
137
|
+
if (!["windows", "linux", "macos"].includes(snapshot.platform)) {
|
|
138
|
+
fail("invalid_platform");
|
|
139
|
+
}
|
|
140
|
+
if (!["x86_64", "arm64"].includes(snapshot.arch)) fail("invalid_arch");
|
|
141
|
+
if (typeof snapshot.mac_hash !== "string"
|
|
142
|
+
|| !SHA256_HEX_PATTERN.test(snapshot.mac_hash)) {
|
|
143
|
+
fail("invalid_mac_hash");
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (snapshot.state === "provisioning") {
|
|
147
|
+
if (snapshot.api_key !== null
|
|
148
|
+
|| snapshot.account_id !== null
|
|
149
|
+
|| snapshot.project_id !== null) {
|
|
150
|
+
fail("invalid_shape");
|
|
151
|
+
}
|
|
152
|
+
return Object.freeze(snapshot);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (typeof snapshot.api_key !== "string" || !API_KEY_PATTERN.test(snapshot.api_key)) {
|
|
156
|
+
fail("invalid_api_key");
|
|
157
|
+
}
|
|
158
|
+
if (!decimalPublicId(snapshot.account_id)) fail("invalid_account_id");
|
|
159
|
+
if (!decimalPublicId(snapshot.project_id)) fail("invalid_project_id");
|
|
160
|
+
return Object.freeze(snapshot);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function decimalPublicId(value) {
|
|
164
|
+
return typeof value === "string" && DECIMAL_PUBLIC_ID_PATTERN.test(value);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function validBoundedText(value, maximumLength) {
|
|
168
|
+
return typeof value === "string"
|
|
169
|
+
&& value.length > 0
|
|
170
|
+
&& value.length <= maximumLength
|
|
171
|
+
&& value === value.trim()
|
|
172
|
+
&& !value.includes("\0");
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function isRecord(value) {
|
|
176
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
177
|
+
const prototype = Object.getPrototypeOf(value);
|
|
178
|
+
return prototype === Object.prototype || prototype === null;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function fail(reason) {
|
|
182
|
+
throw new TrialCredentialRecordError(reason);
|
|
183
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { AsyncJsonFileLockOptions } from "../config-utils.mjs";
|
|
2
|
+
import type { TrialCredentialRecord } from "./trial-credential-record.mjs";
|
|
3
|
+
|
|
4
|
+
export type TrialCredentialSecureBackend = Readonly<{
|
|
5
|
+
load(): Promise<string | null>;
|
|
6
|
+
save(serialized: string): Promise<void>;
|
|
7
|
+
delete(): Promise<boolean>;
|
|
8
|
+
}>;
|
|
9
|
+
|
|
10
|
+
export type TrialCredentialStoreOptions = Readonly<{
|
|
11
|
+
memoraxCodeHome?: string;
|
|
12
|
+
env?: Record<string, string | undefined>;
|
|
13
|
+
platform?: NodeJS.Platform;
|
|
14
|
+
backend?: TrialCredentialSecureBackend;
|
|
15
|
+
runtime?: Record<string, unknown>;
|
|
16
|
+
lockOptions?: AsyncJsonFileLockOptions;
|
|
17
|
+
provisionLockOptions?: AsyncJsonFileLockOptions;
|
|
18
|
+
}>;
|
|
19
|
+
|
|
20
|
+
export type TrialCredentialNamespaceOptions = Readonly<{
|
|
21
|
+
platform?: NodeJS.Platform;
|
|
22
|
+
resolveHome?: (value: string) => string;
|
|
23
|
+
}>;
|
|
24
|
+
|
|
25
|
+
export type TrialCredentialStorePort = Readonly<{
|
|
26
|
+
load(): Promise<TrialCredentialRecord | null>;
|
|
27
|
+
createIfAbsent(
|
|
28
|
+
value: unknown,
|
|
29
|
+
): Promise<Readonly<{ record: TrialCredentialRecord; created: boolean }>>;
|
|
30
|
+
complete(
|
|
31
|
+
value: TrialCredentialRecord,
|
|
32
|
+
metadata: Readonly<{
|
|
33
|
+
accountId: string;
|
|
34
|
+
projectId: string;
|
|
35
|
+
apiKey: string;
|
|
36
|
+
}>,
|
|
37
|
+
): Promise<TrialCredentialRecord>;
|
|
38
|
+
withProvisionLock<T>(
|
|
39
|
+
operation: () => T | Promise<T>,
|
|
40
|
+
options?: AsyncJsonFileLockOptions,
|
|
41
|
+
): Promise<T>;
|
|
42
|
+
}>;
|
|
43
|
+
|
|
44
|
+
export function trialCredentialNamespace(
|
|
45
|
+
memoraxCodeHome: string,
|
|
46
|
+
options?: TrialCredentialNamespaceOptions,
|
|
47
|
+
): string;
|
|
48
|
+
|
|
49
|
+
export function trialCredentialLockPath(memoraxCodeHome: string): string;
|
|
50
|
+
export function trialCredentialProvisionLockPath(memoraxCodeHome: string): string;
|
|
51
|
+
|
|
52
|
+
export function withTrialCredentialProvisionLock<T>(
|
|
53
|
+
operation: () => T | Promise<T>,
|
|
54
|
+
options?: TrialCredentialStoreOptions,
|
|
55
|
+
): Promise<T>;
|
|
56
|
+
|
|
57
|
+
export function createTrialCredentialStorePort(
|
|
58
|
+
options?: TrialCredentialStoreOptions,
|
|
59
|
+
): TrialCredentialStorePort;
|
|
60
|
+
|
|
61
|
+
export function loadTrialCredentialRecord(
|
|
62
|
+
options?: TrialCredentialStoreOptions,
|
|
63
|
+
): Promise<TrialCredentialRecord | null>;
|
|
64
|
+
|
|
65
|
+
export function createTrialCredentialRecordIfAbsent(
|
|
66
|
+
value: unknown,
|
|
67
|
+
options?: TrialCredentialStoreOptions,
|
|
68
|
+
): Promise<Readonly<{ record: TrialCredentialRecord; created: boolean }>>;
|
|
69
|
+
|
|
70
|
+
export function completeTrialCredentialRecord(
|
|
71
|
+
value: TrialCredentialRecord,
|
|
72
|
+
metadata: Readonly<{
|
|
73
|
+
accountId: string;
|
|
74
|
+
projectId: string;
|
|
75
|
+
apiKey: string;
|
|
76
|
+
}>,
|
|
77
|
+
options?: TrialCredentialStoreOptions,
|
|
78
|
+
): Promise<TrialCredentialRecord>;
|
|
79
|
+
|
|
80
|
+
export function clearTrialCredentialRecord(
|
|
81
|
+
options?: TrialCredentialStoreOptions,
|
|
82
|
+
): Promise<Readonly<{ deleted: boolean }>>;
|