@crewhaus/tool-money 0.7.0 → 0.8.1
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 +26 -5
- package/dist/index.js +75 -22
- package/dist/lib/allocate.d.ts +7 -0
- package/dist/lib/allocate.js +95 -55
- package/dist/lib/coding.d.ts +46 -4
- package/dist/lib/coding.js +221 -40
- package/dist/lib/controls.d.ts +5 -2
- package/dist/lib/controls.js +21 -14
- package/dist/lib/exact.d.ts +68 -0
- package/dist/lib/exact.js +122 -0
- package/dist/lib/lots.d.ts +7 -0
- package/dist/lib/lots.js +141 -45
- package/dist/lib/match.d.ts +5 -1
- package/dist/lib/match.js +122 -20
- package/dist/lib/statement.d.ts +44 -5
- package/dist/lib/statement.js +205 -106
- package/dist/lib/tax.d.ts +7 -1
- package/dist/lib/tax.js +60 -42
- package/dist/paths.d.ts +21 -2
- package/dist/paths.js +72 -13
- package/package.json +12 -7
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-money",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Deterministic money tools: payment identifier checksums, tax, refund allocation, three-way match, cost basis, spend controls and statement parsing",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -8,17 +8,19 @@
|
|
|
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.
|
|
21
|
-
"@crewhaus/tool-
|
|
19
|
+
"@crewhaus/errors": "0.8.1",
|
|
20
|
+
"@crewhaus/tool-builder": "0.8.1",
|
|
21
|
+
"@crewhaus/tool-catalog": "0.8.1",
|
|
22
|
+
"@crewhaus/tool-safety": "0.8.1",
|
|
23
|
+
"@crewhaus/tool-schema": "0.8.1",
|
|
22
24
|
"zod": "^3.23.8"
|
|
23
25
|
},
|
|
24
26
|
"devDependencies": {
|
|
@@ -42,5 +44,8 @@
|
|
|
42
44
|
"publishConfig": {
|
|
43
45
|
"access": "public"
|
|
44
46
|
},
|
|
45
|
-
"files": ["dist", "README.md", "LICENSE", "NOTICE"]
|
|
47
|
+
"files": ["dist", "README.md", "LICENSE", "NOTICE"],
|
|
48
|
+
"engines": {
|
|
49
|
+
"bun": ">=1.3.14"
|
|
50
|
+
}
|
|
46
51
|
}
|