@terpjs/eslint-boundaries 0.21.0 → 0.22.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/package.json +2 -2
- package/src/index.js +27 -0
- package/src/index.test.js +55 -0
- package/src/spec.js +2 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@terpjs/eslint-boundaries",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.22.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Terp frontend boundary rules (as data) + the ESLint adapter: no cross-module imports, no package internals, design-token-only styling (no style/className/module stylesheets), token-styled components for raw HTML tags, router-only in-app links, generated-client-only, and browser XSS/navigation sink bans. Strict-only (no modes); governed opt-outs via terp-allow markers + the escape-hatch budget ratchet (terp-boundaries-budget).",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"typescript-eslint": "^8.69.0"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
|
-
"@terpjs/spec": "0.
|
|
28
|
+
"@terpjs/spec": "0.34.0",
|
|
29
29
|
"eslint": "^10.10.0",
|
|
30
30
|
"vitest": "^5.0.0"
|
|
31
31
|
},
|
package/src/index.js
CHANGED
|
@@ -918,6 +918,12 @@ const clipboardMessage =
|
|
|
918
918
|
"TypeError that no .catch and no try around an await ever sees. Use copyText or " +
|
|
919
919
|
"useCopyToClipboard from @terpjs/react-core, which feature-detect, fall back, and " +
|
|
920
920
|
"report a refusal instead of failing silently.";
|
|
921
|
+
const randomUuidMessage =
|
|
922
|
+
"crypto.randomUUID exists only in a secure context and lib.dom declares it "
|
|
923
|
+
+ "unconditionally, so on an http origin this is a call on undefined -- a SYNCHRONOUS "
|
|
924
|
+
+ "TypeError that type-checks cleanly and never fires on localhost, so no test sees it. "
|
|
925
|
+
+ "Use randomUuid from @terpjs/react-core, which falls back to crypto.getRandomValues "
|
|
926
|
+
+ "(not secure-context-gated) for the same entropy.";
|
|
921
927
|
const deepImportMessage =
|
|
922
928
|
"Import from the package root (@terpjs/react-core, @terpjs/contract), not its internals.";
|
|
923
929
|
const styleImportMessage =
|
|
@@ -998,11 +1004,32 @@ function restrictedSyntaxWithCatalogIds() {
|
|
|
998
1004
|
},
|
|
999
1005
|
]
|
|
1000
1006
|
: [];
|
|
1007
|
+
const rawRandomUuid = BOUNDARY_SPEC.restrictRawRandomUuid
|
|
1008
|
+
? [
|
|
1009
|
+
{
|
|
1010
|
+
// Any ACCESS, like the clipboard rule beside it and for the same reason:
|
|
1011
|
+
// `const gen = crypto.randomUUID` binds the same undefined one line before the
|
|
1012
|
+
// call that throws on it.
|
|
1013
|
+
catalogId: "frontend/no-raw-random-uuid",
|
|
1014
|
+
selector:
|
|
1015
|
+
"MemberExpression[object.name='crypto'][property.name='randomUUID'], MemberExpression[object.name='crypto'][computed=true][property.value='randomUUID'], MemberExpression[object.type='MemberExpression'][object.object.name=/^(window|globalThis|self)$/][object.property.name='crypto'][property.name='randomUUID'], MemberExpression[object.type='MemberExpression'][object.object.name=/^(window|globalThis|self)$/][object.computed=true][object.property.value='crypto'][property.name='randomUUID']",
|
|
1016
|
+
message: randomUuidMessage,
|
|
1017
|
+
},
|
|
1018
|
+
{
|
|
1019
|
+
// The destructuring spelling, which no member-expression selector reaches.
|
|
1020
|
+
catalogId: "frontend/no-raw-random-uuid",
|
|
1021
|
+
selector:
|
|
1022
|
+
"VariableDeclarator[init.name='crypto'] ObjectPattern > Property[key.name='randomUUID'], VariableDeclarator[init.type='MemberExpression'][init.property.name='crypto'] ObjectPattern > Property[key.name='randomUUID']",
|
|
1023
|
+
message: randomUuidMessage,
|
|
1024
|
+
},
|
|
1025
|
+
]
|
|
1026
|
+
: [];
|
|
1001
1027
|
return [
|
|
1002
1028
|
...rawElements,
|
|
1003
1029
|
...rawAttributes,
|
|
1004
1030
|
...inAppAnchors,
|
|
1005
1031
|
...rawClipboard,
|
|
1032
|
+
...rawRandomUuid,
|
|
1006
1033
|
{
|
|
1007
1034
|
catalogId: "frontend/no-dom-html-injection",
|
|
1008
1035
|
selector: "JSXAttribute[name.name='dangerouslySetInnerHTML']",
|
package/src/index.test.js
CHANGED
|
@@ -196,6 +196,61 @@ describe("terpBoundaries", () => {
|
|
|
196
196
|
expect(await lint("export const pick = (o) => o.clipboard;")).toEqual([]);
|
|
197
197
|
});
|
|
198
198
|
|
|
199
|
+
it("refuses crypto.randomUUID in every spelling that binds the same undefined", async () => {
|
|
200
|
+
// Secure-context-only, declared unconditionally by lib.dom: on an http origin each of
|
|
201
|
+
// these is a synchronous TypeError that type-checks cleanly and never fires on
|
|
202
|
+
// localhost, so neither tsc nor CI ever sees it.
|
|
203
|
+
expect(await lint("export const id = () => crypto.randomUUID();")).toContain(
|
|
204
|
+
"no-restricted-syntax",
|
|
205
|
+
);
|
|
206
|
+
expect(await lint('export const id = () => crypto["randomUUID"]();')).toContain(
|
|
207
|
+
"no-restricted-syntax",
|
|
208
|
+
);
|
|
209
|
+
expect(await lint("export const id = () => window.crypto.randomUUID();")).toContain(
|
|
210
|
+
"no-restricted-syntax",
|
|
211
|
+
);
|
|
212
|
+
expect(await lint("export const id = () => globalThis.crypto.randomUUID();")).toContain(
|
|
213
|
+
"no-restricted-syntax",
|
|
214
|
+
);
|
|
215
|
+
expect(await lint("export const id = () => self.crypto.randomUUID();")).toContain(
|
|
216
|
+
"no-restricted-syntax",
|
|
217
|
+
);
|
|
218
|
+
expect(
|
|
219
|
+
await lint('export const id = () => globalThis["crypto"].randomUUID();'),
|
|
220
|
+
).toContain("no-restricted-syntax");
|
|
221
|
+
// Bound rather than called: the reference is already the broken one.
|
|
222
|
+
expect(await lint("export const gen = crypto.randomUUID;")).toContain(
|
|
223
|
+
"no-restricted-syntax",
|
|
224
|
+
);
|
|
225
|
+
expect(await lint("export const { randomUUID } = crypto;")).toContain(
|
|
226
|
+
"no-restricted-syntax",
|
|
227
|
+
);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it("accepts the react-core uuid seam, and the API that is not gated", async () => {
|
|
231
|
+
expect(
|
|
232
|
+
await lint(
|
|
233
|
+
'import { randomUuid } from "@terpjs/react-core";\nexport const id = () => randomUuid();',
|
|
234
|
+
),
|
|
235
|
+
).toEqual([]);
|
|
236
|
+
// getRandomValues is NOT secure-context-gated, so it is not this defect and the seam
|
|
237
|
+
// itself is built on it. Flagging it would make the rule a word filter on "crypto".
|
|
238
|
+
expect(
|
|
239
|
+
await lint("export const bytes = () => crypto.getRandomValues(new Uint8Array(16));"),
|
|
240
|
+
).toEqual([]);
|
|
241
|
+
// And a `randomUUID` on something that is not `crypto` is somebody else's function.
|
|
242
|
+
expect(await lint("export const id = (lib) => lib.randomUUID();")).toEqual([]);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it("honours the uuid rule's declared escape hatch, and only its own name", async () => {
|
|
246
|
+
const call = "export const id = () => crypto.randomUUID();";
|
|
247
|
+
expect(
|
|
248
|
+
await lint(`// terp-allow-no-raw-random-uuid: https-only admin origin\n${call}`),
|
|
249
|
+
).toEqual([]);
|
|
250
|
+
const wrong = await lint(`// terp-allow-no-random-uuid: typo\n${call}`);
|
|
251
|
+
expect(wrong).toContain("no-restricted-syntax");
|
|
252
|
+
});
|
|
253
|
+
|
|
199
254
|
it("honours the clipboard rule's declared escape hatch, and only its own name", async () => {
|
|
200
255
|
// The catalog entry declares `// terp-allow-no-raw-clipboard: <reason>`, and a
|
|
201
256
|
// declared opt-out that does not actually suppress is a false promise in the
|
package/src/spec.js
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* NOT by this package's own suite, which certification runs against candidate spec releases
|
|
17
17
|
* whose version is allowed to be newer).
|
|
18
18
|
*/
|
|
19
|
-
export const SPEC_VERSION = "0.
|
|
19
|
+
export const SPEC_VERSION = "0.34.0";
|
|
20
20
|
|
|
21
21
|
export const BOUNDARY_SPEC = {
|
|
22
22
|
/** Every app-authored TypeScript source file whose user-facing copy must be cataloged. */
|
|
@@ -120,6 +120,7 @@ export const BOUNDARY_SPEC = {
|
|
|
120
120
|
* `copyText` / `useCopyToClipboard` feature-detect and report a refusal instead.
|
|
121
121
|
*/
|
|
122
122
|
restrictRawClipboard: true,
|
|
123
|
+
restrictRawRandomUuid: true,
|
|
123
124
|
/**
|
|
124
125
|
* The governed escape hatch (the frontend analog of the backend's `# arch-allow-*`): a
|
|
125
126
|
* justified `// terp-allow-<rule>: <reason>` comment on (or immediately above) a violating
|