@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.
- package/dist/{calibration-674TDQNV.js → calibration-WCHOK6DX.js} +12 -4
- package/dist/capsule-cli-GBM3WPAM.js +33 -0
- package/dist/capsule-crypto-K3IRTKRH.js +17 -0
- package/dist/capsule-export-IXVERCQG.js +17 -0
- package/dist/capsule-import-IA6VIOPQ.js +16 -0
- package/dist/capsule-merge-IWOQ34KL.js +189 -0
- package/dist/{causal-chain-OKDZSDEB.js → causal-chain-WYN5QOPS.js} +3 -2
- package/dist/{causal-consolidation-5BEXLQV5.js → causal-consolidation-YI53C2AO.js} +16 -12
- package/dist/{causal-retrieval-3BKBXVXD.js → causal-retrieval-NZHQOZOE.js} +6 -5
- package/dist/{causal-trajectory-graph-RQIT37DN.js → causal-trajectory-graph-VBPE2WPM.js} +1 -1
- package/dist/chunk-37NKFWSO.js +233 -0
- package/dist/chunk-3G7FAF6S.js +60 -0
- package/dist/{chunk-Z7GRLVK3.js → chunk-3GUF7RQI.js} +235 -19
- package/dist/chunk-4G2XCSD2.js +186 -0
- package/dist/chunk-4LYQ4ONL.js +185 -0
- package/dist/chunk-6F6EKSVP.js +453 -0
- package/dist/chunk-6IWEAUN6.js +148 -0
- package/dist/{chunk-LN5UZQVG.js → chunk-6UFI73TJ.js} +5 -3
- package/dist/chunk-7OQEPGQF.js +529 -0
- package/dist/{chunk-JJSNPSCD.js → chunk-7UZNLMW5.js} +652 -174
- package/dist/chunk-B52XADV3.js +244 -0
- package/dist/chunk-BU5KJVWF.js +78 -0
- package/dist/chunk-CDAZGIGT.js +720 -0
- package/dist/chunk-CXM7EBAO.js +289 -0
- package/dist/{chunk-HCFFXBLV.js → chunk-EXDYWXMB.js} +6 -1861
- package/dist/chunk-FGTYFLL5.js +274 -0
- package/dist/chunk-FQRSVYY4.js +110 -0
- package/dist/chunk-HRGFO6AW.js +349 -0
- package/dist/chunk-I6B2W2IY.js +47 -0
- package/dist/chunk-JZBOXOUC.js +259 -0
- package/dist/chunk-L6I4MQKO.js +227 -0
- package/dist/chunk-LLUROTZJ.js +328 -0
- package/dist/chunk-MBIFE6SA.js +250 -0
- package/dist/chunk-NKVIN6RD.js +118 -0
- package/dist/chunk-OAE7AQ6R.js +1832 -0
- package/dist/chunk-OEI7GLV2.js +17 -0
- package/dist/chunk-RKR6PTPA.js +308 -0
- package/dist/{chunk-7TENHBV2.js → chunk-RQCTMECT.js} +10 -48
- package/dist/chunk-SSFTU6LP.js +182 -0
- package/dist/{chunk-BXTMZDRT.js → chunk-SVSQAG6M.js} +7 -5
- package/dist/{chunk-S2ISS4AH.js → chunk-TILAJIJR.js} +10 -10
- package/dist/chunk-TLVIQLB4.js +874 -0
- package/dist/{chunk-YHH3SXKD.js → chunk-WPINX4MF.js} +1 -59
- package/dist/chunk-YGGGUTG3.js +125 -0
- package/dist/cipher-VHAFCG7Z.js +27 -0
- package/dist/dreams-ledger-3I52ISYR.js +285 -0
- package/dist/{engine-65C2J63X.js → engine-BIYI3P4J.js} +7 -2
- package/dist/{fallback-llm-LVK5PDIM.js → fallback-llm-WCWNGIQ3.js} +2 -1
- package/dist/first-start-migration-I24M2JEE.js +258 -0
- package/dist/forget-NI4RBDPB.js +68 -0
- package/dist/fs-utils-PZRI2HDZ.js +29 -0
- package/dist/graph-edge-decay-5CVKWBYH.js +203 -0
- package/dist/index.js +10654 -3067
- package/dist/kdf-H5B23ZM2.js +25 -0
- package/dist/memory-governance-SJ5DGRB3.js +25 -0
- package/dist/metadata-JAGIWHEA.js +20 -0
- package/dist/migrate-from-identity-anchor-N3354WMP.js +7 -0
- package/dist/path-5LCUBAAZ.js +8 -0
- package/dist/peers-JF2I6RCR.js +43 -0
- package/dist/purge-XN2VSPZ2.js +204 -0
- package/dist/secure-store-A4NGCNXV.js +155 -0
- package/dist/state-PVISYXRH.js +7 -0
- package/dist/state-store-LP5BO6SF.js +15 -0
- package/dist/{storage-DM4ZGOCN.js → storage-PTQ2H2YJ.js} +3 -1
- package/dist/tier-stats-IZNW66NC.js +147 -0
- package/dist/trace-NJESSGH7.js +289 -0
- package/dist/tui-MGK2LYJY.js +12 -0
- package/dist/types-R4DO7AKM.js +30 -0
- package/openclaw.plugin.json +519 -4
- package/package.json +2 -2
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CAPSULE_ID_PATTERN,
|
|
3
|
+
CapsuleBlockSchema,
|
|
4
|
+
ExportBundleV2Schema,
|
|
5
|
+
ExportManifestV2Schema
|
|
6
|
+
} from "./chunk-4LYQ4ONL.js";
|
|
7
|
+
import {
|
|
8
|
+
listFilesRecursive,
|
|
9
|
+
sha256File,
|
|
10
|
+
toPosixRelPath,
|
|
11
|
+
writeJsonFile
|
|
12
|
+
} from "./chunk-NKVIN6RD.js";
|
|
13
|
+
import {
|
|
14
|
+
encryptCapsuleFile
|
|
15
|
+
} from "./chunk-SSFTU6LP.js";
|
|
16
|
+
|
|
17
|
+
// ../remnic-core/src/transfer/capsule-export.ts
|
|
18
|
+
import { mkdir, readFile, stat, writeFile } from "fs/promises";
|
|
19
|
+
import path from "path";
|
|
20
|
+
import { gzipSync } from "zlib";
|
|
21
|
+
|
|
22
|
+
// ../remnic-core/src/transfer/constants.ts
|
|
23
|
+
var EXPORT_FORMAT = "openclaw-engram-export";
|
|
24
|
+
var EXPORT_SCHEMA_VERSION = 1;
|
|
25
|
+
var CAPSULE_SCHEMA_VERSION = 2;
|
|
26
|
+
|
|
27
|
+
// ../remnic-core/src/transfer/capsule-export.ts
|
|
28
|
+
var DEFAULT_EXCLUDE_DIRS = /* @__PURE__ */ new Set([
|
|
29
|
+
"node_modules",
|
|
30
|
+
".git",
|
|
31
|
+
// Never export the secure-store directory: it contains the encryption
|
|
32
|
+
// header (KDF params + verifier) which is security-sensitive and
|
|
33
|
+
// machine-specific. The passphrase is not stored here, but including
|
|
34
|
+
// the header in a capsule would let an attacker brute-force the
|
|
35
|
+
// passphrase offline if the capsule is intercepted.
|
|
36
|
+
".secure-store",
|
|
37
|
+
// Exclude .capsules to avoid recursive self-inclusion.
|
|
38
|
+
".capsules"
|
|
39
|
+
]);
|
|
40
|
+
var TRANSCRIPTS_DIR = "transcripts";
|
|
41
|
+
var PEERS_DIR = "peers";
|
|
42
|
+
async function exportCapsule(opts) {
|
|
43
|
+
validateName(opts.name);
|
|
44
|
+
const sinceMs = parseSince(opts.since);
|
|
45
|
+
const includeKinds = normalizeIncludeKinds(opts.includeKinds);
|
|
46
|
+
const peerFilter = normalizePeerIds(opts.peerIds);
|
|
47
|
+
const transcriptsOverride = opts.includeTranscripts === true;
|
|
48
|
+
const rootAbs = path.resolve(opts.root);
|
|
49
|
+
await assertIsDirectory(rootAbs);
|
|
50
|
+
const outDirAbs = path.resolve(opts.outDir ?? path.join(rootAbs, ".capsules"));
|
|
51
|
+
if (outDirAbs === rootAbs) {
|
|
52
|
+
throw new Error(
|
|
53
|
+
"exportCapsule: 'outDir' must not equal 'root'. Choose a separate directory (default: <root>/.capsules) so the export does not overwrite or shadow the source tree."
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
await mkdir(outDirAbs, { recursive: true });
|
|
57
|
+
const outDirRelPosix = computeOutDirRel(rootAbs, outDirAbs);
|
|
58
|
+
const filesAbs = await listFilesRecursive(rootAbs);
|
|
59
|
+
const records = [];
|
|
60
|
+
const manifestFiles = [];
|
|
61
|
+
for (const abs of filesAbs) {
|
|
62
|
+
const relPosix = toPosixRelPath(abs, rootAbs);
|
|
63
|
+
if (!shouldInclude(relPosix, includeKinds, peerFilter, outDirRelPosix, transcriptsOverride)) continue;
|
|
64
|
+
if (sinceMs !== null) {
|
|
65
|
+
const st = await stat(abs);
|
|
66
|
+
if (st.mtimeMs < sinceMs) continue;
|
|
67
|
+
}
|
|
68
|
+
const content = await readFile(abs, "utf-8");
|
|
69
|
+
records.push({ path: relPosix, content });
|
|
70
|
+
const { sha256, bytes } = await sha256File(abs);
|
|
71
|
+
manifestFiles.push({ path: relPosix, sha256, bytes });
|
|
72
|
+
}
|
|
73
|
+
records.sort((a, b) => a.path.localeCompare(b.path));
|
|
74
|
+
manifestFiles.sort((a, b) => a.path.localeCompare(b.path));
|
|
75
|
+
const capsule = buildCapsuleBlock(opts.name, opts.capsule);
|
|
76
|
+
const includesTranscripts = transcriptsOverride || (includeKinds ?? /* @__PURE__ */ new Set()).has(TRANSCRIPTS_DIR);
|
|
77
|
+
const createdAtMs = opts.now ?? Date.now();
|
|
78
|
+
const manifest = ExportManifestV2Schema.parse({
|
|
79
|
+
format: EXPORT_FORMAT,
|
|
80
|
+
schemaVersion: CAPSULE_SCHEMA_VERSION,
|
|
81
|
+
createdAt: new Date(createdAtMs).toISOString(),
|
|
82
|
+
pluginVersion: opts.pluginVersion ?? "0.0.0",
|
|
83
|
+
includesTranscripts,
|
|
84
|
+
files: manifestFiles,
|
|
85
|
+
capsule
|
|
86
|
+
});
|
|
87
|
+
const bundle = ExportBundleV2Schema.parse({
|
|
88
|
+
manifest,
|
|
89
|
+
records
|
|
90
|
+
});
|
|
91
|
+
const archivePath = path.join(outDirAbs, `${opts.name}.capsule.json.gz`);
|
|
92
|
+
const manifestPath = path.join(outDirAbs, `${opts.name}.manifest.json`);
|
|
93
|
+
await writeJsonFile(manifestPath, manifest);
|
|
94
|
+
const json = JSON.stringify(bundle);
|
|
95
|
+
const gz = gzipSync(Buffer.from(json, "utf-8"));
|
|
96
|
+
await writeFile(archivePath, gz);
|
|
97
|
+
if (opts.encrypt === true) {
|
|
98
|
+
if (!opts.memoryDir) {
|
|
99
|
+
throw new Error(
|
|
100
|
+
"exportCapsule: 'memoryDir' is required when 'encrypt' is true so the secure-store key can be retrieved."
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
const { encPath } = await encryptCapsuleFile({
|
|
104
|
+
sourceGzPath: archivePath,
|
|
105
|
+
memoryDir: opts.memoryDir
|
|
106
|
+
});
|
|
107
|
+
const { unlink } = await import("fs/promises");
|
|
108
|
+
await unlink(archivePath);
|
|
109
|
+
return { archivePath: encPath, manifestPath, manifest, encryptedArchivePath: encPath };
|
|
110
|
+
}
|
|
111
|
+
return { archivePath, manifestPath, manifest, encryptedArchivePath: null };
|
|
112
|
+
}
|
|
113
|
+
function validateName(name) {
|
|
114
|
+
if (typeof name !== "string" || !CAPSULE_ID_PATTERN.test(name)) {
|
|
115
|
+
throw new Error(
|
|
116
|
+
`exportCapsule: invalid capsule name. Expected /${CAPSULE_ID_PATTERN.source}/`
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
if (name.length > 64) {
|
|
120
|
+
throw new Error(
|
|
121
|
+
"exportCapsule: invalid capsule name. Must be 64 characters or fewer."
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
var ISO_8601_RE = /^\d{4}-\d{2}-\d{2}(?:[Tt]\d{2}:\d{2}(?::\d{2}(?:\.\d{1,9})?)?(?:[Zz]|[+-]\d{2}:?\d{2}))?$/;
|
|
126
|
+
function parseSince(since) {
|
|
127
|
+
if (since === void 0) return null;
|
|
128
|
+
if (typeof since !== "string" || since.trim() === "") {
|
|
129
|
+
throw new Error("exportCapsule: 'since' must be a non-empty ISO-8601 string");
|
|
130
|
+
}
|
|
131
|
+
if (!ISO_8601_RE.test(since)) {
|
|
132
|
+
throw new Error(
|
|
133
|
+
`exportCapsule: 'since' is not a valid ISO-8601 timestamp: ${since}`
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
const ms = Date.parse(since);
|
|
137
|
+
if (!Number.isFinite(ms)) {
|
|
138
|
+
throw new Error(
|
|
139
|
+
`exportCapsule: 'since' is not a valid ISO-8601 timestamp: ${since}`
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
assertCalendarRoundTrip(since, ms);
|
|
143
|
+
return ms;
|
|
144
|
+
}
|
|
145
|
+
function isValidCapsuleSince(since) {
|
|
146
|
+
try {
|
|
147
|
+
parseSince(since);
|
|
148
|
+
return true;
|
|
149
|
+
} catch {
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
function assertCalendarRoundTrip(since, ms) {
|
|
154
|
+
const m = /^(\d{4})-(\d{2})-(\d{2})/.exec(since);
|
|
155
|
+
if (!m) return;
|
|
156
|
+
const wantY = Number(m[1]);
|
|
157
|
+
const wantMo = Number(m[2]);
|
|
158
|
+
const wantD = Number(m[3]);
|
|
159
|
+
const offsetMatch = /([+-])(\d{2}):?(\d{2})$/.exec(since);
|
|
160
|
+
let displayMs = ms;
|
|
161
|
+
if (offsetMatch) {
|
|
162
|
+
const sign = offsetMatch[1] === "-" ? -1 : 1;
|
|
163
|
+
const offsetMin = sign * (Number(offsetMatch[2]) * 60 + Number(offsetMatch[3]));
|
|
164
|
+
displayMs = ms + offsetMin * 6e4;
|
|
165
|
+
}
|
|
166
|
+
const dd = new Date(displayMs);
|
|
167
|
+
const gotY = dd.getUTCFullYear();
|
|
168
|
+
const gotMo = dd.getUTCMonth() + 1;
|
|
169
|
+
const gotD = dd.getUTCDate();
|
|
170
|
+
if (gotY !== wantY || gotMo !== wantMo || gotD !== wantD) {
|
|
171
|
+
throw new Error(
|
|
172
|
+
`exportCapsule: 'since' is not a valid ISO-8601 timestamp: ${since}`
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
function normalizeIncludeKinds(kinds) {
|
|
177
|
+
if (kinds === void 0) return null;
|
|
178
|
+
const set = /* @__PURE__ */ new Set();
|
|
179
|
+
for (const raw of kinds) {
|
|
180
|
+
if (typeof raw !== "string" || raw.trim() === "") {
|
|
181
|
+
throw new Error("exportCapsule: 'includeKinds' entries must be non-empty strings");
|
|
182
|
+
}
|
|
183
|
+
if (raw.includes("/") || raw.includes("\\")) {
|
|
184
|
+
throw new Error(
|
|
185
|
+
`exportCapsule: 'includeKinds' entries must be top-level segment names, got: ${raw}`
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
set.add(raw);
|
|
189
|
+
}
|
|
190
|
+
return set;
|
|
191
|
+
}
|
|
192
|
+
function normalizePeerIds(peerIds) {
|
|
193
|
+
if (peerIds === void 0) return null;
|
|
194
|
+
const set = /* @__PURE__ */ new Set();
|
|
195
|
+
for (const raw of peerIds) {
|
|
196
|
+
if (typeof raw !== "string" || raw.trim() === "") {
|
|
197
|
+
throw new Error("exportCapsule: 'peerIds' entries must be non-empty strings");
|
|
198
|
+
}
|
|
199
|
+
if (raw.includes("/") || raw.includes("\\") || raw === "." || raw === "..") {
|
|
200
|
+
throw new Error(
|
|
201
|
+
`exportCapsule: 'peerIds' entries must be plain segment names, got: ${raw}`
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
set.add(raw);
|
|
205
|
+
}
|
|
206
|
+
return set;
|
|
207
|
+
}
|
|
208
|
+
function computeOutDirRel(rootAbs, outDirAbs) {
|
|
209
|
+
const rel = path.relative(rootAbs, outDirAbs);
|
|
210
|
+
if (rel === "") return null;
|
|
211
|
+
if (rel === ".." || rel.startsWith(`..${path.sep}`)) return null;
|
|
212
|
+
if (path.isAbsolute(rel)) return null;
|
|
213
|
+
return rel.split(path.sep).join("/");
|
|
214
|
+
}
|
|
215
|
+
async function assertIsDirectory(absPath) {
|
|
216
|
+
const st = await stat(absPath).catch(() => null);
|
|
217
|
+
if (!st || !st.isDirectory()) {
|
|
218
|
+
throw new Error(
|
|
219
|
+
`exportCapsule: 'root' must be an existing directory: ${absPath}`
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
function shouldInclude(relPosix, includeKinds, peerFilter, outDirRelPosix, transcriptsOverride = false) {
|
|
224
|
+
const parts = relPosix.split("/");
|
|
225
|
+
if (parts.some((p) => DEFAULT_EXCLUDE_DIRS.has(p))) return false;
|
|
226
|
+
if (outDirRelPosix !== null) {
|
|
227
|
+
if (relPosix === outDirRelPosix) return false;
|
|
228
|
+
if (relPosix.startsWith(`${outDirRelPosix}/`)) return false;
|
|
229
|
+
}
|
|
230
|
+
const top = parts[0];
|
|
231
|
+
if (top === TRANSCRIPTS_DIR && !transcriptsOverride && (includeKinds === null || !includeKinds.has(TRANSCRIPTS_DIR))) {
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
if (includeKinds !== null) {
|
|
235
|
+
if (parts.length < 2) return false;
|
|
236
|
+
if (!includeKinds.has(top)) return false;
|
|
237
|
+
}
|
|
238
|
+
if (top === PEERS_DIR && peerFilter !== null) {
|
|
239
|
+
if (peerFilter.size === 0) return false;
|
|
240
|
+
if (parts.length < 2) return false;
|
|
241
|
+
if (!peerFilter.has(parts[1])) return false;
|
|
242
|
+
}
|
|
243
|
+
return true;
|
|
244
|
+
}
|
|
245
|
+
function buildCapsuleBlock(name, override) {
|
|
246
|
+
const parent = override?.parent ?? null;
|
|
247
|
+
const parentCapsule = override && Object.prototype.hasOwnProperty.call(override, "parentCapsule") ? override.parentCapsule ?? null : parent?.capsuleId ?? null;
|
|
248
|
+
const merged = {
|
|
249
|
+
id: name,
|
|
250
|
+
version: override?.version ?? "0.1.0",
|
|
251
|
+
schemaVersion: override?.schemaVersion ?? "taxonomy-v1",
|
|
252
|
+
parentCapsule,
|
|
253
|
+
parent,
|
|
254
|
+
description: override?.description ?? "",
|
|
255
|
+
retrievalPolicy: override?.retrievalPolicy ?? {
|
|
256
|
+
tierWeights: {},
|
|
257
|
+
directAnswerEnabled: false
|
|
258
|
+
},
|
|
259
|
+
includes: override?.includes ?? {
|
|
260
|
+
taxonomy: false,
|
|
261
|
+
identityAnchors: false,
|
|
262
|
+
peerProfiles: false,
|
|
263
|
+
procedural: false
|
|
264
|
+
}
|
|
265
|
+
};
|
|
266
|
+
return CapsuleBlockSchema.parse(merged);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export {
|
|
270
|
+
EXPORT_FORMAT,
|
|
271
|
+
EXPORT_SCHEMA_VERSION,
|
|
272
|
+
exportCapsule,
|
|
273
|
+
isValidCapsuleSince
|
|
274
|
+
};
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// ../remnic-core/src/routing/engine.ts
|
|
2
|
+
var DEFAULT_CATEGORIES = [
|
|
3
|
+
"fact",
|
|
4
|
+
"preference",
|
|
5
|
+
"correction",
|
|
6
|
+
"entity",
|
|
7
|
+
"decision",
|
|
8
|
+
"relationship",
|
|
9
|
+
"principle",
|
|
10
|
+
"commitment",
|
|
11
|
+
"moment",
|
|
12
|
+
"skill",
|
|
13
|
+
"rule",
|
|
14
|
+
"procedure",
|
|
15
|
+
"reasoning_trace"
|
|
16
|
+
];
|
|
17
|
+
function normalizeNamespace(namespace) {
|
|
18
|
+
return namespace.trim();
|
|
19
|
+
}
|
|
20
|
+
function isLikelyUnsafeRegex(pattern) {
|
|
21
|
+
const value = pattern.trim();
|
|
22
|
+
if (value.length === 0) return true;
|
|
23
|
+
if (value.length > 120) return true;
|
|
24
|
+
if (/\\[1-9]/.test(value)) return true;
|
|
25
|
+
if (/\(\?<?[=!]/.test(value)) return true;
|
|
26
|
+
if (/\((?:[^()\\]|\\.)*[+*](?:[^()\\]|\\.)*\)[+*{]/.test(value)) return true;
|
|
27
|
+
if (/(^|[^\\])[()|]/.test(value)) return true;
|
|
28
|
+
const quantifierCount = (value.match(/(^|[^\\])[*+?]/g)?.length ?? 0) + (value.match(/(^|[^\\])\{/g)?.length ?? 0);
|
|
29
|
+
if (quantifierCount > 1) return true;
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
function isSafeRouteNamespace(namespace) {
|
|
33
|
+
const value = normalizeNamespace(namespace);
|
|
34
|
+
if (value.length === 0) return false;
|
|
35
|
+
if (value === ".") return false;
|
|
36
|
+
if (value.includes("/") || value.includes("\\")) return false;
|
|
37
|
+
if (value.includes("..")) return false;
|
|
38
|
+
return /^[A-Za-z0-9._-]{1,64}$/.test(value);
|
|
39
|
+
}
|
|
40
|
+
function validateRouteTarget(target, options) {
|
|
41
|
+
if (!target || typeof target !== "object") {
|
|
42
|
+
return { ok: false, error: "target must be an object" };
|
|
43
|
+
}
|
|
44
|
+
const allowedCategories = new Set(options?.allowedCategories ?? DEFAULT_CATEGORIES);
|
|
45
|
+
const allowedNamespaces = options?.allowedNamespaces ? new Set(options.allowedNamespaces.map((v) => v.trim()).filter((v) => v.length > 0)) : null;
|
|
46
|
+
const normalized = {};
|
|
47
|
+
if (typeof target.category === "string") {
|
|
48
|
+
if (!allowedCategories.has(target.category)) {
|
|
49
|
+
return { ok: false, error: `invalid category: ${target.category}` };
|
|
50
|
+
}
|
|
51
|
+
normalized.category = target.category;
|
|
52
|
+
}
|
|
53
|
+
if (typeof target.namespace === "string") {
|
|
54
|
+
const namespace = normalizeNamespace(target.namespace);
|
|
55
|
+
if (!isSafeRouteNamespace(namespace)) {
|
|
56
|
+
return { ok: false, error: `invalid namespace: ${target.namespace}` };
|
|
57
|
+
}
|
|
58
|
+
if (allowedNamespaces && !allowedNamespaces.has(namespace)) {
|
|
59
|
+
return { ok: false, error: `namespace not allowed: ${namespace}` };
|
|
60
|
+
}
|
|
61
|
+
normalized.namespace = namespace;
|
|
62
|
+
}
|
|
63
|
+
if (!normalized.category && !normalized.namespace) {
|
|
64
|
+
return { ok: false, error: "target must include category or namespace" };
|
|
65
|
+
}
|
|
66
|
+
return { ok: true, target: normalized };
|
|
67
|
+
}
|
|
68
|
+
function doesRuleMatch(rule, text) {
|
|
69
|
+
if (!rule || typeof rule !== "object") return false;
|
|
70
|
+
if (rule.enabled === false) return false;
|
|
71
|
+
if (typeof rule.pattern !== "string") return false;
|
|
72
|
+
const pattern = rule.pattern.trim();
|
|
73
|
+
if (pattern.length === 0) return false;
|
|
74
|
+
if (rule.patternType === "keyword") {
|
|
75
|
+
return text.toLowerCase().includes(pattern.toLowerCase());
|
|
76
|
+
}
|
|
77
|
+
if (rule.patternType !== "regex") {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
if (isLikelyUnsafeRegex(pattern)) {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
return new RegExp(pattern, "i").test(text);
|
|
85
|
+
} catch {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function selectRouteRule(text, rules, options) {
|
|
90
|
+
const ranked = rules.map((rule, index) => ({ rule, index })).sort((a, b) => {
|
|
91
|
+
if (b.rule.priority !== a.rule.priority) return b.rule.priority - a.rule.priority;
|
|
92
|
+
return a.index - b.index;
|
|
93
|
+
});
|
|
94
|
+
for (const entry of ranked) {
|
|
95
|
+
if (!doesRuleMatch(entry.rule, text)) continue;
|
|
96
|
+
const validation = validateRouteTarget(entry.rule.target, options);
|
|
97
|
+
if (!validation.ok || !validation.target) continue;
|
|
98
|
+
return {
|
|
99
|
+
rule: entry.rule,
|
|
100
|
+
target: validation.target
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export {
|
|
107
|
+
isSafeRouteNamespace,
|
|
108
|
+
validateRouteTarget,
|
|
109
|
+
selectRouteRule
|
|
110
|
+
};
|