@crewhaus/tool-secure 0.7.0 → 0.8.0
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/LICENSE +201 -0
- package/NOTICE +9 -0
- package/README.md +18 -1
- package/dist/index.d.ts +16 -0
- package/dist/index.js +113 -37
- package/dist/lib/injection.d.ts +31 -0
- package/dist/lib/injection.js +176 -6
- package/dist/lib/ipv6-scan.d.ts +16 -0
- package/dist/lib/ipv6-scan.js +73 -0
- package/dist/lib/literal.d.ts +55 -0
- package/dist/lib/literal.js +146 -0
- package/dist/lib/pii.d.ts +19 -30
- package/dist/lib/pii.js +71 -8
- package/dist/lib/policy.d.ts +24 -2
- package/dist/lib/policy.js +176 -79
- package/dist/lib/secrets.d.ts +8 -0
- package/dist/lib/secrets.js +43 -5
- package/dist/lib/text.d.ts +61 -2
- package/dist/lib/text.js +174 -5
- package/dist/paths.d.ts +21 -2
- package/dist/paths.js +72 -13
- package/package.json +12 -6
package/dist/lib/text.js
CHANGED
|
@@ -79,6 +79,89 @@ export function maskValue(value, keep = 2) {
|
|
|
79
79
|
const tail = value.slice(n - keep);
|
|
80
80
|
return `${head}${"*".repeat(Math.min(n - 2 * keep, 12))}${tail}`;
|
|
81
81
|
}
|
|
82
|
+
/**
|
|
83
|
+
* Spans, asked one question: does `[start, end)` overlap any of them?
|
|
84
|
+
*
|
|
85
|
+
* Overlap is `start < k.end && k.start < end`, the test `dedupeOverlaps` and
|
|
86
|
+
* `secretFindings` make, answered for any numbers, empty spans included.
|
|
87
|
+
* Among the spans that start before `end`, one overlaps exactly when the
|
|
88
|
+
* largest of their ends is past `start`. So the index keeps that largest end
|
|
89
|
+
* in a Fenwick tree with one slot per distinct start, and a question or an
|
|
90
|
+
* addition visits O(log n) slots. Checking every span kept so far made
|
|
91
|
+
* those two passes quadratic in the findings, on text a model or a web page
|
|
92
|
+
* supplies.
|
|
93
|
+
*
|
|
94
|
+
* Every start a span will be added with is named up front, because the
|
|
95
|
+
* slots are the distinct starts in order. `steps` counts the slots and
|
|
96
|
+
* probes the calls visit, so a test can hold the index to its bound.
|
|
97
|
+
*/
|
|
98
|
+
export class SpanIndex {
|
|
99
|
+
/** Tree slots and binary-search probes visited so far. */
|
|
100
|
+
steps = 0;
|
|
101
|
+
/** The distinct starts, ascending. */
|
|
102
|
+
starts;
|
|
103
|
+
/** 1-based Fenwick tree of the largest end per prefix of `starts`. */
|
|
104
|
+
maxEnd;
|
|
105
|
+
constructor(starts) {
|
|
106
|
+
const sorted = new Float64Array(starts.length);
|
|
107
|
+
let n = 0;
|
|
108
|
+
// A NaN start never satisfies the test, so it never needs a slot.
|
|
109
|
+
for (let i = 0; i < starts.length; i++) {
|
|
110
|
+
const s = starts[i];
|
|
111
|
+
if (!Number.isNaN(s))
|
|
112
|
+
sorted[n++] = s;
|
|
113
|
+
}
|
|
114
|
+
sorted.subarray(0, n).sort();
|
|
115
|
+
let distinct = 0;
|
|
116
|
+
for (let i = 0; i < n; i++) {
|
|
117
|
+
const s = sorted[i];
|
|
118
|
+
if (distinct === 0 || s !== sorted[distinct - 1])
|
|
119
|
+
sorted[distinct++] = s;
|
|
120
|
+
}
|
|
121
|
+
this.starts = sorted.slice(0, distinct);
|
|
122
|
+
this.maxEnd = new Float64Array(distinct + 1).fill(Number.NEGATIVE_INFINITY);
|
|
123
|
+
}
|
|
124
|
+
/** How many distinct starts are below `bound`; none are below NaN. */
|
|
125
|
+
below(bound) {
|
|
126
|
+
let lo = 0;
|
|
127
|
+
let hi = this.starts.length;
|
|
128
|
+
while (lo < hi) {
|
|
129
|
+
this.steps++;
|
|
130
|
+
const mid = (lo + hi) >>> 1;
|
|
131
|
+
if (this.starts[mid] < bound)
|
|
132
|
+
lo = mid + 1;
|
|
133
|
+
else
|
|
134
|
+
hi = mid;
|
|
135
|
+
}
|
|
136
|
+
return lo;
|
|
137
|
+
}
|
|
138
|
+
/** Whether some span added so far has `k.start < end && start < k.end`. */
|
|
139
|
+
overlaps(start, end) {
|
|
140
|
+
let most = Number.NEGATIVE_INFINITY;
|
|
141
|
+
for (let i = this.below(end); i > 0; i -= i & -i) {
|
|
142
|
+
this.steps++;
|
|
143
|
+
const e = this.maxEnd[i];
|
|
144
|
+
if (e > most)
|
|
145
|
+
most = e;
|
|
146
|
+
}
|
|
147
|
+
return most > start;
|
|
148
|
+
}
|
|
149
|
+
/** Add `[start, end)`; `start` must be one the index was built with. */
|
|
150
|
+
add(start, end) {
|
|
151
|
+
// A NaN on either side never satisfies the test, so it is never found.
|
|
152
|
+
if (Number.isNaN(start) || Number.isNaN(end))
|
|
153
|
+
return;
|
|
154
|
+
const at = this.below(start);
|
|
155
|
+
if (at >= this.starts.length || this.starts[at] !== start) {
|
|
156
|
+
throw new Error(`SpanIndex: start ${start} was not named when the index was built`);
|
|
157
|
+
}
|
|
158
|
+
for (let i = at + 1; i < this.maxEnd.length; i += i & -i) {
|
|
159
|
+
this.steps++;
|
|
160
|
+
if (end > this.maxEnd[i])
|
|
161
|
+
this.maxEnd[i] = end;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
82
165
|
/**
|
|
83
166
|
* Drop findings swallowed by a stronger one.
|
|
84
167
|
*
|
|
@@ -87,6 +170,10 @@ export function maskValue(value, keep = 2) {
|
|
|
87
170
|
* survivor is chosen by confidence, then by length, then by earliest start,
|
|
88
171
|
* then by rule name — a total order, so the same input always yields the
|
|
89
172
|
* same set.
|
|
173
|
+
*
|
|
174
|
+
* Each candidate, in that order, is kept unless it overlaps one kept before
|
|
175
|
+
* it. A `SpanIndex` answers that, so the pass is O(k log k) in the findings
|
|
176
|
+
* rather than a check against every finding kept so far.
|
|
90
177
|
*/
|
|
91
178
|
export function dedupeOverlaps(findings) {
|
|
92
179
|
const ranked = [...findings].sort((a, b) => {
|
|
@@ -100,11 +187,13 @@ export function dedupeOverlaps(findings) {
|
|
|
100
187
|
return a.start - b.start;
|
|
101
188
|
return compareStrings(a.rule, b.rule);
|
|
102
189
|
});
|
|
190
|
+
const index = new SpanIndex(ranked.map((f) => f.start));
|
|
103
191
|
const kept = [];
|
|
104
192
|
for (const candidate of ranked) {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
193
|
+
if (index.overlaps(candidate.start, candidate.end))
|
|
194
|
+
continue;
|
|
195
|
+
kept.push(candidate);
|
|
196
|
+
index.add(candidate.start, candidate.end);
|
|
108
197
|
}
|
|
109
198
|
return sortFindings(kept);
|
|
110
199
|
}
|
|
@@ -123,11 +212,54 @@ export function withPositions(text, raw) {
|
|
|
123
212
|
/**
|
|
124
213
|
* Replace each span with the string `replace` returns for it.
|
|
125
214
|
*
|
|
126
|
-
* Spans must not overlap (`dedupeOverlaps` guarantees that)
|
|
127
|
-
*
|
|
215
|
+
* Spans must not overlap (`dedupeOverlaps` guarantees that). `replace` is
|
|
216
|
+
* called right to left, as it always has been, and the output is the text
|
|
217
|
+
* between the spans joined with their replacements: one copy of the text.
|
|
218
|
+
* Rebuilding the whole string for every span was O(spans × text), and a
|
|
219
|
+
* redaction pass over megabytes of findings-dense text has hundreds of
|
|
220
|
+
* thousands of spans. Spans that overlap, sit out of order or outside the
|
|
221
|
+
* text, or have fractional offsets keep the old right-to-left rebuild, which
|
|
222
|
+
* gives the same result on spans that do none of those things.
|
|
128
223
|
*/
|
|
129
224
|
export function applySpans(text, findings, replace) {
|
|
130
225
|
const ordered = sortFindings(findings);
|
|
226
|
+
if (!disjointWithin(ordered, text.length))
|
|
227
|
+
return rebuildRightToLeft(text, ordered, replace);
|
|
228
|
+
const replacements = new Array(ordered.length);
|
|
229
|
+
for (let i = ordered.length - 1; i >= 0; i--) {
|
|
230
|
+
const f = ordered[i];
|
|
231
|
+
if (f)
|
|
232
|
+
replacements[i] = replace(f);
|
|
233
|
+
}
|
|
234
|
+
const parts = [];
|
|
235
|
+
let at = 0;
|
|
236
|
+
for (let i = 0; i < ordered.length; i++) {
|
|
237
|
+
const f = ordered[i];
|
|
238
|
+
if (!f)
|
|
239
|
+
continue;
|
|
240
|
+
parts.push(text.slice(at, f.start), replacements[i]);
|
|
241
|
+
at = f.end;
|
|
242
|
+
}
|
|
243
|
+
parts.push(text.slice(at));
|
|
244
|
+
return parts.join("");
|
|
245
|
+
}
|
|
246
|
+
/** Whether sorted spans are whole offsets, in order, apart, and inside the text. */
|
|
247
|
+
function disjointWithin(ordered, length) {
|
|
248
|
+
let at = 0;
|
|
249
|
+
for (let i = 0; i < ordered.length; i++) {
|
|
250
|
+
const f = ordered[i];
|
|
251
|
+
if (!f)
|
|
252
|
+
continue;
|
|
253
|
+
if (!Number.isInteger(f.start) || !Number.isInteger(f.end))
|
|
254
|
+
return false;
|
|
255
|
+
if (f.start < at || f.end < f.start || f.end > length)
|
|
256
|
+
return false;
|
|
257
|
+
at = f.end;
|
|
258
|
+
}
|
|
259
|
+
return true;
|
|
260
|
+
}
|
|
261
|
+
/** The original rebuild, kept for spans `disjointWithin` refuses. */
|
|
262
|
+
function rebuildRightToLeft(text, ordered, replace) {
|
|
131
263
|
let out = text;
|
|
132
264
|
for (let i = ordered.length - 1; i >= 0; i--) {
|
|
133
265
|
const f = ordered[i];
|
|
@@ -173,6 +305,43 @@ export function* matchAll(text, pattern) {
|
|
|
173
305
|
re.lastIndex += 1;
|
|
174
306
|
}
|
|
175
307
|
}
|
|
308
|
+
/**
|
|
309
|
+
* `matchAll(text, pattern)`'s matches, in order, with the pattern tried
|
|
310
|
+
* (sticky) only where `canStart` says a match may begin, and, after a try
|
|
311
|
+
* that fails at `at`, not again before `failedTo(text, at)`.
|
|
312
|
+
*
|
|
313
|
+
* For a pattern that opens with an unbounded run (an email's local part, a
|
|
314
|
+
* word before a comma, the whitespace after a line start), a global search
|
|
315
|
+
* that fails at one start moves one character right and scans the same run
|
|
316
|
+
* again: quadratic in the run's length. Every later start inside such a run
|
|
317
|
+
* shares the run's end and reads the same text after it, so it fails too;
|
|
318
|
+
* `failedTo` names where that stops being true, and the matches are the
|
|
319
|
+
* global pattern's, start for start. After a match the next try is at its
|
|
320
|
+
* end (one further for an empty match), as `lastIndex` would be.
|
|
321
|
+
*/
|
|
322
|
+
export function* matchAllSkipping(text, pattern, canStart, failedTo) {
|
|
323
|
+
let flags = pattern.flags.replace("g", "");
|
|
324
|
+
if (!flags.includes("d"))
|
|
325
|
+
flags = `${flags}d`;
|
|
326
|
+
if (!flags.includes("y"))
|
|
327
|
+
flags = `${flags}y`;
|
|
328
|
+
const re = new RegExp(pattern.source, flags);
|
|
329
|
+
let at = 0;
|
|
330
|
+
while (at <= text.length) {
|
|
331
|
+
if (!canStart(text, at)) {
|
|
332
|
+
at++;
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
re.lastIndex = at;
|
|
336
|
+
const m = re.exec(text);
|
|
337
|
+
if (m !== null) {
|
|
338
|
+
yield { index: at, match: m };
|
|
339
|
+
at += Math.max(1, m[0].length);
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
at = Math.max(at + 1, failedTo(text, at));
|
|
343
|
+
}
|
|
344
|
+
}
|
|
176
345
|
/**
|
|
177
346
|
* The absolute `[start, end)` of one capture group of a match produced by
|
|
178
347
|
* `matchAll`, or `undefined` when the group did not participate.
|
package/dist/paths.d.ts
CHANGED
|
@@ -1,10 +1,29 @@
|
|
|
1
1
|
import { CrewhausError } from "@crewhaus/errors";
|
|
2
|
-
/**
|
|
2
|
+
/**
|
|
3
|
+
* The longest path this resolves, in UTF-16 code units; a longer one is
|
|
4
|
+
* refused before it is walked. Nothing that long can be opened or created:
|
|
5
|
+
* PATH_MAX is 1,024 bytes on macOS and 4,096 on Linux, Windows stops at
|
|
6
|
+
* 32,767 UTF-16 units, and a path's UTF-8 form is never shorter than its
|
|
7
|
+
* UTF-16 one. The limit is twice that because the part of a path that exists
|
|
8
|
+
* is itself under PATH_MAX, so the part that does not is over PATH_MAX on its
|
|
9
|
+
* own: no spelling of the place reaches it, not even one through a symlink
|
|
10
|
+
* that shortens the part that exists.
|
|
11
|
+
*/
|
|
12
|
+
export declare const MAX_PATH_CHARS: number;
|
|
13
|
+
/**
|
|
14
|
+
* Raised when a caller-supplied path resolves outside the workspace root, or
|
|
15
|
+
* is longer than any path can be.
|
|
16
|
+
*/
|
|
3
17
|
export declare class ToolPermissionError extends CrewhausError {
|
|
4
18
|
readonly name = "ToolPermissionError";
|
|
5
19
|
readonly toolName: string;
|
|
6
20
|
readonly path: string;
|
|
7
|
-
|
|
21
|
+
/**
|
|
22
|
+
* Why, when it is not that the location escapes the workspace root: a path
|
|
23
|
+
* longer than any path can be. A caller that rewords the refusal says this.
|
|
24
|
+
*/
|
|
25
|
+
readonly reason: string | undefined;
|
|
26
|
+
constructor(toolName: string, attemptedPath: string, reason?: string);
|
|
8
27
|
}
|
|
9
28
|
/**
|
|
10
29
|
* A validated path, in both forms the tools need.
|
package/dist/paths.js
CHANGED
|
@@ -11,15 +11,45 @@
|
|
|
11
11
|
import { lstatSync, readlinkSync, realpathSync } from "node:fs";
|
|
12
12
|
import * as path from "node:path";
|
|
13
13
|
import { CrewhausError } from "@crewhaus/errors";
|
|
14
|
-
/**
|
|
14
|
+
/**
|
|
15
|
+
* The longest path this resolves, in UTF-16 code units; a longer one is
|
|
16
|
+
* refused before it is walked. Nothing that long can be opened or created:
|
|
17
|
+
* PATH_MAX is 1,024 bytes on macOS and 4,096 on Linux, Windows stops at
|
|
18
|
+
* 32,767 UTF-16 units, and a path's UTF-8 form is never shorter than its
|
|
19
|
+
* UTF-16 one. The limit is twice that because the part of a path that exists
|
|
20
|
+
* is itself under PATH_MAX, so the part that does not is over PATH_MAX on its
|
|
21
|
+
* own: no spelling of the place reaches it, not even one through a symlink
|
|
22
|
+
* that shortens the part that exists.
|
|
23
|
+
*/
|
|
24
|
+
export const MAX_PATH_CHARS = 2 * (process.platform === "darwin" ? 1_024 : process.platform === "win32" ? 32_767 : 4_096);
|
|
25
|
+
/** A path as a refusal quotes it: whole, unless it is past {@link MAX_PATH_CHARS}. */
|
|
26
|
+
function shownPath(given) {
|
|
27
|
+
return given.length > MAX_PATH_CHARS ? `${given.slice(0, 64)}…` : given;
|
|
28
|
+
}
|
|
29
|
+
/** Why a path past {@link MAX_PATH_CHARS} is refused: its length and the limit. */
|
|
30
|
+
function tooLong(abs) {
|
|
31
|
+
return `it is ${abs.length} characters once resolved, over the ${MAX_PATH_CHARS}-character limit: no path that long can be opened or created`;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Raised when a caller-supplied path resolves outside the workspace root, or
|
|
35
|
+
* is longer than any path can be.
|
|
36
|
+
*/
|
|
15
37
|
export class ToolPermissionError extends CrewhausError {
|
|
16
38
|
name = "ToolPermissionError";
|
|
17
39
|
toolName;
|
|
18
40
|
path;
|
|
19
|
-
|
|
20
|
-
|
|
41
|
+
/**
|
|
42
|
+
* Why, when it is not that the location escapes the workspace root: a path
|
|
43
|
+
* longer than any path can be. A caller that rewords the refusal says this.
|
|
44
|
+
*/
|
|
45
|
+
reason;
|
|
46
|
+
constructor(toolName, attemptedPath, reason) {
|
|
47
|
+
super("tool", reason === undefined
|
|
48
|
+
? `tool "${toolName}" rejected path "${attemptedPath}": resolved location escapes the workspace root`
|
|
49
|
+
: `tool "${toolName}" rejected path "${shownPath(attemptedPath)}": ${reason}`);
|
|
21
50
|
this.toolName = toolName;
|
|
22
51
|
this.path = attemptedPath;
|
|
52
|
+
this.reason = reason;
|
|
23
53
|
}
|
|
24
54
|
}
|
|
25
55
|
/** The workspace root, resolved. Every path is contained within it. */
|
|
@@ -67,15 +97,7 @@ function nameExists(p) {
|
|
|
67
97
|
function resolveLocation(target, depth = 0) {
|
|
68
98
|
if (depth > 40)
|
|
69
99
|
throw new Error(`symlink chain at "${target}" is too long to resolve`);
|
|
70
|
-
|
|
71
|
-
const tail = [];
|
|
72
|
-
while (!nameExists(probe)) {
|
|
73
|
-
tail.unshift(path.basename(probe));
|
|
74
|
-
const parent = path.dirname(probe);
|
|
75
|
-
if (parent === probe)
|
|
76
|
-
break; // reached the filesystem root
|
|
77
|
-
probe = parent;
|
|
78
|
-
}
|
|
100
|
+
const { probe, tail } = deepestName(target);
|
|
79
101
|
let probeReal;
|
|
80
102
|
try {
|
|
81
103
|
probeReal = realpathSync(probe);
|
|
@@ -97,7 +119,41 @@ function resolveLocation(target, depth = 0) {
|
|
|
97
119
|
const base = realpathSync(path.dirname(probe));
|
|
98
120
|
probeReal = resolveLocation(path.resolve(base, link), depth + 1);
|
|
99
121
|
}
|
|
100
|
-
|
|
122
|
+
// One string, not one argument per name: Bun's `path.join` keeps only
|
|
123
|
+
// (argument count mod 65,536) of its arguments.
|
|
124
|
+
return tail.length > 0 ? path.join(probeReal, tail) : probeReal;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* `target`, an absolute path, split where its deepest prefix that exists as
|
|
128
|
+
* a NAME ends: that prefix, and the names below it, which do not exist, as
|
|
129
|
+
* one string ("" when `target` itself exists). This is `path.dirname` and
|
|
130
|
+
* `path.basename` worked by index on `target` itself: handing them each
|
|
131
|
+
* shorter prefix in turn copied the rest of the path once per missing name,
|
|
132
|
+
* the square of a count the caller picks.
|
|
133
|
+
*/
|
|
134
|
+
function deepestName(target) {
|
|
135
|
+
const top = path.parse(target).root.length;
|
|
136
|
+
const isSep = (i) => target[i] === "/" || target[i] === path.sep;
|
|
137
|
+
let probe = target;
|
|
138
|
+
let start = target.length;
|
|
139
|
+
let stop = -1;
|
|
140
|
+
while (!nameExists(probe)) {
|
|
141
|
+
// `path.basename(probe)` is `target.slice(start, end)`, and
|
|
142
|
+
// `path.dirname(probe)` is `target.slice(0, up)`.
|
|
143
|
+
let end = probe.length;
|
|
144
|
+
while (end > top && isSep(end - 1))
|
|
145
|
+
end--;
|
|
146
|
+
if (stop < 0)
|
|
147
|
+
stop = end;
|
|
148
|
+
start = end;
|
|
149
|
+
while (start > top && !isSep(start - 1))
|
|
150
|
+
start--;
|
|
151
|
+
const up = start > top ? start - 1 : top;
|
|
152
|
+
if (up === probe.length)
|
|
153
|
+
break; // reached the filesystem root
|
|
154
|
+
probe = target.slice(0, up);
|
|
155
|
+
}
|
|
156
|
+
return { probe, tail: stop < 0 ? "" : target.slice(start, stop) };
|
|
101
157
|
}
|
|
102
158
|
export function resolveSafe(toolName, rel, root = workspaceRoot()) {
|
|
103
159
|
const rootResolved = path.resolve(root);
|
|
@@ -107,6 +163,9 @@ export function resolveSafe(toolName, rel, root = workspaceRoot()) {
|
|
|
107
163
|
if (abs !== rootResolved && !abs.startsWith(`${rootResolved}${path.sep}`)) {
|
|
108
164
|
throw new ToolPermissionError(toolName, rel);
|
|
109
165
|
}
|
|
166
|
+
// Longer than any path can be: refused before the walk below reads it.
|
|
167
|
+
if (abs.length > MAX_PATH_CHARS)
|
|
168
|
+
throw new ToolPermissionError(toolName, rel, tooLong(abs));
|
|
110
169
|
let real;
|
|
111
170
|
try {
|
|
112
171
|
const rootReal = realpathSync(rootResolved);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crewhaus/tool-secure",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Deterministic security tools: PII detection and redaction, secret scanning, prompt-injection heuristics, policy checks and evidence hashing",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -8,19 +8,22 @@
|
|
|
8
8
|
"exports": {
|
|
9
9
|
".": {
|
|
10
10
|
"types": "./dist/index.d.ts",
|
|
11
|
-
"import": "./dist/index.js"
|
|
11
|
+
"import": "./dist/index.js",
|
|
12
|
+
"default": "./dist/index.js"
|
|
12
13
|
}
|
|
13
14
|
},
|
|
14
15
|
"scripts": {
|
|
15
16
|
"test": "bun test src"
|
|
16
17
|
},
|
|
17
18
|
"dependencies": {
|
|
18
|
-
"@crewhaus/errors": "0.
|
|
19
|
-
"@crewhaus/tool-builder": "0.
|
|
20
|
-
"@crewhaus/tool-catalog": "0.
|
|
19
|
+
"@crewhaus/errors": "0.8.0",
|
|
20
|
+
"@crewhaus/tool-builder": "0.8.0",
|
|
21
|
+
"@crewhaus/tool-catalog": "0.8.0",
|
|
22
|
+
"@crewhaus/tool-safety": "0.8.0",
|
|
21
23
|
"zod": "^3.23.8"
|
|
22
24
|
},
|
|
23
25
|
"devDependencies": {
|
|
26
|
+
"@crewhaus/tool-categories": "workspace:*",
|
|
24
27
|
"@crewhaus/tool-executor": "workspace:*"
|
|
25
28
|
},
|
|
26
29
|
"license": "Apache-2.0",
|
|
@@ -41,5 +44,8 @@
|
|
|
41
44
|
"publishConfig": {
|
|
42
45
|
"access": "public"
|
|
43
46
|
},
|
|
44
|
-
"files": ["dist", "README.md", "LICENSE", "NOTICE"]
|
|
47
|
+
"files": ["dist", "README.md", "LICENSE", "NOTICE"],
|
|
48
|
+
"engines": {
|
|
49
|
+
"bun": ">=1.3.14"
|
|
50
|
+
}
|
|
45
51
|
}
|