@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
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
const DEFAULT_SERVICE_BASE_URL = "https://platform.memorax.net";
|
|
2
|
+
const PROVISION_PATH = "/account/api/v1/trial/provision";
|
|
3
|
+
const DEFAULT_TIMEOUT_MS = 15_000;
|
|
4
|
+
const DEFAULT_MAX_RESPONSE_BYTES = 16_384;
|
|
5
|
+
const MAX_RETRY_AFTER_MS = 120_000;
|
|
6
|
+
const MARK_ID_PATTERN = /^mk_[0-9a-f]{64}$/;
|
|
7
|
+
const API_KEY_PATTERN = /^sk_[A-Za-z0-9_-]{43}$/;
|
|
8
|
+
const MACHINE_ID_PATTERN = /^[A-Za-z0-9._:-]{1,128}$/;
|
|
9
|
+
const SHA256_PATTERN = /^[0-9a-f]{64}$/;
|
|
10
|
+
const ERROR_REASONS = new Set([
|
|
11
|
+
"invalid_options",
|
|
12
|
+
"invalid_service_url",
|
|
13
|
+
"tls_unsafe",
|
|
14
|
+
"invalid_request",
|
|
15
|
+
"aborted",
|
|
16
|
+
"timeout",
|
|
17
|
+
"transport",
|
|
18
|
+
"response_too_large",
|
|
19
|
+
"invalid_response",
|
|
20
|
+
"response_contract",
|
|
21
|
+
"rate_limit_exceeded",
|
|
22
|
+
"server_error",
|
|
23
|
+
"server_rejected",
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
export class TrialProvisionClientError extends Error {
|
|
27
|
+
constructor(reason, fields = {}) {
|
|
28
|
+
const safeReason = ERROR_REASONS.has(reason) ? reason : "invalid_response";
|
|
29
|
+
super(`Trial provision request failed (${safeReason})`);
|
|
30
|
+
this.name = "TrialProvisionClientError";
|
|
31
|
+
this.code = "TRIAL_PROVISION_CLIENT_FAILED";
|
|
32
|
+
this.reason = safeReason;
|
|
33
|
+
if (Number.isInteger(fields.httpStatus)) this.httpStatus = fields.httpStatus;
|
|
34
|
+
if (Number.isSafeInteger(fields.retryAfterMs)
|
|
35
|
+
&& fields.retryAfterMs >= 0
|
|
36
|
+
&& fields.retryAfterMs <= MAX_RETRY_AFTER_MS) {
|
|
37
|
+
this.retryAfterMs = fields.retryAfterMs;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function createTrialProvisionClient(options = {}) {
|
|
43
|
+
const serviceBaseUrl = validateServiceBaseUrl(
|
|
44
|
+
options.serviceBaseUrl ?? DEFAULT_SERVICE_BASE_URL,
|
|
45
|
+
);
|
|
46
|
+
const fetchImpl = options.fetchImpl ?? globalThis.fetch;
|
|
47
|
+
if (typeof fetchImpl !== "function") throw clientError("invalid_options");
|
|
48
|
+
const env = isRecord(options.env) ? options.env : process.env;
|
|
49
|
+
if (process.env.NODE_TLS_REJECT_UNAUTHORIZED === "0"
|
|
50
|
+
|| env.NODE_TLS_REJECT_UNAUTHORIZED === "0") {
|
|
51
|
+
throw clientError("tls_unsafe");
|
|
52
|
+
}
|
|
53
|
+
const timeoutMs = positiveInteger(options.timeoutMs ?? DEFAULT_TIMEOUT_MS, 120_000);
|
|
54
|
+
const maxResponseBytes = positiveInteger(
|
|
55
|
+
options.maxResponseBytes ?? DEFAULT_MAX_RESPONSE_BYTES,
|
|
56
|
+
DEFAULT_MAX_RESPONSE_BYTES,
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
return Object.freeze({
|
|
60
|
+
async provision(request, requestOptions = {}) {
|
|
61
|
+
const snapshot = validateRequest(request);
|
|
62
|
+
const body = JSON.stringify({
|
|
63
|
+
mark_id: snapshot.markId,
|
|
64
|
+
mark_version: snapshot.markVersion,
|
|
65
|
+
app_salt: snapshot.appSalt,
|
|
66
|
+
machine_id: snapshot.machineId,
|
|
67
|
+
hostname: snapshot.hostname,
|
|
68
|
+
platform: snapshot.platform,
|
|
69
|
+
arch: snapshot.arch,
|
|
70
|
+
mac_hash: snapshot.macHash,
|
|
71
|
+
});
|
|
72
|
+
const response = await postJson(
|
|
73
|
+
`${serviceBaseUrl}${PROVISION_PATH}`,
|
|
74
|
+
body,
|
|
75
|
+
{
|
|
76
|
+
fetchImpl,
|
|
77
|
+
maxResponseBytes,
|
|
78
|
+
signal: requestOptions.signal,
|
|
79
|
+
timeoutMs,
|
|
80
|
+
},
|
|
81
|
+
);
|
|
82
|
+
return mapProvisionResponse(response, snapshot.markId);
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function postJson(url, body, options) {
|
|
88
|
+
const externalSignal = options.signal;
|
|
89
|
+
if (externalSignal?.aborted) throw clientError("aborted");
|
|
90
|
+
const controller = new AbortController();
|
|
91
|
+
let timedOut = false;
|
|
92
|
+
const onAbort = () => controller.abort();
|
|
93
|
+
externalSignal?.addEventListener?.("abort", onAbort, { once: true });
|
|
94
|
+
const timer = setTimeout(() => {
|
|
95
|
+
timedOut = true;
|
|
96
|
+
controller.abort();
|
|
97
|
+
}, options.timeoutMs);
|
|
98
|
+
timer.unref?.();
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
const response = await options.fetchImpl(url, {
|
|
102
|
+
method: "POST",
|
|
103
|
+
headers: {
|
|
104
|
+
Accept: "application/json",
|
|
105
|
+
"Content-Type": "application/json",
|
|
106
|
+
},
|
|
107
|
+
body,
|
|
108
|
+
cache: "no-store",
|
|
109
|
+
credentials: "omit",
|
|
110
|
+
redirect: "error",
|
|
111
|
+
signal: controller.signal,
|
|
112
|
+
});
|
|
113
|
+
return await readResponse(response, options.maxResponseBytes);
|
|
114
|
+
} catch (error) {
|
|
115
|
+
if (error instanceof TrialProvisionClientError) throw error;
|
|
116
|
+
if (timedOut) throw clientError("timeout");
|
|
117
|
+
if (externalSignal?.aborted) throw clientError("aborted");
|
|
118
|
+
throw clientError("transport");
|
|
119
|
+
} finally {
|
|
120
|
+
clearTimeout(timer);
|
|
121
|
+
externalSignal?.removeEventListener?.("abort", onAbort);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function readResponse(response, maxResponseBytes) {
|
|
126
|
+
const status = response?.status;
|
|
127
|
+
if (!Number.isInteger(status) || status < 100 || status > 599) {
|
|
128
|
+
throw clientError("invalid_response");
|
|
129
|
+
}
|
|
130
|
+
const contentLength = response.headers?.get?.("content-length");
|
|
131
|
+
if (/^[0-9]+$/.test(contentLength ?? "")
|
|
132
|
+
&& Number(contentLength) > maxResponseBytes) {
|
|
133
|
+
throw clientError("response_too_large", { httpStatus: status });
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const text = await readResponseBody(response, maxResponseBytes, status);
|
|
137
|
+
|
|
138
|
+
let body;
|
|
139
|
+
try {
|
|
140
|
+
body = JSON.parse(text);
|
|
141
|
+
} catch {
|
|
142
|
+
if (status !== 200) throw responseError(status, response);
|
|
143
|
+
throw clientError("invalid_response", { httpStatus: status });
|
|
144
|
+
}
|
|
145
|
+
if (status !== 200) {
|
|
146
|
+
throw responseError(status, response, body);
|
|
147
|
+
}
|
|
148
|
+
if (!isRecord(body)) throw clientError("invalid_response", { httpStatus: status });
|
|
149
|
+
if (body.success !== true) throw responseError(status, response, body);
|
|
150
|
+
return body;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function readResponseBody(response, maxResponseBytes, status) {
|
|
154
|
+
if (response.body == null) return "";
|
|
155
|
+
const reader = response.body.getReader?.();
|
|
156
|
+
if (!reader || typeof reader.read !== "function") {
|
|
157
|
+
throw clientError("invalid_response", { httpStatus: status });
|
|
158
|
+
}
|
|
159
|
+
const chunks = [];
|
|
160
|
+
let byteLength = 0;
|
|
161
|
+
try {
|
|
162
|
+
while (true) {
|
|
163
|
+
const { done, value } = await reader.read();
|
|
164
|
+
if (done) break;
|
|
165
|
+
if (!(value instanceof Uint8Array)) {
|
|
166
|
+
throw clientError("invalid_response", { httpStatus: status });
|
|
167
|
+
}
|
|
168
|
+
byteLength += value.byteLength;
|
|
169
|
+
if (byteLength > maxResponseBytes) {
|
|
170
|
+
await reader.cancel().catch(() => undefined);
|
|
171
|
+
throw clientError("response_too_large", { httpStatus: status });
|
|
172
|
+
}
|
|
173
|
+
chunks.push(Buffer.from(value));
|
|
174
|
+
}
|
|
175
|
+
} finally {
|
|
176
|
+
reader.releaseLock?.();
|
|
177
|
+
}
|
|
178
|
+
return Buffer.concat(chunks, byteLength).toString("utf8");
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function responseError(status, response, body) {
|
|
182
|
+
const retryAfterMs = retryAfterDelay(response.headers?.get?.("retry-after"), body);
|
|
183
|
+
if (status === 429) {
|
|
184
|
+
return clientError("rate_limit_exceeded", { httpStatus: status, retryAfterMs });
|
|
185
|
+
}
|
|
186
|
+
if (status >= 500) {
|
|
187
|
+
return clientError("server_error", { httpStatus: status, retryAfterMs });
|
|
188
|
+
}
|
|
189
|
+
return clientError("server_rejected", { httpStatus: status, retryAfterMs });
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function mapProvisionResponse(envelope, expectedMarkId) {
|
|
193
|
+
const data = envelope.data;
|
|
194
|
+
const accountId = decimalPublicId(data?.account_id);
|
|
195
|
+
const projectId = decimalPublicId(data?.project_id);
|
|
196
|
+
const apiKey = typeof data?.api_key === "string" && API_KEY_PATTERN.test(data.api_key)
|
|
197
|
+
? data.api_key
|
|
198
|
+
: undefined;
|
|
199
|
+
const keyPrefix = typeof data?.key_prefix === "string" ? data.key_prefix.trim() : "";
|
|
200
|
+
if (!accountId
|
|
201
|
+
|| !projectId
|
|
202
|
+
|| data?.mark_id !== expectedMarkId
|
|
203
|
+
|| !apiKey
|
|
204
|
+
|| !keyPrefix
|
|
205
|
+
|| !apiKey.startsWith(keyPrefix)
|
|
206
|
+
|| typeof data?.created !== "boolean") {
|
|
207
|
+
throw clientError("response_contract", { httpStatus: 200 });
|
|
208
|
+
}
|
|
209
|
+
return Object.freeze({
|
|
210
|
+
accountId,
|
|
211
|
+
projectId,
|
|
212
|
+
apiKey,
|
|
213
|
+
created: data.created,
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function validateRequest(request) {
|
|
218
|
+
const snapshot = {
|
|
219
|
+
markId: request?.markId,
|
|
220
|
+
markVersion: request?.markVersion,
|
|
221
|
+
appSalt: request?.appSalt,
|
|
222
|
+
machineId: request?.machineId,
|
|
223
|
+
hostname: request?.hostname,
|
|
224
|
+
platform: request?.platform,
|
|
225
|
+
arch: request?.arch,
|
|
226
|
+
macHash: request?.macHash,
|
|
227
|
+
};
|
|
228
|
+
if (!MARK_ID_PATTERN.test(snapshot.markId ?? "")
|
|
229
|
+
|| snapshot.markVersion !== 1
|
|
230
|
+
|| snapshot.appSalt !== "memorax-plugin-v1"
|
|
231
|
+
|| !MACHINE_ID_PATTERN.test(snapshot.machineId ?? "")
|
|
232
|
+
|| !validText(snapshot.hostname, 120)
|
|
233
|
+
|| !["windows", "linux", "macos"].includes(snapshot.platform)
|
|
234
|
+
|| !["x86_64", "arm64"].includes(snapshot.arch)
|
|
235
|
+
|| !SHA256_PATTERN.test(snapshot.macHash ?? "")) {
|
|
236
|
+
throw clientError("invalid_request");
|
|
237
|
+
}
|
|
238
|
+
return snapshot;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function validateServiceBaseUrl(value) {
|
|
242
|
+
try {
|
|
243
|
+
const url = new URL(value);
|
|
244
|
+
if (typeof value !== "string"
|
|
245
|
+
|| value !== value.trim()
|
|
246
|
+
|| url.protocol !== "https:"
|
|
247
|
+
|| !url.hostname
|
|
248
|
+
|| url.username
|
|
249
|
+
|| url.password
|
|
250
|
+
|| url.search
|
|
251
|
+
|| url.hash
|
|
252
|
+
|| !["", "/"].includes(url.pathname)) {
|
|
253
|
+
throw new Error();
|
|
254
|
+
}
|
|
255
|
+
return url.origin;
|
|
256
|
+
} catch {
|
|
257
|
+
throw clientError("invalid_service_url");
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function retryAfterDelay(header, body) {
|
|
262
|
+
const headerText = typeof header === "string" ? header.trim() : "";
|
|
263
|
+
let headerDelay;
|
|
264
|
+
if (/^\d+$/.test(headerText)) {
|
|
265
|
+
headerDelay = Number(headerText) * 1000;
|
|
266
|
+
} else if (headerText) {
|
|
267
|
+
const retryAt = Date.parse(headerText);
|
|
268
|
+
if (Number.isFinite(retryAt)) headerDelay = Math.max(0, retryAt - Date.now());
|
|
269
|
+
}
|
|
270
|
+
if (Number.isSafeInteger(headerDelay) && headerDelay <= MAX_RETRY_AFTER_MS) {
|
|
271
|
+
return headerDelay;
|
|
272
|
+
}
|
|
273
|
+
const bodySeconds = isRecord(body) && isRecord(body.error)
|
|
274
|
+
? body.error.retry_after_seconds
|
|
275
|
+
: undefined;
|
|
276
|
+
const delay = Number.isSafeInteger(bodySeconds) && bodySeconds >= 0
|
|
277
|
+
? bodySeconds * 1000
|
|
278
|
+
: undefined;
|
|
279
|
+
return Number.isSafeInteger(delay) && delay <= MAX_RETRY_AFTER_MS ? delay : undefined;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function validText(value, maximumLength) {
|
|
283
|
+
return typeof value === "string"
|
|
284
|
+
&& value.length > 0
|
|
285
|
+
&& value.length <= maximumLength
|
|
286
|
+
&& value === value.trim()
|
|
287
|
+
&& !value.includes("\0");
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function decimalPublicId(value) {
|
|
291
|
+
return typeof value === "string" && /^\d+$/.test(value) ? value : undefined;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function positiveInteger(value, maximum) {
|
|
295
|
+
if (!Number.isSafeInteger(value) || value <= 0 || value > maximum) {
|
|
296
|
+
throw clientError("invalid_options");
|
|
297
|
+
}
|
|
298
|
+
return value;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function clientError(reason, fields) {
|
|
302
|
+
return new TrialProvisionClientError(reason, fields);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function isRecord(value) {
|
|
306
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
307
|
+
}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import {
|
|
2
|
+
TrialProvisionClientError,
|
|
3
|
+
} from "./trial-provision-client.mjs";
|
|
4
|
+
import { generateTrialPluginIdentity } from "./trial-plugin-mark.mjs";
|
|
5
|
+
|
|
6
|
+
export {
|
|
7
|
+
generateTrialPluginIdentity,
|
|
8
|
+
generateTrialMarkId,
|
|
9
|
+
} from "./trial-plugin-mark.mjs";
|
|
10
|
+
|
|
11
|
+
const RETRY_DELAYS_MS = Object.freeze([1_000, 2_000, 4_000]);
|
|
12
|
+
const ERROR_REASONS = new Set([
|
|
13
|
+
"invalid_options",
|
|
14
|
+
"invalid_credential_state",
|
|
15
|
+
"identity_generation_failed",
|
|
16
|
+
"credential_failure",
|
|
17
|
+
"response_state_mismatch",
|
|
18
|
+
"client_failure",
|
|
19
|
+
"retry_failed",
|
|
20
|
+
]);
|
|
21
|
+
const MARK_ID_PATTERN = /^mk_[0-9a-f]{64}$/;
|
|
22
|
+
const API_KEY_PATTERN = /^sk_[A-Za-z0-9_-]{43}$/;
|
|
23
|
+
|
|
24
|
+
export class TrialProvisionFlowError extends Error {
|
|
25
|
+
constructor(reason) {
|
|
26
|
+
const safeReason = ERROR_REASONS.has(reason) ? reason : "client_failure";
|
|
27
|
+
super(`Trial provision flow failed (${safeReason})`);
|
|
28
|
+
this.name = "TrialProvisionFlowError";
|
|
29
|
+
this.code = "TRIAL_PROVISION_FLOW_FAILED";
|
|
30
|
+
this.reason = safeReason;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function ensureTrialCredentialReady(options = {}) {
|
|
35
|
+
const context = validateOptions(options);
|
|
36
|
+
try {
|
|
37
|
+
return await context.credentialPort.withProvisionLock(
|
|
38
|
+
() => runTrialCredentialFlow(context),
|
|
39
|
+
{ timeoutMs: 30_000 },
|
|
40
|
+
);
|
|
41
|
+
} catch (error) {
|
|
42
|
+
if (error instanceof TrialProvisionFlowError
|
|
43
|
+
|| error instanceof TrialProvisionClientError) {
|
|
44
|
+
throw error;
|
|
45
|
+
}
|
|
46
|
+
throw flowError("credential_failure");
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function runTrialCredentialFlow(context) {
|
|
51
|
+
let record = await context.credentialPort.load();
|
|
52
|
+
if (record === null) record = await createCredential(context);
|
|
53
|
+
assertCredentialRecord(record);
|
|
54
|
+
if (record.state === "ready") return readyResult(record, false);
|
|
55
|
+
|
|
56
|
+
const response = await provisionWithRetry(record, context);
|
|
57
|
+
validateProvisionResponse(response);
|
|
58
|
+
const ready = await context.credentialPort.complete(record, {
|
|
59
|
+
accountId: response.accountId,
|
|
60
|
+
projectId: response.projectId,
|
|
61
|
+
apiKey: response.apiKey,
|
|
62
|
+
});
|
|
63
|
+
return readyResult(ready, true);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function createCredential(context) {
|
|
67
|
+
let seed;
|
|
68
|
+
try {
|
|
69
|
+
const identity = context.generatePluginIdentity();
|
|
70
|
+
seed = context.recordPort.createInitial({
|
|
71
|
+
markId: identity.markId,
|
|
72
|
+
markVersion: identity.markVersion,
|
|
73
|
+
appSalt: identity.appSalt,
|
|
74
|
+
machineId: identity.machineId,
|
|
75
|
+
hostname: identity.hostname,
|
|
76
|
+
platform: identity.platform,
|
|
77
|
+
arch: identity.arch,
|
|
78
|
+
macHash: identity.macHash,
|
|
79
|
+
});
|
|
80
|
+
} catch {
|
|
81
|
+
throw flowError("identity_generation_failed");
|
|
82
|
+
}
|
|
83
|
+
const result = await context.credentialPort.createIfAbsent(seed);
|
|
84
|
+
if (!isRecord(result) || !Object.hasOwn(result, "record")) {
|
|
85
|
+
throw flowError("credential_failure");
|
|
86
|
+
}
|
|
87
|
+
return result.record;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function provisionWithRetry(record, context) {
|
|
91
|
+
const request = Object.freeze({
|
|
92
|
+
markId: record.mark_id,
|
|
93
|
+
markVersion: record.mark_version,
|
|
94
|
+
appSalt: record.app_salt,
|
|
95
|
+
machineId: record.machine_id,
|
|
96
|
+
hostname: record.hostname,
|
|
97
|
+
platform: record.platform,
|
|
98
|
+
arch: record.arch,
|
|
99
|
+
macHash: record.mac_hash,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
for (let attempt = 0; ; attempt += 1) {
|
|
103
|
+
try {
|
|
104
|
+
return await context.client.provision(request);
|
|
105
|
+
} catch (error) {
|
|
106
|
+
if (!(error instanceof TrialProvisionClientError)) {
|
|
107
|
+
throw flowError("client_failure");
|
|
108
|
+
}
|
|
109
|
+
const delay = retryDelay(error, attempt);
|
|
110
|
+
if (delay === undefined) throw error;
|
|
111
|
+
try {
|
|
112
|
+
await context.sleep(delay);
|
|
113
|
+
} catch {
|
|
114
|
+
throw flowError("retry_failed");
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function retryDelay(error, attempt) {
|
|
121
|
+
if (attempt >= RETRY_DELAYS_MS.length) return undefined;
|
|
122
|
+
if (error.reason === "rate_limit_exceeded") return error.retryAfterMs;
|
|
123
|
+
if (["transport", "timeout", "server_error"].includes(error.reason)) {
|
|
124
|
+
return RETRY_DELAYS_MS[attempt];
|
|
125
|
+
}
|
|
126
|
+
if (["invalid_response", "response_contract"].includes(error.reason)
|
|
127
|
+
&& error.httpStatus === 200
|
|
128
|
+
&& attempt === 0) {
|
|
129
|
+
return 0;
|
|
130
|
+
}
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function validateProvisionResponse(response) {
|
|
135
|
+
if (!isRecord(response)
|
|
136
|
+
|| typeof response.accountId !== "string"
|
|
137
|
+
|| typeof response.projectId !== "string"
|
|
138
|
+
|| typeof response.apiKey !== "string"
|
|
139
|
+
|| !API_KEY_PATTERN.test(response.apiKey)
|
|
140
|
+
|| typeof response.created !== "boolean") {
|
|
141
|
+
throw flowError("response_state_mismatch");
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function assertCredentialRecord(record) {
|
|
146
|
+
if (!isRecord(record)
|
|
147
|
+
|| !["provisioning", "ready"].includes(record.state)
|
|
148
|
+
|| typeof record.mark_id !== "string"
|
|
149
|
+
|| !MARK_ID_PATTERN.test(record.mark_id)
|
|
150
|
+
|| (record.state === "provisioning" && record.api_key !== null)
|
|
151
|
+
|| (record.state === "ready"
|
|
152
|
+
&& (typeof record.api_key !== "string"
|
|
153
|
+
|| !API_KEY_PATTERN.test(record.api_key)
|
|
154
|
+
|| typeof record.account_id !== "string"
|
|
155
|
+
|| typeof record.project_id !== "string"))) {
|
|
156
|
+
throw flowError("invalid_credential_state");
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function readyResult(record, provisioned) {
|
|
161
|
+
assertCredentialRecord(record);
|
|
162
|
+
if (record.state !== "ready") throw flowError("credential_failure");
|
|
163
|
+
return Object.freeze({
|
|
164
|
+
status: "ready",
|
|
165
|
+
provisioned,
|
|
166
|
+
markId: record.mark_id,
|
|
167
|
+
accountId: record.account_id,
|
|
168
|
+
projectId: record.project_id,
|
|
169
|
+
apiKey: record.api_key,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function validateOptions(options) {
|
|
174
|
+
const credentialPort = options.credentialPort;
|
|
175
|
+
const recordPort = options.recordPort;
|
|
176
|
+
const client = options.client;
|
|
177
|
+
if (!hasFunctions(credentialPort, [
|
|
178
|
+
"load",
|
|
179
|
+
"createIfAbsent",
|
|
180
|
+
"complete",
|
|
181
|
+
"withProvisionLock",
|
|
182
|
+
])
|
|
183
|
+
|| !hasFunctions(recordPort, ["createInitial"])
|
|
184
|
+
|| !hasFunctions(client, ["provision"])) {
|
|
185
|
+
throw flowError("invalid_options");
|
|
186
|
+
}
|
|
187
|
+
const generatePluginIdentity = options.generatePluginIdentity ?? generateTrialPluginIdentity;
|
|
188
|
+
const sleep = options.sleep ?? defaultSleep;
|
|
189
|
+
if (typeof generatePluginIdentity !== "function" || typeof sleep !== "function") {
|
|
190
|
+
throw flowError("invalid_options");
|
|
191
|
+
}
|
|
192
|
+
return {
|
|
193
|
+
credentialPort,
|
|
194
|
+
recordPort,
|
|
195
|
+
client,
|
|
196
|
+
generatePluginIdentity,
|
|
197
|
+
sleep,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function defaultSleep(delayMs) {
|
|
202
|
+
return new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function hasFunctions(value, names) {
|
|
206
|
+
return isRecord(value) && names.every((name) => typeof value[name] === "function");
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function flowError(reason) {
|
|
210
|
+
return new TrialProvisionFlowError(reason);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function isRecord(value) {
|
|
214
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
215
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { createTrialProvisionClient } from "./trial-provision-client.mjs";
|
|
2
|
+
import { ensureTrialCredentialReady } from "./trial-provision-flow.mjs";
|
|
3
|
+
|
|
4
|
+
export async function ensureTrialSetupCredential(options = {}) {
|
|
5
|
+
const env = options.env ?? process.env;
|
|
6
|
+
const credentialApis = options.credentialApis ?? await loadCredentialApis();
|
|
7
|
+
const credentialPort = trialCredentialPort(options, credentialApis, env);
|
|
8
|
+
const recordPort = options.recordPort ?? {
|
|
9
|
+
createInitial: credentialApis.createInitialTrialCredentialRecord,
|
|
10
|
+
};
|
|
11
|
+
const client = options.client ?? createTrialProvisionClient({
|
|
12
|
+
...options.provisionClientOptions,
|
|
13
|
+
env,
|
|
14
|
+
});
|
|
15
|
+
return await ensureTrialCredentialReady({
|
|
16
|
+
...options.flowOptions,
|
|
17
|
+
credentialPort,
|
|
18
|
+
recordPort,
|
|
19
|
+
client,
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function loadReadyTrialSetupCredential(options = {}) {
|
|
24
|
+
const env = options.env ?? process.env;
|
|
25
|
+
const credentialApis = options.credentialApis ?? await loadCredentialApis();
|
|
26
|
+
const record = await trialCredentialPort(options, credentialApis, env).load();
|
|
27
|
+
if (record?.state !== "ready") return undefined;
|
|
28
|
+
return Object.freeze({
|
|
29
|
+
status: "ready",
|
|
30
|
+
provisioned: false,
|
|
31
|
+
markId: record.mark_id,
|
|
32
|
+
accountId: record.account_id,
|
|
33
|
+
projectId: record.project_id,
|
|
34
|
+
apiKey: record.api_key,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function trialCredentialPort(options, credentialApis, env) {
|
|
39
|
+
return options.credentialPort
|
|
40
|
+
?? credentialApis.createTrialCredentialStorePort({
|
|
41
|
+
...options.credentialStoreOptions,
|
|
42
|
+
memoraxCodeHome: options.memoraxCodeHome,
|
|
43
|
+
env,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function loadCredentialApis() {
|
|
48
|
+
const [recordApi, storeApi] = await Promise.all([
|
|
49
|
+
import("./memorax-code-adapter-common/src/credentials/trial-credential-record.mjs"),
|
|
50
|
+
import("./memorax-code-adapter-common/src/credentials/trial-credential-store.mjs"),
|
|
51
|
+
]);
|
|
52
|
+
return { ...recordApi, ...storeApi };
|
|
53
|
+
}
|