@remnic/plugin-openclaw 1.0.10 → 1.0.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/dist/{calibration-674TDQNV.js → calibration-WCHOK6DX.js} +12 -4
  2. package/dist/capsule-cli-GBM3WPAM.js +33 -0
  3. package/dist/capsule-crypto-K3IRTKRH.js +17 -0
  4. package/dist/capsule-export-IXVERCQG.js +17 -0
  5. package/dist/capsule-import-IA6VIOPQ.js +16 -0
  6. package/dist/capsule-merge-IWOQ34KL.js +189 -0
  7. package/dist/{causal-chain-OKDZSDEB.js → causal-chain-WYN5QOPS.js} +3 -2
  8. package/dist/{causal-consolidation-5BEXLQV5.js → causal-consolidation-YI53C2AO.js} +16 -12
  9. package/dist/{causal-retrieval-3BKBXVXD.js → causal-retrieval-NZHQOZOE.js} +6 -5
  10. package/dist/{causal-trajectory-graph-RQIT37DN.js → causal-trajectory-graph-VBPE2WPM.js} +1 -1
  11. package/dist/chunk-37NKFWSO.js +233 -0
  12. package/dist/chunk-3G7FAF6S.js +60 -0
  13. package/dist/{chunk-Z7GRLVK3.js → chunk-3GUF7RQI.js} +235 -19
  14. package/dist/chunk-4G2XCSD2.js +186 -0
  15. package/dist/chunk-4LYQ4ONL.js +185 -0
  16. package/dist/chunk-6F6EKSVP.js +453 -0
  17. package/dist/chunk-6IWEAUN6.js +148 -0
  18. package/dist/{chunk-LN5UZQVG.js → chunk-6UFI73TJ.js} +5 -3
  19. package/dist/chunk-7OQEPGQF.js +529 -0
  20. package/dist/{chunk-JJSNPSCD.js → chunk-7UZNLMW5.js} +652 -174
  21. package/dist/chunk-B52XADV3.js +244 -0
  22. package/dist/chunk-BU5KJVWF.js +78 -0
  23. package/dist/chunk-CDAZGIGT.js +720 -0
  24. package/dist/chunk-CXM7EBAO.js +289 -0
  25. package/dist/{chunk-HCFFXBLV.js → chunk-EXDYWXMB.js} +6 -1861
  26. package/dist/chunk-FGTYFLL5.js +274 -0
  27. package/dist/chunk-FQRSVYY4.js +110 -0
  28. package/dist/chunk-HRGFO6AW.js +349 -0
  29. package/dist/chunk-I6B2W2IY.js +47 -0
  30. package/dist/chunk-JZBOXOUC.js +259 -0
  31. package/dist/chunk-L6I4MQKO.js +227 -0
  32. package/dist/chunk-LLUROTZJ.js +328 -0
  33. package/dist/chunk-MBIFE6SA.js +250 -0
  34. package/dist/chunk-NKVIN6RD.js +118 -0
  35. package/dist/chunk-OAE7AQ6R.js +1832 -0
  36. package/dist/chunk-OEI7GLV2.js +17 -0
  37. package/dist/chunk-RKR6PTPA.js +308 -0
  38. package/dist/{chunk-7TENHBV2.js → chunk-RQCTMECT.js} +10 -48
  39. package/dist/chunk-SSFTU6LP.js +182 -0
  40. package/dist/{chunk-BXTMZDRT.js → chunk-SVSQAG6M.js} +7 -5
  41. package/dist/{chunk-S2ISS4AH.js → chunk-TILAJIJR.js} +10 -10
  42. package/dist/chunk-TLVIQLB4.js +874 -0
  43. package/dist/{chunk-YHH3SXKD.js → chunk-WPINX4MF.js} +1 -59
  44. package/dist/chunk-YGGGUTG3.js +125 -0
  45. package/dist/cipher-VHAFCG7Z.js +27 -0
  46. package/dist/dreams-ledger-3I52ISYR.js +285 -0
  47. package/dist/{engine-65C2J63X.js → engine-BIYI3P4J.js} +7 -2
  48. package/dist/{fallback-llm-LVK5PDIM.js → fallback-llm-WCWNGIQ3.js} +2 -1
  49. package/dist/first-start-migration-I24M2JEE.js +258 -0
  50. package/dist/forget-NI4RBDPB.js +68 -0
  51. package/dist/fs-utils-PZRI2HDZ.js +29 -0
  52. package/dist/graph-edge-decay-5CVKWBYH.js +203 -0
  53. package/dist/index.js +10654 -3067
  54. package/dist/kdf-H5B23ZM2.js +25 -0
  55. package/dist/memory-governance-SJ5DGRB3.js +25 -0
  56. package/dist/metadata-JAGIWHEA.js +20 -0
  57. package/dist/migrate-from-identity-anchor-N3354WMP.js +7 -0
  58. package/dist/path-5LCUBAAZ.js +8 -0
  59. package/dist/peers-JF2I6RCR.js +43 -0
  60. package/dist/purge-XN2VSPZ2.js +204 -0
  61. package/dist/secure-store-A4NGCNXV.js +155 -0
  62. package/dist/state-PVISYXRH.js +7 -0
  63. package/dist/state-store-LP5BO6SF.js +15 -0
  64. package/dist/{storage-DM4ZGOCN.js → storage-PTQ2H2YJ.js} +3 -1
  65. package/dist/tier-stats-IZNW66NC.js +147 -0
  66. package/dist/trace-NJESSGH7.js +289 -0
  67. package/dist/tui-MGK2LYJY.js +12 -0
  68. package/dist/types-R4DO7AKM.js +30 -0
  69. package/openclaw.plugin.json +519 -4
  70. package/package.json +2 -2
@@ -0,0 +1,349 @@
1
+ import {
2
+ PEER_ID_PATTERN,
3
+ appendInteractionLog,
4
+ listPeers,
5
+ readPeerInteractionLog,
6
+ readPeerProfile,
7
+ writePeerProfile
8
+ } from "./chunk-TLVIQLB4.js";
9
+
10
+ // ../remnic-core/src/peers/profile-reasoner.ts
11
+ function buildPeerProfileReasonerPrompt(input) {
12
+ const existingFields = input.existingProfile ? Object.keys(input.existingProfile.fields) : [];
13
+ const logBlock = input.log.map((e) => {
14
+ const session = e.sessionId ? ` session=${e.sessionId}` : "";
15
+ return `- [${e.timestamp}] (${e.kind})${session} ${e.summary}`;
16
+ }).join("\n");
17
+ return [
18
+ `You are an async peer-profile reasoner. Your job is to read recent interaction-log entries for one peer and propose 0..${input.maxFields} profile-field updates.`,
19
+ "",
20
+ `Peer:`,
21
+ ` id: ${input.peer.id}`,
22
+ ` kind: ${input.peer.kind}`,
23
+ ` displayName: ${input.peer.displayName}`,
24
+ "",
25
+ `Existing profile field keys (preserve names when proposing updates that refine an existing field): ${existingFields.length > 0 ? existingFields.join(", ") : "(none yet)"}`,
26
+ "",
27
+ `Recent interaction log (oldest first):`,
28
+ logBlock.length > 0 ? logBlock : "(no entries)",
29
+ "",
30
+ `Output a single JSON object: {"proposals": [{"field": "<stable_key>", "value": "<markdown>", "signal": "<short_label>", "note": "<optional>", "sourceSessionId": "<optional>"}]}.`,
31
+ "",
32
+ `Rules:`,
33
+ `1. Only propose fields supported by evidence in the log. Do not invent.`,
34
+ `2. Keys are short snake_case (e.g. "communication_style", "tool_patterns").`,
35
+ `3. value is markdown. signal is a short label like "explicit_preference" or "topic_recurrence".`,
36
+ `4. Omit fields you can't justify. Empty proposals array is valid.`,
37
+ `5. Output JSON ONLY \u2014 no prose before or after.`
38
+ ].join("\n");
39
+ }
40
+ function parsePeerProfileReasonerResponse(raw) {
41
+ if (typeof raw !== "string" || raw.trim() === "") return [];
42
+ const trimmed = raw.trim();
43
+ const fenced = /^```(?:json)?\s*([\s\S]*?)```\s*$/u.exec(trimmed);
44
+ const payload = fenced ? fenced[1].trim() : trimmed;
45
+ let parsed;
46
+ try {
47
+ parsed = JSON.parse(payload);
48
+ } catch {
49
+ return [];
50
+ }
51
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
52
+ return [];
53
+ }
54
+ const obj = parsed;
55
+ if (!Array.isArray(obj.proposals)) return [];
56
+ const out = [];
57
+ const RESERVED_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
58
+ for (const item of obj.proposals) {
59
+ if (typeof item !== "object" || item === null || Array.isArray(item)) continue;
60
+ const r = item;
61
+ if (typeof r.field !== "string" || r.field.trim() === "") continue;
62
+ if (RESERVED_KEYS.has(r.field)) continue;
63
+ if (typeof r.value !== "string" || r.value.trim() === "") continue;
64
+ if (typeof r.signal !== "string" || r.signal.trim() === "") continue;
65
+ const proposal = {
66
+ field: r.field,
67
+ value: r.value,
68
+ signal: r.signal,
69
+ ...typeof r.note === "string" && r.note.length > 0 ? { note: r.note } : {},
70
+ ...typeof r.sourceSessionId === "string" && r.sourceSessionId.length > 0 ? { sourceSessionId: r.sourceSessionId } : {}
71
+ };
72
+ out.push(proposal);
73
+ }
74
+ return out;
75
+ }
76
+ var SYSTEM_MESSAGE = 'You are a peer-profile reasoner. Output ONLY a JSON object of the form {"proposals":[{"field":"...","value":"...","signal":"...","note":"...","sourceSessionId":"..."}]}. No prose, no fenced code block, no commentary.';
77
+ var RUN_MARKER_KIND = "peer_profile_reasoner_run";
78
+ function lastRunTimestamp(log) {
79
+ let latest;
80
+ for (const entry of log) {
81
+ if (entry.kind !== RUN_MARKER_KIND) continue;
82
+ if (latest === void 0 || entry.timestamp > latest) {
83
+ latest = entry.timestamp;
84
+ }
85
+ }
86
+ return latest;
87
+ }
88
+ function noopLogger() {
89
+ return { debug: () => {
90
+ }, info: () => {
91
+ }, warn: () => {
92
+ } };
93
+ }
94
+ async function runPeerProfileReasoner(options) {
95
+ const log = {
96
+ debug: options.log?.debug ?? noopLogger().debug,
97
+ info: options.log?.info ?? noopLogger().info,
98
+ warn: options.log?.warn ?? noopLogger().warn
99
+ };
100
+ const result = {
101
+ peersConsidered: 0,
102
+ peersProcessed: 0,
103
+ fieldsApplied: 0,
104
+ perPeer: []
105
+ };
106
+ if (options.enabled !== true) {
107
+ log.debug("[peer-profile-reasoner] disabled \u2014 no-op");
108
+ return result;
109
+ }
110
+ if (!options.llm) {
111
+ log.warn("[peer-profile-reasoner] no LLM client supplied \u2014 skipping run");
112
+ return result;
113
+ }
114
+ const minInteractions = Number.isFinite(options.minInteractions) ? Math.max(0, Math.floor(options.minInteractions)) : 0;
115
+ const maxFields = Number.isFinite(options.maxFieldsPerRun) ? Math.max(0, Math.floor(options.maxFieldsPerRun)) : 0;
116
+ if (maxFields === 0) {
117
+ log.debug("[peer-profile-reasoner] maxFieldsPerRun=0 \u2014 no-op");
118
+ return result;
119
+ }
120
+ const maxLogPerPeer = Number.isFinite(options.maxLogEntriesPerPeer ?? NaN) ? Math.max(1, Math.floor(options.maxLogEntriesPerPeer)) : 50;
121
+ const now = options.now ?? /* @__PURE__ */ new Date();
122
+ const nowIso = now.toISOString();
123
+ let peers;
124
+ try {
125
+ if (options.peerIds && options.peerIds.length > 0) {
126
+ const all = await listPeers(options.memoryDir);
127
+ const wanted = new Set(
128
+ options.peerIds.filter(
129
+ (id) => typeof id === "string" && PEER_ID_PATTERN.test(id)
130
+ )
131
+ );
132
+ peers = all.filter((p) => wanted.has(p.id));
133
+ } else {
134
+ peers = await listPeers(options.memoryDir);
135
+ }
136
+ } catch (err) {
137
+ log.warn(
138
+ `[peer-profile-reasoner] listPeers failed: ${err instanceof Error ? err.message : String(err)}`
139
+ );
140
+ return result;
141
+ }
142
+ result.peersConsidered = peers.length;
143
+ let fieldsAppliedTotal = 0;
144
+ for (const peer of peers) {
145
+ if (options.signal?.aborted) {
146
+ result.perPeer.push({
147
+ peerId: peer.id,
148
+ status: "skipped_aborted",
149
+ fieldsApplied: 0,
150
+ droppedDueToCap: 0,
151
+ fields: []
152
+ });
153
+ continue;
154
+ }
155
+ try {
156
+ const fullLog = await readPeerInteractionLog(
157
+ options.memoryDir,
158
+ peer.id
159
+ );
160
+ if (fullLog.length === 0) {
161
+ result.perPeer.push({
162
+ peerId: peer.id,
163
+ status: "skipped_no_log",
164
+ fieldsApplied: 0,
165
+ droppedDueToCap: 0,
166
+ fields: []
167
+ });
168
+ continue;
169
+ }
170
+ const lastRun = lastRunTimestamp(fullLog);
171
+ const sinceLastRunFull = lastRun ? fullLog.filter(
172
+ (e) => e.timestamp > lastRun && e.kind !== RUN_MARKER_KIND
173
+ ) : fullLog.filter((e) => e.kind !== RUN_MARKER_KIND);
174
+ if (sinceLastRunFull.length < minInteractions) {
175
+ result.perPeer.push({
176
+ peerId: peer.id,
177
+ status: "skipped_below_min_interactions",
178
+ fieldsApplied: 0,
179
+ droppedDueToCap: 0,
180
+ fields: []
181
+ });
182
+ continue;
183
+ }
184
+ const sinceLastRun = sinceLastRunFull.length > maxLogPerPeer ? sinceLastRunFull.slice(sinceLastRunFull.length - maxLogPerPeer) : sinceLastRunFull;
185
+ const existingProfile = await readPeerProfile(options.memoryDir, peer.id);
186
+ const remainingBudget = maxFields - fieldsAppliedTotal;
187
+ if (remainingBudget <= 0) {
188
+ result.perPeer.push({
189
+ peerId: peer.id,
190
+ status: "skipped_cap_reached",
191
+ fieldsApplied: 0,
192
+ droppedDueToCap: 0,
193
+ fields: []
194
+ });
195
+ continue;
196
+ }
197
+ const prompt = buildPeerProfileReasonerPrompt({
198
+ peer,
199
+ existingProfile,
200
+ log: sinceLastRun,
201
+ maxFields: remainingBudget
202
+ });
203
+ const messages = [
204
+ { role: "system", content: SYSTEM_MESSAGE },
205
+ { role: "user", content: prompt }
206
+ ];
207
+ let response;
208
+ try {
209
+ response = await options.llm.chatCompletion(messages, {
210
+ temperature: 0.2,
211
+ maxTokens: 1500
212
+ });
213
+ } catch (err) {
214
+ log.warn(
215
+ `[peer-profile-reasoner] LLM call failed for "${peer.id}": ${err instanceof Error ? err.message : String(err)}`
216
+ );
217
+ result.perPeer.push({
218
+ peerId: peer.id,
219
+ status: "skipped_llm_unavailable",
220
+ fieldsApplied: 0,
221
+ droppedDueToCap: 0,
222
+ fields: []
223
+ });
224
+ continue;
225
+ }
226
+ if (!response || typeof response.content !== "string") {
227
+ result.perPeer.push({
228
+ peerId: peer.id,
229
+ status: "skipped_llm_unavailable",
230
+ fieldsApplied: 0,
231
+ droppedDueToCap: 0,
232
+ fields: []
233
+ });
234
+ continue;
235
+ }
236
+ const proposals = parsePeerProfileReasonerResponse(response.content);
237
+ if (proposals.length === 0) {
238
+ result.perPeer.push({
239
+ peerId: peer.id,
240
+ status: "processed",
241
+ fieldsApplied: 0,
242
+ droppedDueToCap: 0,
243
+ fields: []
244
+ });
245
+ continue;
246
+ }
247
+ const sessionIdsInWindow = new Set(
248
+ sinceLastRun.map((e) => e.sessionId).filter((s) => typeof s === "string" && s.length > 0)
249
+ );
250
+ const baseFields = existingProfile ? { ...existingProfile.fields } : {};
251
+ const baseProvenance = {};
252
+ if (existingProfile) {
253
+ for (const [k, list] of Object.entries(existingProfile.provenance)) {
254
+ baseProvenance[k] = [...list];
255
+ }
256
+ }
257
+ const appliedFieldsForPeer = [];
258
+ let droppedDueToCap = 0;
259
+ let invalidProposalSeen = false;
260
+ for (const proposal of proposals) {
261
+ const candidateBudget = maxFields - fieldsAppliedTotal - appliedFieldsForPeer.length;
262
+ if (candidateBudget <= 0) {
263
+ droppedDueToCap += 1;
264
+ continue;
265
+ }
266
+ if (proposal.field === "__proto__" || proposal.field === "constructor" || proposal.field === "prototype") {
267
+ invalidProposalSeen = true;
268
+ continue;
269
+ }
270
+ if (!/^[a-zA-Z][a-zA-Z0-9_]{0,63}$/.test(proposal.field)) {
271
+ invalidProposalSeen = true;
272
+ continue;
273
+ }
274
+ baseFields[proposal.field] = proposal.value;
275
+ const sourceSessionId = proposal.sourceSessionId && sessionIdsInWindow.has(proposal.sourceSessionId) ? proposal.sourceSessionId : void 0;
276
+ const provEntry = {
277
+ observedAt: nowIso,
278
+ signal: proposal.signal,
279
+ ...sourceSessionId ? { sourceSessionId } : {},
280
+ ...proposal.note && proposal.note.length > 0 ? { note: proposal.note } : {}
281
+ };
282
+ const list = baseProvenance[proposal.field] ?? [];
283
+ list.push(provEntry);
284
+ baseProvenance[proposal.field] = list;
285
+ appliedFieldsForPeer.push(proposal.field);
286
+ }
287
+ if (appliedFieldsForPeer.length === 0) {
288
+ result.perPeer.push({
289
+ peerId: peer.id,
290
+ status: invalidProposalSeen ? "skipped_invalid_proposal" : droppedDueToCap > 0 ? "skipped_cap_reached" : "processed",
291
+ fieldsApplied: 0,
292
+ droppedDueToCap,
293
+ fields: []
294
+ });
295
+ continue;
296
+ }
297
+ const merged = {
298
+ peerId: peer.id,
299
+ updatedAt: nowIso,
300
+ fields: baseFields,
301
+ provenance: baseProvenance
302
+ };
303
+ await writePeerProfile(options.memoryDir, merged);
304
+ fieldsAppliedTotal += appliedFieldsForPeer.length;
305
+ const wantsMarker = options.appendRunMarkerToLog ?? true;
306
+ if (wantsMarker) {
307
+ try {
308
+ await appendInteractionLog(options.memoryDir, peer.id, {
309
+ timestamp: nowIso,
310
+ kind: RUN_MARKER_KIND,
311
+ summary: `applied ${appliedFieldsForPeer.length} field(s) via ${options.model ?? "unknown-model"}`
312
+ });
313
+ } catch (err) {
314
+ log.warn(
315
+ `[peer-profile-reasoner] run-marker append failed for "${peer.id}": ${err instanceof Error ? err.message : String(err)}`
316
+ );
317
+ }
318
+ }
319
+ result.perPeer.push({
320
+ peerId: peer.id,
321
+ status: "processed",
322
+ fieldsApplied: appliedFieldsForPeer.length,
323
+ droppedDueToCap,
324
+ fields: appliedFieldsForPeer
325
+ });
326
+ result.peersProcessed += 1;
327
+ result.fieldsApplied = fieldsAppliedTotal;
328
+ } catch (err) {
329
+ log.warn(
330
+ `[peer-profile-reasoner] error processing peer "${peer.id}": ${err instanceof Error ? err.message : String(err)}`
331
+ );
332
+ result.perPeer.push({
333
+ peerId: peer.id,
334
+ status: "error",
335
+ fieldsApplied: 0,
336
+ droppedDueToCap: 0,
337
+ fields: [],
338
+ error: err instanceof Error ? err.message : String(err)
339
+ });
340
+ }
341
+ }
342
+ return result;
343
+ }
344
+
345
+ export {
346
+ buildPeerProfileReasonerPrompt,
347
+ parsePeerProfileReasonerResponse,
348
+ runPeerProfileReasoner
349
+ };
@@ -0,0 +1,47 @@
1
+ // ../remnic-core/src/runtime/env.ts
2
+ import os from "os";
3
+ var REMNIC_ENGRAM_PREFIX_PAIRS = [
4
+ ["REMNIC_", "ENGRAM_"],
5
+ ["ENGRAM_", "REMNIC_"]
6
+ ];
7
+ function getEnvMap() {
8
+ const runtimeProcess = globalThis.process;
9
+ return runtimeProcess?.["env"];
10
+ }
11
+ function legacyEnvCandidates(name) {
12
+ const candidates = [name];
13
+ for (const [primary, legacy] of REMNIC_ENGRAM_PREFIX_PAIRS) {
14
+ if (name.startsWith(primary)) {
15
+ candidates.push(`${legacy}${name.slice(primary.length)}`);
16
+ }
17
+ }
18
+ return candidates;
19
+ }
20
+ function readEnvVar(name) {
21
+ const env = getEnvMap();
22
+ for (const candidate of legacyEnvCandidates(name)) {
23
+ const value = env?.[candidate];
24
+ if (typeof value === "string") return value;
25
+ }
26
+ return void 0;
27
+ }
28
+ function resolveHomeDir() {
29
+ return readEnvVar("HOME") || os.homedir();
30
+ }
31
+ function cloneEnv() {
32
+ return { ...getEnvMap() ?? {} };
33
+ }
34
+ function mergeEnv(overrides) {
35
+ const merged = cloneEnv();
36
+ for (const [key, value] of Object.entries(overrides)) {
37
+ if (typeof value === "string") merged[key] = value;
38
+ else delete merged[key];
39
+ }
40
+ return merged;
41
+ }
42
+
43
+ export {
44
+ readEnvVar,
45
+ resolveHomeDir,
46
+ mergeEnv
47
+ };
@@ -0,0 +1,259 @@
1
+ import {
2
+ DEFAULT_ARGON2ID_PARAMS,
3
+ DEFAULT_SCRYPT_PARAMS,
4
+ KDF_SALT_LENGTH
5
+ } from "./chunk-6IWEAUN6.js";
6
+
7
+ // ../remnic-core/src/secure-store/metadata.ts
8
+ var METADATA_FORMAT = "remnic.secure-store.metadata";
9
+ var METADATA_FORMAT_VERSION = 1;
10
+ function bytesToHex(bytes) {
11
+ return Buffer.from(bytes).toString("hex");
12
+ }
13
+ function hexToBytes(hex, expectedLength) {
14
+ if (typeof hex !== "string") {
15
+ throw new Error("hex field must be a string");
16
+ }
17
+ if (!/^[0-9a-f]*$/i.test(hex)) {
18
+ throw new Error("hex field must contain only hexadecimal characters");
19
+ }
20
+ if (hex.length % 2 !== 0) {
21
+ throw new Error("hex field must have even length");
22
+ }
23
+ const buf = Buffer.from(hex, "hex");
24
+ if (buf.length !== expectedLength) {
25
+ throw new Error(
26
+ `hex field decoded to ${buf.length} bytes, expected ${expectedLength}`
27
+ );
28
+ }
29
+ return buf;
30
+ }
31
+ function buildMetadata(options) {
32
+ const { algorithm, salt } = options;
33
+ if (!(salt instanceof Uint8Array) || salt.length !== KDF_SALT_LENGTH) {
34
+ throw new Error(
35
+ `salt must be ${KDF_SALT_LENGTH} bytes, got ${salt?.length ?? "non-buffer"}`
36
+ );
37
+ }
38
+ const createdAt = options.createdAt ?? (/* @__PURE__ */ new Date()).toISOString();
39
+ let kdf;
40
+ if (algorithm === "scrypt") {
41
+ const params = options.params ?? {
42
+ ...DEFAULT_SCRYPT_PARAMS
43
+ };
44
+ kdf = { algorithm: "scrypt", params, salt: bytesToHex(salt) };
45
+ } else if (algorithm === "argon2id") {
46
+ const params = options.params ?? {
47
+ ...DEFAULT_ARGON2ID_PARAMS
48
+ };
49
+ kdf = { algorithm: "argon2id", params, salt: bytesToHex(salt) };
50
+ } else {
51
+ throw new Error(`unknown KDF algorithm: ${algorithm}`);
52
+ }
53
+ const meta = {
54
+ format: METADATA_FORMAT,
55
+ formatVersion: METADATA_FORMAT_VERSION,
56
+ kdf,
57
+ createdAt
58
+ };
59
+ if (options.note !== void 0) {
60
+ meta.note = options.note;
61
+ }
62
+ return meta;
63
+ }
64
+ function serializeMetadata(meta) {
65
+ validateMetadata(meta);
66
+ const ordered = {
67
+ format: meta.format,
68
+ formatVersion: meta.formatVersion,
69
+ kdf: orderKdf(meta.kdf),
70
+ createdAt: meta.createdAt
71
+ };
72
+ if (meta.note !== void 0) {
73
+ ordered.note = meta.note;
74
+ }
75
+ return JSON.stringify(ordered, null, 2);
76
+ }
77
+ function orderKdf(kdf) {
78
+ if (kdf.algorithm === "scrypt") {
79
+ return {
80
+ algorithm: kdf.algorithm,
81
+ params: {
82
+ N: kdf.params.N,
83
+ r: kdf.params.r,
84
+ p: kdf.params.p,
85
+ keyLength: kdf.params.keyLength,
86
+ maxmem: kdf.params.maxmem
87
+ },
88
+ salt: kdf.salt
89
+ };
90
+ }
91
+ return {
92
+ algorithm: kdf.algorithm,
93
+ params: {
94
+ memoryKiB: kdf.params.memoryKiB,
95
+ iterations: kdf.params.iterations,
96
+ parallelism: kdf.params.parallelism,
97
+ keyLength: kdf.params.keyLength
98
+ },
99
+ salt: kdf.salt
100
+ };
101
+ }
102
+ function parseMetadata(json) {
103
+ if (typeof json !== "string") {
104
+ throw new Error("metadata input must be a string");
105
+ }
106
+ let parsed;
107
+ try {
108
+ parsed = JSON.parse(json);
109
+ } catch (e) {
110
+ const msg = e instanceof Error ? e.message : String(e);
111
+ throw new Error(`metadata is not valid JSON: ${msg}`);
112
+ }
113
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
114
+ throw new Error("metadata must be a JSON object");
115
+ }
116
+ const obj = parsed;
117
+ if (obj.format !== METADATA_FORMAT) {
118
+ throw new Error(
119
+ `unexpected metadata format: ${String(obj.format)} (expected ${METADATA_FORMAT})`
120
+ );
121
+ }
122
+ if (obj.formatVersion !== METADATA_FORMAT_VERSION) {
123
+ throw new Error(
124
+ `unsupported metadata formatVersion: ${String(obj.formatVersion)} (this build supports ${METADATA_FORMAT_VERSION})`
125
+ );
126
+ }
127
+ if (typeof obj.createdAt !== "string" || obj.createdAt.length === 0) {
128
+ throw new Error("metadata.createdAt must be a non-empty string");
129
+ }
130
+ if (obj.note !== void 0 && typeof obj.note !== "string") {
131
+ throw new Error("metadata.note must be a string when present");
132
+ }
133
+ const kdf = parseKdf(obj.kdf);
134
+ const meta = {
135
+ format: METADATA_FORMAT,
136
+ formatVersion: METADATA_FORMAT_VERSION,
137
+ kdf,
138
+ createdAt: obj.createdAt
139
+ };
140
+ if (typeof obj.note === "string") {
141
+ meta.note = obj.note;
142
+ }
143
+ validateMetadata(meta);
144
+ return meta;
145
+ }
146
+ function parseKdf(value) {
147
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
148
+ throw new Error("metadata.kdf must be an object");
149
+ }
150
+ const k = value;
151
+ if (k.algorithm !== "scrypt" && k.algorithm !== "argon2id") {
152
+ throw new Error(`metadata.kdf.algorithm must be 'scrypt' or 'argon2id', got ${String(k.algorithm)}`);
153
+ }
154
+ if (typeof k.salt !== "string") {
155
+ throw new Error("metadata.kdf.salt must be a hex string");
156
+ }
157
+ hexToBytes(k.salt, KDF_SALT_LENGTH);
158
+ if (typeof k.params !== "object" || k.params === null || Array.isArray(k.params)) {
159
+ throw new Error("metadata.kdf.params must be an object");
160
+ }
161
+ const params = k.params;
162
+ if (k.algorithm === "scrypt") {
163
+ const required = ["N", "r", "p", "keyLength", "maxmem"];
164
+ for (const key of required) {
165
+ if (typeof params[key] !== "number" || !Number.isFinite(params[key])) {
166
+ throw new Error(`metadata.kdf.params.${key} must be a finite number`);
167
+ }
168
+ }
169
+ return {
170
+ algorithm: "scrypt",
171
+ params: {
172
+ N: params.N,
173
+ r: params.r,
174
+ p: params.p,
175
+ keyLength: params.keyLength,
176
+ maxmem: params.maxmem
177
+ },
178
+ salt: k.salt
179
+ };
180
+ }
181
+ const required2 = [
182
+ "memoryKiB",
183
+ "iterations",
184
+ "parallelism",
185
+ "keyLength"
186
+ ];
187
+ for (const key of required2) {
188
+ if (typeof params[key] !== "number" || !Number.isFinite(params[key])) {
189
+ throw new Error(`metadata.kdf.params.${key} must be a finite number`);
190
+ }
191
+ }
192
+ return {
193
+ algorithm: "argon2id",
194
+ params: {
195
+ memoryKiB: params.memoryKiB,
196
+ iterations: params.iterations,
197
+ parallelism: params.parallelism,
198
+ keyLength: params.keyLength
199
+ },
200
+ salt: k.salt
201
+ };
202
+ }
203
+ function validateMetadata(meta) {
204
+ if (meta.format !== METADATA_FORMAT) {
205
+ throw new Error(`metadata.format must be ${METADATA_FORMAT}`);
206
+ }
207
+ if (meta.formatVersion !== METADATA_FORMAT_VERSION) {
208
+ throw new Error(`metadata.formatVersion must be ${METADATA_FORMAT_VERSION}`);
209
+ }
210
+ if (typeof meta.createdAt !== "string" || meta.createdAt.length === 0) {
211
+ throw new Error("metadata.createdAt must be a non-empty ISO-8601 string");
212
+ }
213
+ hexToBytes(meta.kdf.salt, KDF_SALT_LENGTH);
214
+ if (meta.kdf.algorithm === "scrypt") {
215
+ const { N, r, p, keyLength, maxmem } = meta.kdf.params;
216
+ if (!Number.isInteger(N) || N < 2 || N > 2 ** 30 || !Number.isInteger(Math.log2(N))) {
217
+ throw new Error("metadata.kdf.params.N must be a power of 2 in [2, 2^30]");
218
+ }
219
+ if (!Number.isInteger(r) || r < 1) {
220
+ throw new Error("metadata.kdf.params.r must be a positive integer");
221
+ }
222
+ if (!Number.isInteger(p) || p < 1) {
223
+ throw new Error("metadata.kdf.params.p must be a positive integer");
224
+ }
225
+ if (!Number.isInteger(keyLength) || keyLength !== 32) {
226
+ throw new Error("metadata.kdf.params.keyLength must be 32 (AES-256 requires a 32-byte key)");
227
+ }
228
+ if (!Number.isInteger(maxmem) || maxmem < 1024) {
229
+ throw new Error("metadata.kdf.params.maxmem must be a positive integer");
230
+ }
231
+ } else if (meta.kdf.algorithm === "argon2id") {
232
+ const { memoryKiB, iterations, parallelism, keyLength } = meta.kdf.params;
233
+ if (!Number.isInteger(memoryKiB) || memoryKiB < 8) {
234
+ throw new Error("metadata.kdf.params.memoryKiB must be \u2265 8");
235
+ }
236
+ if (!Number.isInteger(iterations) || iterations < 1) {
237
+ throw new Error("metadata.kdf.params.iterations must be a positive integer");
238
+ }
239
+ if (!Number.isInteger(parallelism) || parallelism < 1 || parallelism > 255) {
240
+ throw new Error("metadata.kdf.params.parallelism must be an integer in [1, 255]");
241
+ }
242
+ if (!Number.isInteger(keyLength) || keyLength !== 32) {
243
+ throw new Error("metadata.kdf.params.keyLength must be 32 (AES-256 requires a 32-byte key)");
244
+ }
245
+ }
246
+ }
247
+ function decodeMetadataSalt(meta) {
248
+ return hexToBytes(meta.kdf.salt, KDF_SALT_LENGTH);
249
+ }
250
+
251
+ export {
252
+ METADATA_FORMAT,
253
+ METADATA_FORMAT_VERSION,
254
+ buildMetadata,
255
+ serializeMetadata,
256
+ parseMetadata,
257
+ validateMetadata,
258
+ decodeMetadataSalt
259
+ };