anhedral 0.8.5 → 0.9.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/README.md +140 -34
- package/assets/anhedral-cli-init-technical.svg +8 -8
- package/assets/anhedral-cli-init.svg +9 -9
- package/assets/security/LICENSES.txt +63 -0
- package/assets/security/audit-deps.mjs +295 -0
- package/assets/security/braces-regression.mjs +102 -0
- package/assets/security/braces@3.0.3.patch +285 -0
- package/assets/security/decode-uri-component@0.2.2.patch +149 -0
- package/assets/security/decoder-regression.mjs +37 -0
- package/assets/security/forge-regression.mjs +46 -0
- package/assets/security/manifest.json +53 -0
- package/assets/security/node-forge@1.4.0.patch +26 -0
- package/dist/access.d.ts +10 -0
- package/dist/access.js +93 -0
- package/dist/auth-schema-snapshot.d.ts +346 -0
- package/dist/auth-schema-snapshot.js +347 -0
- package/dist/auth-schema.d.ts +1 -0
- package/dist/auth-schema.js +57 -0
- package/dist/bin.js +353 -10
- package/dist/bounded-file.d.ts +2 -0
- package/dist/bounded-file.js +30 -0
- package/dist/capabilities.d.ts +19 -33
- package/dist/capabilities.js +73 -71
- package/dist/cloudflare.d.ts +14 -0
- package/dist/cloudflare.js +66 -0
- package/dist/command.js +12 -3
- package/dist/delivery.d.ts +118 -0
- package/dist/delivery.js +611 -0
- package/dist/dependencies.d.ts +22 -0
- package/dist/dependencies.js +109 -67
- package/dist/dependency-order.d.ts +2 -0
- package/dist/dependency-order.js +24 -0
- package/dist/index.d.ts +18 -1
- package/dist/index.js +12 -1
- package/dist/managed-hosting.d.ts +49 -0
- package/dist/managed-hosting.js +373 -0
- package/dist/management.d.ts +3 -0
- package/dist/management.js +47 -0
- package/dist/platforms/desktop.d.ts +2 -2
- package/dist/platforms/desktop.js +22 -12
- package/dist/platforms/extension.d.ts +2 -2
- package/dist/platforms/extension.js +12 -4
- package/dist/platforms/mobile.d.ts +2 -2
- package/dist/platforms/mobile.js +13 -7
- package/dist/pnpm-policy.d.ts +4 -0
- package/dist/pnpm-policy.js +66 -0
- package/dist/project-state.d.ts +28 -0
- package/dist/project-state.js +226 -0
- package/dist/provider-readiness.d.ts +30 -0
- package/dist/provider-readiness.js +87 -0
- package/dist/provision.d.ts +46 -0
- package/dist/provision.js +197 -0
- package/dist/readiness.d.ts +4 -1
- package/dist/readiness.js +80 -8
- package/dist/registry/add.d.ts +22 -0
- package/dist/registry/add.js +198 -0
- package/dist/registry/bootstrap.d.ts +2 -0
- package/dist/registry/bootstrap.js +14 -0
- package/dist/registry/commands.d.ts +2 -0
- package/dist/registry/commands.js +99 -0
- package/dist/registry/context.d.ts +3 -0
- package/dist/registry/context.js +21 -0
- package/dist/registry/options.d.ts +2 -0
- package/dist/registry/options.js +65 -0
- package/dist/registry/receipt.d.ts +3 -0
- package/dist/registry/receipt.js +73 -0
- package/dist/registry/remote.d.ts +25 -0
- package/dist/registry/remote.js +168 -0
- package/dist/registry/resolve.d.ts +10 -0
- package/dist/registry/resolve.js +98 -0
- package/dist/registry/types.d.ts +64 -0
- package/dist/registry/usage.d.ts +1 -0
- package/dist/registry/usage.js +38 -0
- package/dist/render.js +3 -3
- package/dist/repository.d.ts +24 -0
- package/dist/repository.js +96 -0
- package/dist/scaffold/api.d.ts +2 -0
- package/dist/scaffold/api.js +13 -0
- package/dist/scaffold/auth.d.ts +3 -0
- package/dist/scaffold/auth.js +95 -0
- package/dist/scaffold/bootstrap.d.ts +2 -0
- package/dist/scaffold/bootstrap.js +34 -0
- package/dist/scaffold/compose.d.ts +4 -0
- package/dist/scaffold/compose.js +34 -0
- package/dist/scaffold/database.d.ts +2 -0
- package/dist/scaffold/database.js +43 -0
- package/dist/scaffold/documentation.d.ts +3 -0
- package/dist/scaffold/documentation.js +91 -0
- package/dist/scaffold/environment.d.ts +2 -0
- package/dist/scaffold/environment.js +5 -0
- package/dist/scaffold/files.d.ts +25 -0
- package/dist/scaffold/files.js +108 -0
- package/dist/scaffold/integrations.d.ts +3 -0
- package/dist/scaffold/integrations.js +7 -0
- package/dist/scaffold/services.d.ts +5 -0
- package/dist/scaffold/services.js +84 -0
- package/dist/scaffold/web.d.ts +2 -0
- package/dist/scaffold/web.js +102 -0
- package/dist/scaffold/workspace.d.ts +3 -0
- package/dist/scaffold/workspace.js +150 -0
- package/dist/security-backports.d.ts +3 -0
- package/dist/security-backports.js +26 -0
- package/dist/stack-registry.d.ts +80 -0
- package/dist/stack-registry.js +424 -0
- package/dist/standard.d.ts +6 -17
- package/dist/standard.js +41 -585
- package/dist/transaction.js +46 -26
- package/dist/util.js +12 -2
- package/dist/vercel.d.ts +98 -0
- package/dist/vercel.js +796 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/vinext.d.ts +1 -0
- package/dist/vinext.js +83 -0
- package/dist/workspace-graph.d.ts +7 -0
- package/dist/workspace-graph.js +73 -0
- package/docs/application-stack-standard.md +75 -674
- package/docs/delivery-workflow.md +83 -0
- package/docs/managed-hosting-architecture.md +87 -0
- package/docs/sponsorships.md +25 -0
- package/package.json +23 -28
- package/dist/project.d.ts +0 -4
- package/dist/standard-products.d.ts +0 -2
- package/dist/standard-products.js +0 -6
- /package/assets/{images/svg/logo-white-subtract.svg → anhedral-mark.svg} +0 -0
- /package/dist/{project.js → registry/types.js} +0 -0
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
diff --git a/index.js b/index.js
|
|
2
|
+
--- a/index.js
|
|
3
|
+
+++ b/index.js
|
|
4
|
+
@@ -1,4 +1,6 @@
|
|
5
|
+
'use strict';
|
|
6
|
+
+
|
|
7
|
+
+const limits = require('./lib/limits');
|
|
8
|
+
|
|
9
|
+
const stringify = require('./lib/stringify');
|
|
10
|
+
const compile = require('./lib/compile');
|
|
11
|
+
@@ -23,11 +25,18 @@
|
|
12
|
+
let output = [];
|
|
13
|
+
|
|
14
|
+
if (Array.isArray(input)) {
|
|
15
|
+
+ if (input.length > limits.MAX_RESULTS) limits.fail();
|
|
16
|
+
+ let chars = 0;
|
|
17
|
+
for (const pattern of input) {
|
|
18
|
+
const result = braces.create(pattern, options);
|
|
19
|
+
if (Array.isArray(result)) {
|
|
20
|
+
- output.push(...result);
|
|
21
|
+
+ const size = limits.measure(result);
|
|
22
|
+
+ chars += size.chars;
|
|
23
|
+
+ if (output.length + size.count > limits.MAX_RESULTS || chars > limits.MAX_CHARS) limits.fail();
|
|
24
|
+
+ for (const value of result) output.push(value);
|
|
25
|
+
} else {
|
|
26
|
+
+ chars += String(result).length;
|
|
27
|
+
+ if (output.length >= limits.MAX_RESULTS || chars > limits.MAX_CHARS) limits.fail();
|
|
28
|
+
output.push(result);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
diff --git a/lib/compile.js b/lib/compile.js
|
|
32
|
+
--- a/lib/compile.js
|
|
33
|
+
+++ b/lib/compile.js
|
|
34
|
+
@@ -1,9 +1,12 @@
|
|
35
|
+
'use strict';
|
|
36
|
+
+
|
|
37
|
+
+const limits = require('./limits');
|
|
38
|
+
|
|
39
|
+
const fill = require('fill-range');
|
|
40
|
+
const utils = require('./utils');
|
|
41
|
+
|
|
42
|
+
const compile = (ast, options = {}) => {
|
|
43
|
+
+ limits.ast(ast);
|
|
44
|
+
const walk = (node, parent = {}) => {
|
|
45
|
+
const invalidBlock = utils.isInvalidBrace(parent);
|
|
46
|
+
const invalidNode = node.invalid === true && options.escapeInvalid === true;
|
|
47
|
+
@@ -38,6 +41,7 @@
|
|
48
|
+
|
|
49
|
+
if (node.nodes && node.ranges > 0) {
|
|
50
|
+
const args = utils.reduce(node.nodes);
|
|
51
|
+
+ limits.range(args, options, true);
|
|
52
|
+
const range = fill(...args, { ...options, wrap: false, toRegex: true, strictZeros: true });
|
|
53
|
+
|
|
54
|
+
if (range.length !== 0) {
|
|
55
|
+
@@ -48,13 +52,14 @@
|
|
56
|
+
if (node.nodes) {
|
|
57
|
+
for (const child of node.nodes) {
|
|
58
|
+
output += walk(child, node);
|
|
59
|
+
+ limits.string(output);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return output;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
- return walk(ast);
|
|
67
|
+
+ return limits.string(walk(ast));
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
module.exports = compile;
|
|
71
|
+
diff --git a/lib/expand.js b/lib/expand.js
|
|
72
|
+
--- a/lib/expand.js
|
|
73
|
+
+++ b/lib/expand.js
|
|
74
|
+
@@ -1,4 +1,6 @@
|
|
75
|
+
'use strict';
|
|
76
|
+
+
|
|
77
|
+
+const limits = require('./limits');
|
|
78
|
+
|
|
79
|
+
const fill = require('fill-range');
|
|
80
|
+
const stringify = require('./stringify');
|
|
81
|
+
@@ -9,6 +11,7 @@
|
|
82
|
+
|
|
83
|
+
queue = [].concat(queue);
|
|
84
|
+
stash = [].concat(stash);
|
|
85
|
+
+ limits.append(queue, stash, enclose);
|
|
86
|
+
|
|
87
|
+
if (!stash.length) return queue;
|
|
88
|
+
if (!queue.length) {
|
|
89
|
+
@@ -31,6 +34,7 @@
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const expand = (ast, options = {}) => {
|
|
93
|
+
+ limits.ast(ast);
|
|
94
|
+
const rangeLimit = options.rangeLimit === undefined ? 1000 : options.rangeLimit;
|
|
95
|
+
|
|
96
|
+
const walk = (node, parent = {}) => {
|
|
97
|
+
@@ -61,6 +65,7 @@
|
|
98
|
+
throw new RangeError('expanded array length exceeds range limit. Use options.rangeLimit to increase or disable the limit.');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
+ limits.range(args, options);
|
|
102
|
+
let range = fill(...args, options);
|
|
103
|
+
if (range.length === 0) {
|
|
104
|
+
range = stringify(node, options);
|
|
105
|
+
@@ -107,7 +112,9 @@
|
|
106
|
+
return queue;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
- return utils.flatten(walk(ast));
|
|
110
|
+
+ const result = utils.flatten(walk(ast));
|
|
111
|
+
+ limits.measure(result);
|
|
112
|
+
+ return result;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
module.exports = expand;
|
|
116
|
+
diff --git a/lib/limits.js b/lib/limits.js
|
|
117
|
+
new file mode 100644
|
|
118
|
+
--- /dev/null
|
|
119
|
+
+++ b/lib/limits.js
|
|
120
|
+
@@ -0,0 +1,102 @@
|
|
121
|
+
+'use strict';
|
|
122
|
+
+
|
|
123
|
+
+// Anhedral's local 3.0.3 security backport. Limits cannot be disabled by options.
|
|
124
|
+
+const MAX_DEPTH = 100;
|
|
125
|
+
+const MAX_NODES = 20000;
|
|
126
|
+
+const MAX_RESULTS = 100000;
|
|
127
|
+
+const MAX_CHARS = 10 * 1024 * 1024;
|
|
128
|
+
+const fail = () => { throw new RangeError('Brace operation exceeds safe resource limits'); };
|
|
129
|
+
+const string = value => {
|
|
130
|
+
+ if (String(value).length > MAX_CHARS) fail();
|
|
131
|
+
+ return value;
|
|
132
|
+
+};
|
|
133
|
+
+
|
|
134
|
+
+const ast = root => {
|
|
135
|
+
+ const work = [[root, 0, false]];
|
|
136
|
+
+ const active = new Set();
|
|
137
|
+
+ const parentDepths = new Map();
|
|
138
|
+
+ let count = 0;
|
|
139
|
+
+ let chars = 0;
|
|
140
|
+
+ while (work.length) {
|
|
141
|
+
+ const [node, depth, leaving] = work.pop();
|
|
142
|
+
+ if (leaving) { active.delete(node); continue; }
|
|
143
|
+
+ if (!node || (typeof node !== 'object' && typeof node !== 'function')) throw new TypeError('Expected an AST node');
|
|
144
|
+
+ if (depth > MAX_DEPTH || ++count > MAX_NODES || active.has(node)) fail();
|
|
145
|
+
+ chars += node.value ? String(node.value).length : 0;
|
|
146
|
+
+ if (chars > MAX_CHARS) fail();
|
|
147
|
+
+ // expand follows parent links independently of the child traversal.
|
|
148
|
+
+ const parents = new Set();
|
|
149
|
+
+ let parent = node;
|
|
150
|
+
+ while (parent && !parentDepths.has(parent)) {
|
|
151
|
+
+ if (parents.has(parent) || parents.size > MAX_DEPTH) fail();
|
|
152
|
+
+ if (parentDepths.size + parents.size >= MAX_NODES) fail();
|
|
153
|
+
+ parents.add(parent);
|
|
154
|
+
+ parent = parent.parent;
|
|
155
|
+
+ }
|
|
156
|
+
+ let parentDepth = parent ? parentDepths.get(parent) : 0;
|
|
157
|
+
+ for (const value of [...parents].reverse()) {
|
|
158
|
+
+ if (++parentDepth > MAX_DEPTH + 1) fail();
|
|
159
|
+
+ parentDepths.set(value, parentDepth);
|
|
160
|
+
+ }
|
|
161
|
+
+ if (node.nodes) {
|
|
162
|
+
+ if (!Array.isArray(node.nodes)) throw new TypeError('Expected AST children');
|
|
163
|
+
+ if (node.nodes.length > MAX_NODES || work.length + node.nodes.length > MAX_NODES * 2) fail();
|
|
164
|
+
+ active.add(node);
|
|
165
|
+
+ work.push([node, depth, true]);
|
|
166
|
+
+ for (let i = node.nodes.length - 1; i >= 0; i--) work.push([node.nodes[i], depth + 1, false]);
|
|
167
|
+
+ }
|
|
168
|
+
+ }
|
|
169
|
+
+};
|
|
170
|
+
+
|
|
171
|
+
+const measure = value => {
|
|
172
|
+
+ const work = [[value, 0, false]];
|
|
173
|
+
+ const active = new Set();
|
|
174
|
+
+ let count = 0;
|
|
175
|
+
+ let chars = 0;
|
|
176
|
+
+ let visits = 0;
|
|
177
|
+
+ while (work.length) {
|
|
178
|
+
+ const [item, depth, leaving] = work.pop();
|
|
179
|
+
+ if (leaving) { active.delete(item); continue; }
|
|
180
|
+
+ if (depth > MAX_DEPTH || ++visits > MAX_RESULTS + MAX_NODES) fail();
|
|
181
|
+
+ if (Array.isArray(item)) {
|
|
182
|
+
+ if (active.has(item) || item.length > MAX_RESULTS || work.length + item.length > MAX_RESULTS + MAX_NODES) fail();
|
|
183
|
+
+ active.add(item);
|
|
184
|
+
+ work.push([item, depth, true]);
|
|
185
|
+
+ for (let i = item.length - 1; i >= 0; i--) work.push([item[i], depth + 1, false]);
|
|
186
|
+
+ } else if (item !== undefined) {
|
|
187
|
+
+ count++;
|
|
188
|
+
+ chars += String(item).length;
|
|
189
|
+
+ if (count > MAX_RESULTS || chars > MAX_CHARS) fail();
|
|
190
|
+
+ }
|
|
191
|
+
+ }
|
|
192
|
+
+ return { count, chars };
|
|
193
|
+
+};
|
|
194
|
+
+
|
|
195
|
+
+const append = (queue, stash, enclose) => {
|
|
196
|
+
+ const left = measure(queue);
|
|
197
|
+
+ const right = measure(stash);
|
|
198
|
+
+ const count = left.count ? left.count * right.count : right.count;
|
|
199
|
+
+ const chars = left.count ? left.chars * right.count + right.chars * left.count : right.chars;
|
|
200
|
+
+ if (count > MAX_RESULTS || chars + (enclose ? count * 2 : 0) > MAX_CHARS) fail();
|
|
201
|
+
+};
|
|
202
|
+
+
|
|
203
|
+
+// fill-range enumerates descending ranges too; rangeLimit:false must not bypass the cap.
|
|
204
|
+
+const range = (args, options, compiling = false) => {
|
|
205
|
+
+ const [start, end] = args;
|
|
206
|
+
+ const step = Math.max(Math.abs(Number(args[2] === undefined ? options.step || 1 : args[2])), 1);
|
|
207
|
+
+ if (compiling && step === 1) return; // Numeric regex ranges do not allocate each member.
|
|
208
|
+
+ let a = Number(start);
|
|
209
|
+
+ let b = Number(end);
|
|
210
|
+
+ if (!Number.isInteger(a) || !Number.isInteger(b)) {
|
|
211
|
+
+ if (String(start).length !== 1 || String(end).length !== 1) return;
|
|
212
|
+
+ a = String(start).charCodeAt(0);
|
|
213
|
+
+ b = String(end).charCodeAt(0);
|
|
214
|
+
+ }
|
|
215
|
+
+ if (!Number.isSafeInteger(a) || !Number.isSafeInteger(b)) fail();
|
|
216
|
+
+ if (!Number.isFinite(step)) return; // fill-range handles invalid steps.
|
|
217
|
+
+ const count = Math.floor(Math.abs(b - a) / step) + 1;
|
|
218
|
+
+ const width = Math.max(String(start).length, String(end).length, String(args[2] || '').length, 1);
|
|
219
|
+
+ if (count > MAX_RESULTS || count * (width + (compiling ? 2 : 0)) > MAX_CHARS) fail();
|
|
220
|
+
+};
|
|
221
|
+
+
|
|
222
|
+
+module.exports = { MAX_DEPTH, MAX_RESULTS, MAX_CHARS, fail, string, ast, measure, append, range };
|
|
223
|
+
diff --git a/lib/parse.js b/lib/parse.js
|
|
224
|
+
--- a/lib/parse.js
|
|
225
|
+
+++ b/lib/parse.js
|
|
226
|
+
@@ -1,4 +1,6 @@
|
|
227
|
+
'use strict';
|
|
228
|
+
+
|
|
229
|
+
+const limits = require('./limits');
|
|
230
|
+
|
|
231
|
+
const stringify = require('./stringify');
|
|
232
|
+
|
|
233
|
+
@@ -143,6 +145,7 @@
|
|
234
|
+
*/
|
|
235
|
+
|
|
236
|
+
if (value === CHAR_LEFT_PARENTHESES) {
|
|
237
|
+
+ if (stack.length >= limits.MAX_DEPTH) limits.fail();
|
|
238
|
+
block = push({ type: 'paren', nodes: [] });
|
|
239
|
+
stack.push(block);
|
|
240
|
+
push({ type: 'text', value });
|
|
241
|
+
@@ -195,6 +198,7 @@
|
|
242
|
+
*/
|
|
243
|
+
|
|
244
|
+
if (value === CHAR_LEFT_CURLY_BRACE) {
|
|
245
|
+
+ if (stack.length >= limits.MAX_DEPTH) limits.fail();
|
|
246
|
+
depth++;
|
|
247
|
+
|
|
248
|
+
const dollar = prev.value && prev.value.slice(-1) === '$' || block.dollar === true;
|
|
249
|
+
@@ -325,6 +329,7 @@
|
|
250
|
+
} while (stack.length > 0);
|
|
251
|
+
|
|
252
|
+
push({ type: 'eos' });
|
|
253
|
+
+ limits.ast(ast);
|
|
254
|
+
return ast;
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
diff --git a/lib/stringify.js b/lib/stringify.js
|
|
258
|
+
--- a/lib/stringify.js
|
|
259
|
+
+++ b/lib/stringify.js
|
|
260
|
+
@@ -1,8 +1,11 @@
|
|
261
|
+
'use strict';
|
|
262
|
+
+
|
|
263
|
+
+const limits = require('./limits');
|
|
264
|
+
|
|
265
|
+
const utils = require('./utils');
|
|
266
|
+
|
|
267
|
+
module.exports = (ast, options = {}) => {
|
|
268
|
+
+ limits.ast(ast);
|
|
269
|
+
const stringify = (node, parent = {}) => {
|
|
270
|
+
const invalidBlock = options.escapeInvalid && utils.isInvalidBrace(parent);
|
|
271
|
+
const invalidNode = node.invalid === true && options.escapeInvalid === true;
|
|
272
|
+
@@ -22,11 +25,12 @@
|
|
273
|
+
if (node.nodes) {
|
|
274
|
+
for (const child of node.nodes) {
|
|
275
|
+
output += stringify(child);
|
|
276
|
+
+ limits.string(output);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return output;
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
- return stringify(ast);
|
|
283
|
+
+ return limits.string(stringify(ast));
|
|
284
|
+
};
|
|
285
|
+
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
diff --git a/index.js b/index.js
|
|
2
|
+
index 24e7db0cc55aa22d0f98358b73298067deb33b52..896c03cce865e681bc95ef0d0ed7d64b18488224 100644
|
|
3
|
+
--- a/index.js
|
|
4
|
+
+++ b/index.js
|
|
5
|
+
@@ -1,42 +1,122 @@
|
|
6
|
+
'use strict';
|
|
7
|
+
-var token = '%[a-f0-9]{2}';
|
|
8
|
+
-var singleMatcher = new RegExp('(' + token + ')|([^%]+?)', 'gi');
|
|
9
|
+
-var multiMatcher = new RegExp('(' + token + ')+', 'gi');
|
|
10
|
+
+// Matches one or more consecutive percent-encoded bytes (e.g. `%C3%A5`).
|
|
11
|
+
+const token = '%[a-f0-9]{2}';
|
|
12
|
+
+const multiMatcher = new RegExp(`(${token})+`, 'gi');
|
|
13
|
+
|
|
14
|
+
-function decodeComponents(components, split) {
|
|
15
|
+
- try {
|
|
16
|
+
- // Try to decode the entire string first
|
|
17
|
+
- return [decodeURIComponent(components.join(''))];
|
|
18
|
+
- } catch (err) {
|
|
19
|
+
- // Do nothing
|
|
20
|
+
+const hexPair = /^[a-f\d]{2}$/i;
|
|
21
|
+
+
|
|
22
|
+
+// Read a `%XX` sequence at `position`, returning the byte value and where to continue scanning.
|
|
23
|
+
+function parsePercentByte(input, position) {
|
|
24
|
+
+ if (input.codePointAt(position) !== 37 || position + 3 > input.length) {
|
|
25
|
+
+ return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
- if (components.length === 1) {
|
|
29
|
+
- return components;
|
|
30
|
+
+ const digits = input.slice(position + 1, position + 3);
|
|
31
|
+
+
|
|
32
|
+
+ if (!hexPair.test(digits)) {
|
|
33
|
+
+ return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
- split = split || 1;
|
|
37
|
+
+ return {byte: Number.parseInt(digits, 16), next: position + 3};
|
|
38
|
+
+}
|
|
39
|
+
+
|
|
40
|
+
+/**
|
|
41
|
+
+ * Return how many bytes a UTF-8 code point needs based on its lead byte.
|
|
42
|
+
+ * Returns 0 for continuation bytes and other invalid lead bytes.
|
|
43
|
+
+ */
|
|
44
|
+
+function utf8SequenceLength(byte) {
|
|
45
|
+
+ if (byte <= 0x7F) {
|
|
46
|
+
+ return 1;
|
|
47
|
+
+ }
|
|
48
|
+
|
|
49
|
+
- // Split the array in 2 parts
|
|
50
|
+
- var left = components.slice(0, split);
|
|
51
|
+
- var right = components.slice(split);
|
|
52
|
+
+ // Start at 0xC2 to exclude overlong 2-byte encodings (0xC0/0xC1).
|
|
53
|
+
+ if (byte >= 0xC2 && byte <= 0xDF) {
|
|
54
|
+
+ return 2;
|
|
55
|
+
+ }
|
|
56
|
+
|
|
57
|
+
- return Array.prototype.concat.call([], decodeComponents(left), decodeComponents(right));
|
|
58
|
+
+ if (byte >= 0xE0 && byte <= 0xEF) {
|
|
59
|
+
+ return 3;
|
|
60
|
+
+ }
|
|
61
|
+
+
|
|
62
|
+
+ if (byte >= 0xF0 && byte <= 0xF4) {
|
|
63
|
+
+ return 4;
|
|
64
|
+
+ }
|
|
65
|
+
+
|
|
66
|
+
+ return 0;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
+function isContinuationByte(byte) {
|
|
70
|
+
+ return byte >= 0x80 && byte <= 0xBF;
|
|
71
|
+
+}
|
|
72
|
+
+
|
|
73
|
+
+/**
|
|
74
|
+
+ * Decode as much of `input` as possible without throwing.
|
|
75
|
+
+ *Scans left-to-right in O(n), decoding valid UTF-8 runs and leaving the rest literal.
|
|
76
|
+
+ */
|
|
77
|
+
function decode(input) {
|
|
78
|
+
try {
|
|
79
|
+
return decodeURIComponent(input);
|
|
80
|
+
- } catch (err) {
|
|
81
|
+
- var tokens = input.match(singleMatcher) || [];
|
|
82
|
+
+ } catch {
|
|
83
|
+
+ let output = '';
|
|
84
|
+
+ let position = 0;
|
|
85
|
+
+
|
|
86
|
+
+ while (position < input.length) {
|
|
87
|
+
+ if (input.codePointAt(position) !== 37) {
|
|
88
|
+
+ output += input.charAt(position);
|
|
89
|
+
+ position++;
|
|
90
|
+
+ continue;
|
|
91
|
+
+ }
|
|
92
|
+
+
|
|
93
|
+
+ const firstByte = parsePercentByte(input, position);
|
|
94
|
+
+
|
|
95
|
+
+ // `%` not followed by two hex digits (e.g. `%` or `%G`).
|
|
96
|
+
+ if (!firstByte) {
|
|
97
|
+
+ output += input.charAt(position);
|
|
98
|
+
+ position++;
|
|
99
|
+
+ continue;
|
|
100
|
+
+ }
|
|
101
|
+
+
|
|
102
|
+
+ const sequenceLength = utf8SequenceLength(firstByte.byte);
|
|
103
|
+
+
|
|
104
|
+
+ // Continuation byte or invalid lead byte — emit one `%XX` literally.
|
|
105
|
+
+ if (sequenceLength === 0) {
|
|
106
|
+
+ output += input.slice(position, position + 3);
|
|
107
|
+
+ position += 3;
|
|
108
|
+
+ continue;
|
|
109
|
+
+ }
|
|
110
|
+
|
|
111
|
+
- for (var i = 1; i < tokens.length; i++) {
|
|
112
|
+
- input = decodeComponents(tokens, i).join('');
|
|
113
|
+
+ let end = firstByte.next;
|
|
114
|
+
+ let validSequence = true;
|
|
115
|
+
+
|
|
116
|
+
+ for (let index = 1; index < sequenceLength; index++) {
|
|
117
|
+
+ const nextByte = parsePercentByte(input, end);
|
|
118
|
+
+
|
|
119
|
+
+ if (!nextByte || !isContinuationByte(nextByte.byte)) {
|
|
120
|
+
+ validSequence = false;
|
|
121
|
+
+ break;
|
|
122
|
+
+ }
|
|
123
|
+
+
|
|
124
|
+
+ end = nextByte.next;
|
|
125
|
+
+ }
|
|
126
|
+
+
|
|
127
|
+
+ if (validSequence) {
|
|
128
|
+
+ const encodedSequence = input.slice(position, end);
|
|
129
|
+
+
|
|
130
|
+
+ try {
|
|
131
|
+
+ output += decodeURIComponent(encodedSequence);
|
|
132
|
+
+ position = end;
|
|
133
|
+
+ continue;
|
|
134
|
+
+ } catch {
|
|
135
|
+
+ // Invalid UTF-8 despite correct structure — emit the first byte literally.
|
|
136
|
+
+ }
|
|
137
|
+
+ }
|
|
138
|
+
|
|
139
|
+
- tokens = input.match(singleMatcher) || [];
|
|
140
|
+
+ // Missing continuation bytes or decode failure — emit the first byte literally.
|
|
141
|
+
+ output += input.slice(position, position + 3);
|
|
142
|
+
+ position += 3;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
- return input;
|
|
146
|
+
+ return output;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { execFileSync } from 'node:child_process';
|
|
3
|
+
import { createRequire } from 'node:module';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
|
|
6
|
+
// Upstream fa479da scanner backport retains the legacy CommonJS/plus API.
|
|
7
|
+
export async function verifyDecoder(packageRoot) {
|
|
8
|
+
const require = createRequire(join(packageRoot, 'package.json'));
|
|
9
|
+
const decode = require(join(packageRoot, 'index.js'));
|
|
10
|
+
assert.equal(typeof decode, 'function', 'Decoder CommonJS export changed');
|
|
11
|
+
for (const [input, expected] of [
|
|
12
|
+
['', ''], ['a+b%2Bc', 'a b+c'], ['%25', '%'], ['%2525', '%25'],
|
|
13
|
+
['st%C3%A5le%', 'ståle%'], ['%E4%BD%A0%E5%A5%BD', '你好'],
|
|
14
|
+
['%F0%9F%98%80', '😀'], ['%F4%8F%BF%BF', '\u{10FFFF}'],
|
|
15
|
+
['%C2', '\uFFFD'], ['%C2%B5%C2', 'µ\uFFFD'],
|
|
16
|
+
['%FE%FF', '\uFFFD\uFFFD'], ['%FF%FE', '\uFFFD\uFFFD'],
|
|
17
|
+
['%F0%9F%98', '%F0%9F%98'], ['%ED%A0%80', '%ED%A0%80'],
|
|
18
|
+
['%C0%AF', '%C0%AF'], ['%F4%90%80%80', '%F4%90%80%80'],
|
|
19
|
+
['%C3%5A%A5', '%C3Z%A5'], ['%G0%C3%A5%ab', '%G0å%ab'],
|
|
20
|
+
['%84%D7%25%88%90', '%84%D7%%88%90'], ['%20%20%25%80', ' %%80'],
|
|
21
|
+
]) assert.equal(decode(input), expected, `Decoder compatibility failure: ${input}`);
|
|
22
|
+
for (const value of [undefined, null, 1, {}, [], Symbol('x')]) {
|
|
23
|
+
assert.throws(() => decode(value), TypeError);
|
|
24
|
+
}
|
|
25
|
+
// Execute hostile runs in an isolated, hard-bounded process: the old recursive
|
|
26
|
+
// fallback exhausts its stack or CPU; the scanner leaves invalid bytes literal.
|
|
27
|
+
const script = `
|
|
28
|
+
const assert = require('node:assert/strict');
|
|
29
|
+
const decode = require(${JSON.stringify(join(packageRoot, 'index.js'))});
|
|
30
|
+
assert.equal(decode('%AB'.repeat(10000)), '%AB'.repeat(10000));
|
|
31
|
+
assert.equal(decode('%C3%5A%A5'.repeat(10000)), '%C3Z%A5'.repeat(10000));
|
|
32
|
+
assert.equal(decode('%E0%80%80'.repeat(10000)), '%E0%80%80'.repeat(10000));
|
|
33
|
+
`;
|
|
34
|
+
execFileSync(process.execPath, ['--input-type=commonjs', '--eval', script], {
|
|
35
|
+
timeout: 5000, maxBuffer: 128 * 1024, stdio: 'pipe',
|
|
36
|
+
});
|
|
37
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { Buffer } from 'node:buffer';
|
|
3
|
+
import { createHash, generateKeyPairSync, privateEncrypt, constants } from 'node:crypto';
|
|
4
|
+
import { createRequire } from 'node:module';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
|
|
7
|
+
// PR1152 validates nested DigestAlgorithm children; Anhedral also enforces empty
|
|
8
|
+
// DER NULL parameters. Fixture keys never leave memory.
|
|
9
|
+
export async function verifyForge(packageRoot) {
|
|
10
|
+
const require = createRequire(join(packageRoot, 'package.json'));
|
|
11
|
+
const forge = require(join(packageRoot, 'lib/index.js'));
|
|
12
|
+
const { publicKey, privateKey } = generateKeyPairSync('rsa', {
|
|
13
|
+
modulusLength: 2048,
|
|
14
|
+
publicExponent: 65537,
|
|
15
|
+
});
|
|
16
|
+
const key = forge.pki.publicKeyFromPem(publicKey.export({ type: 'spki', format: 'pem' }));
|
|
17
|
+
const digest = createHash('sha256').update('Anhedral RSA validation fixture').digest();
|
|
18
|
+
const asn1 = forge.asn1;
|
|
19
|
+
const node = (type, value, constructed = false) =>
|
|
20
|
+
asn1.create(asn1.Class.UNIVERSAL, type, constructed, value);
|
|
21
|
+
const oid = () => node(asn1.Type.OID, asn1.oidToDer(forge.oids.sha256).getBytes());
|
|
22
|
+
const nil = (value = '') => node(asn1.Type.NULL, value);
|
|
23
|
+
const garbage = () => node(asn1.Type.OCTETSTRING, 'unconsumed');
|
|
24
|
+
const verify = (algorithm, extraOuter = []) => {
|
|
25
|
+
const info = node(asn1.Type.SEQUENCE, [
|
|
26
|
+
node(asn1.Type.SEQUENCE, algorithm, true),
|
|
27
|
+
node(asn1.Type.OCTETSTRING, digest.toString('binary')),
|
|
28
|
+
...extraOuter,
|
|
29
|
+
], true);
|
|
30
|
+
const der = Buffer.from(asn1.toDer(info).getBytes(), 'binary');
|
|
31
|
+
// Use normal PKCS#1 padding, never Forge's internal padding bypass options.
|
|
32
|
+
const encoded = Buffer.concat([
|
|
33
|
+
Buffer.from([0, 1]), Buffer.alloc(256 - der.length - 3, 255), Buffer.from([0]), der,
|
|
34
|
+
]);
|
|
35
|
+
const signature = privateEncrypt({ key: privateKey, padding: constants.RSA_NO_PADDING }, encoded);
|
|
36
|
+
return key.verify(digest.toString('binary'), signature.toString('binary'));
|
|
37
|
+
};
|
|
38
|
+
assert.equal(verify([oid(), nil()]), true, 'Valid RSA SHA-256 with NULL parameters rejected');
|
|
39
|
+
assert.equal(verify([oid()]), true, 'Valid RSA SHA-256 without parameters rejected');
|
|
40
|
+
for (const algorithm of [[oid(), nil(), garbage()], [oid(), garbage()], [oid(), nil('garbage')], [oid(), nil('\x00')]]) {
|
|
41
|
+
assert.throws(() => verify(algorithm), /valid RSASSA-PKCS1-v1_5 DigestInfo/,
|
|
42
|
+
'RSA verification accepted malformed DigestAlgorithm parameters');
|
|
43
|
+
}
|
|
44
|
+
assert.throws(() => verify([oid(), nil()], [garbage()]), /valid RSASSA-PKCS1-v1_5 DigestInfo/,
|
|
45
|
+
'RSA verification accepted an extra outer DigestInfo element');
|
|
46
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 1,
|
|
3
|
+
"packages": [
|
|
4
|
+
{
|
|
5
|
+
"name": "node-forge",
|
|
6
|
+
"version": "1.4.0",
|
|
7
|
+
"advisoryIds": [
|
|
8
|
+
"GHSA-86w9-cpqp-85rv"
|
|
9
|
+
],
|
|
10
|
+
"patch": "node-forge@1.4.0.patch",
|
|
11
|
+
"patchSha256": "1b5e41ae73d70d18d2541c50e67d5b408eda4cdd52a426c3d085ec17b8f9b2a9",
|
|
12
|
+
"installedTreeSha256": "bb76d231d6aba9cad2da5f1ec1dee54b64c6720c5fb0b0c6aa0119b661d541c0",
|
|
13
|
+
"provenanceUrls": [
|
|
14
|
+
"https://github.com/digitalbazaar/forge/pull/1152",
|
|
15
|
+
"https://github.com/digitalbazaar/forge/commit/ceba34402e329f0365134f23fe19898756527d65",
|
|
16
|
+
"https://github.com/advisories/GHSA-86w9-cpqp-85rv"
|
|
17
|
+
],
|
|
18
|
+
"origin": "Upstream PR1152 cardinality backport plus Anhedral strict DER NULL payload validation"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"name": "braces",
|
|
22
|
+
"version": "3.0.3",
|
|
23
|
+
"advisoryIds": [
|
|
24
|
+
"GHSA-vfj7-8cjw-p6xm"
|
|
25
|
+
],
|
|
26
|
+
"patch": "braces@3.0.3.patch",
|
|
27
|
+
"patchSha256": "7274fa48b0167311b25a1ef8cf34f4bfe45f6ef58a0cc2813712fb7bb26e2e6a",
|
|
28
|
+
"installedTreeSha256": "c3d6d7c5bc0b6a578effc6d0e4bf6b384cbf04c6a81cfd2c8af10c185acdb0fb",
|
|
29
|
+
"provenanceUrls": [
|
|
30
|
+
"https://github.com/advisories/GHSA-vfj7-8cjw-p6xm",
|
|
31
|
+
"https://github.com/micromatch/braces/commit/74b2db2938fad48a2ea54a9c8bf27a37a62c350d",
|
|
32
|
+
"https://registry.npmjs.org/braces/-/braces-3.0.3.tgz"
|
|
33
|
+
],
|
|
34
|
+
"origin": "Anhedral local guarded backport; upstream advisory still lists no patched release"
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"name": "decode-uri-component",
|
|
38
|
+
"version": "0.2.2",
|
|
39
|
+
"advisoryIds": [
|
|
40
|
+
"GHSA-vcc3-ghjq-m6fr"
|
|
41
|
+
],
|
|
42
|
+
"patch": "decode-uri-component@0.2.2.patch",
|
|
43
|
+
"patchSha256": "f6a891bfb1a3d59b5b72602d4f6e5de5b6f5ee64467a9a24a1e182a3b07f8c54",
|
|
44
|
+
"installedTreeSha256": "821832e5d3622308a9159706eebb0b9ac702576b39e0cd4ea4fcb15cb9d7cbfb",
|
|
45
|
+
"provenanceUrls": [
|
|
46
|
+
"https://github.com/SamVerschueren/decode-uri-component/commit/fa479dafeede7bedf04e5c89aa78f2a78c664005",
|
|
47
|
+
"https://github.com/SamVerschueren/decode-uri-component/releases/tag/v0.5.0",
|
|
48
|
+
"https://github.com/advisories/GHSA-vcc3-ghjq-m6fr"
|
|
49
|
+
],
|
|
50
|
+
"origin": "Official 0.5.0 security scanner backported to 0.2.2 while preserving its CommonJS API and plus-to-space behavior"
|
|
51
|
+
}
|
|
52
|
+
]
|
|
53
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
diff --git a/lib/rsa.js b/lib/rsa.js
|
|
2
|
+
index a4264822b..430905451 100644
|
|
3
|
+
--- a/lib/rsa.js
|
|
4
|
+
+++ b/lib/rsa.js
|
|
5
|
+
@@ -1168,11 +1168,19 @@ pki.setRsaPublicKey = pki.rsa.setPublicKey = function(n, e) {
|
|
6
|
+
parseAllBytes: options._parseAllDigestBytes
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
- // validate DigestInfo structure and element count
|
|
10
|
+
+ // validate DigestInfo structure and element counts (outer DigestInfo
|
|
11
|
+
+ // and nested DigestAlgorithm). asn1.validate ignores extra children,
|
|
12
|
+
+ // so length must be checked explicitly at each nesting level to
|
|
13
|
+
+ // prevent low-exponent PKCS#1 v1.5 signature forgery (CVE-2026-85393).
|
|
14
|
+
+ // Anhedral additionally rejects nonempty primitive NULL parameters:
|
|
15
|
+
+ // DER NULL has zero content octets; child counts do not check this.
|
|
16
|
+
var capture = {};
|
|
17
|
+
var errors = [];
|
|
18
|
+
if(!asn1.validate(obj, digestInfoValidator, capture, errors) ||
|
|
19
|
+
- obj.value.length !== 2) {
|
|
20
|
+
+ obj.value.length !== 2 ||
|
|
21
|
+
+ obj.value[0].value.length !==
|
|
22
|
+
+ (('parameters' in capture) ? 2 : 1) ||
|
|
23
|
+
+ (('parameters' in capture) && capture.parameters !== '')) {
|
|
24
|
+
var error = new Error(
|
|
25
|
+
'ASN.1 object does not contain a valid RSASSA-PKCS1-v1_5 ' +
|
|
26
|
+
'DigestInfo value.');
|
package/dist/access.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type ApplicationProject } from './project-state.js';
|
|
2
|
+
import { type CloudflareRequest } from './cloudflare.js';
|
|
3
|
+
export interface AccessReceipt {
|
|
4
|
+
target: string;
|
|
5
|
+
workerId: string;
|
|
6
|
+
applicationId: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function validatePrivateAudience(project: ApplicationProject, root: string): void;
|
|
9
|
+
/** Protect the entire Worker, including every route and preview, before product code is deployed. */
|
|
10
|
+
export declare function reconcileAccess(root: string, project: ApplicationProject, request: CloudflareRequest, create: boolean): Promise<AccessReceipt[]>;
|