@ingram-tech/nk-dev 0.8.0 → 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.
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import baseUi from "./base-ui.js";
|
|
6
6
|
import deferredCurrentTarget from "./deferred-current-target.js";
|
|
7
7
|
import lucideIconSuffix from "./lucide-icon-suffix.js";
|
|
8
|
+
import noCryptoRandomUuid from "./no-crypto-random-uuid.js";
|
|
8
9
|
import noRedirectOnlyPage from "./no-redirect-only-page.js";
|
|
9
10
|
import redundantUseStateType from "./redundant-usestate-type.js";
|
|
10
11
|
import tNoPositionalArgs from "./t-no-positional-args.js";
|
|
@@ -16,6 +17,7 @@ export default {
|
|
|
16
17
|
...baseUi.rules,
|
|
17
18
|
...deferredCurrentTarget.rules,
|
|
18
19
|
...lucideIconSuffix.rules,
|
|
20
|
+
...noCryptoRandomUuid.rules,
|
|
19
21
|
...noRedirectOnlyPage.rules,
|
|
20
22
|
...redundantUseStateType.rules,
|
|
21
23
|
...tNoPositionalArgs.rules,
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// nextkit oxlint JS plugin rule: keep `crypto.randomUUID()` off the id write
|
|
2
|
+
// path.
|
|
3
|
+
//
|
|
4
|
+
// nextkit ids are UUIDv7: time-ordered, so inserts land at the right edge of the
|
|
5
|
+
// primary-key B-tree instead of scattering across it. `crypto.randomUUID()` is
|
|
6
|
+
// v4 — uniformly random — so a single call site minting a stored id fragments
|
|
7
|
+
// that index while every other row in the table stays ordered. The mismatch is
|
|
8
|
+
// invisible until the table is large, which is exactly when it is expensive to
|
|
9
|
+
// undo.
|
|
10
|
+
//
|
|
11
|
+
// The mint is `uuidGenerateId()` from `@ingram-tech/nk-db/id`, already typed
|
|
12
|
+
// `Uuid`. Most rows need no mint at all: `uuid("id").primaryKey().default(sql`
|
|
13
|
+
// `uuidv7()`)` lets the database do it, and the app only mints when it needs the
|
|
14
|
+
// id *before* the insert (a client-chosen document PK it must also use as the
|
|
15
|
+
// storage object name).
|
|
16
|
+
//
|
|
17
|
+
// Deliberately not autofixable. The right replacement depends on what the value
|
|
18
|
+
// is, and one of the answers is "leave it alone":
|
|
19
|
+
//
|
|
20
|
+
// - a stored id -> uuidGenerateId(), or drop it for the column default
|
|
21
|
+
// - a bearer token / nonce -> keep crypto.randomUUID()
|
|
22
|
+
//
|
|
23
|
+
// v7 is the *wrong* choice for a secret. It spends 48 bits on a millisecond
|
|
24
|
+
// timestamp, leaving 74 random bits against v4's 122, and it leaks its own
|
|
25
|
+
// creation time to whoever holds it. Invitation tokens, OAuth `state`, password
|
|
26
|
+
// reset links and similar unguessable values must stay v4 — silence the rule at
|
|
27
|
+
// those call sites with a justified disable comment rather than "fixing" them:
|
|
28
|
+
//
|
|
29
|
+
// // oxlint-disable-next-line nextkit/no-crypto-random-uuid -- CSRF nonce, wants v4 entropy
|
|
30
|
+
//
|
|
31
|
+
// Test files are exempt via an override in the shared oxlintrc: test rows are
|
|
32
|
+
// ephemeral, so index locality is meaningless there and `crypto.randomUUID()`
|
|
33
|
+
// stays the zero-import default that keeps fixtures readable.
|
|
34
|
+
|
|
35
|
+
const NODE_CRYPTO_MODULES = new Set(["crypto", "node:crypto"]);
|
|
36
|
+
|
|
37
|
+
/** `crypto.randomUUID` or `globalThis.crypto.randomUUID`, and not shadowed. */
|
|
38
|
+
const isGlobalCryptoRandomUuid = (callee, scope) => {
|
|
39
|
+
if (callee.type !== "MemberExpression") return false;
|
|
40
|
+
if (callee.computed) return false;
|
|
41
|
+
if (callee.property.type !== "Identifier") return false;
|
|
42
|
+
if (callee.property.name !== "randomUUID") return false;
|
|
43
|
+
|
|
44
|
+
const object = callee.object;
|
|
45
|
+
let root;
|
|
46
|
+
if (object.type === "Identifier" && object.name === "crypto") {
|
|
47
|
+
root = object;
|
|
48
|
+
} else if (
|
|
49
|
+
object.type === "MemberExpression" &&
|
|
50
|
+
!object.computed &&
|
|
51
|
+
object.object.type === "Identifier" &&
|
|
52
|
+
object.object.name === "globalThis" &&
|
|
53
|
+
object.property.type === "Identifier" &&
|
|
54
|
+
object.property.name === "crypto"
|
|
55
|
+
) {
|
|
56
|
+
// `globalThis.crypto` can't be shadowed by a local binding.
|
|
57
|
+
return true;
|
|
58
|
+
} else {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// A local `crypto` (a mock, an injected dependency) is not the global one.
|
|
63
|
+
for (let current = scope; current; current = current.upper) {
|
|
64
|
+
const variable = current.variables?.find((v) => v.name === root.name);
|
|
65
|
+
if (!variable) continue;
|
|
66
|
+
return variable.defs.length === 0;
|
|
67
|
+
}
|
|
68
|
+
return true;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const noCryptoRandomUuid = {
|
|
72
|
+
meta: {
|
|
73
|
+
type: "problem",
|
|
74
|
+
docs: {
|
|
75
|
+
description:
|
|
76
|
+
"Disallow crypto.randomUUID() (UUIDv4) where nextkit ids are UUIDv7",
|
|
77
|
+
},
|
|
78
|
+
messages: {
|
|
79
|
+
cryptoRandomUuid:
|
|
80
|
+
"`crypto.randomUUID()` is UUIDv4; stored ids are UUIDv7. Mint with `uuidGenerateId()` from `@ingram-tech/nk-db/id`, or omit the id and let the `uuidv7()` column default apply. If this is a bearer token or nonce, keep v4 and add `// oxlint-disable-next-line nextkit/no-crypto-random-uuid -- <reason>`.",
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
create(context) {
|
|
84
|
+
const sourceCode = context.sourceCode;
|
|
85
|
+
// Local names bound to `randomUUID` imported from node:crypto.
|
|
86
|
+
const importedNames = new Set();
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
ImportDeclaration(node) {
|
|
90
|
+
if (!NODE_CRYPTO_MODULES.has(node.source.value)) return;
|
|
91
|
+
for (const specifier of node.specifiers) {
|
|
92
|
+
if (specifier.type !== "ImportSpecifier") continue;
|
|
93
|
+
if (specifier.imported.type !== "Identifier") continue;
|
|
94
|
+
if (specifier.imported.name !== "randomUUID") continue;
|
|
95
|
+
importedNames.add(specifier.local.name);
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
CallExpression(node) {
|
|
99
|
+
const callee = node.callee;
|
|
100
|
+
const isImported =
|
|
101
|
+
callee.type === "Identifier" && importedNames.has(callee.name);
|
|
102
|
+
if (
|
|
103
|
+
!isImported &&
|
|
104
|
+
!isGlobalCryptoRandomUuid(callee, sourceCode.getScope(node))
|
|
105
|
+
) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
context.report({ node, messageId: "cryptoRandomUuid" });
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
export default {
|
|
115
|
+
meta: { name: "nextkit" },
|
|
116
|
+
rules: { "no-crypto-random-uuid": noCryptoRandomUuid },
|
|
117
|
+
};
|
package/oxlintrc.json
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
"nextkit/no-redirect-only-page": "warn",
|
|
13
13
|
"nextkit/t-requires-values": "error",
|
|
14
14
|
"nextkit/t-no-positional-args": "error",
|
|
15
|
+
"nextkit/no-crypto-random-uuid": "warn",
|
|
15
16
|
"no-unused-vars": "warn",
|
|
16
17
|
"typescript/no-non-null-assertion": "error",
|
|
17
18
|
"typescript/no-explicit-any": "error",
|
|
@@ -28,5 +29,13 @@
|
|
|
28
29
|
"jsx-a11y/prefer-tag-over-role": "off",
|
|
29
30
|
"jsx-a11y/no-autofocus": "off",
|
|
30
31
|
"jsx-a11y/role-has-required-aria-props": "off"
|
|
31
|
-
}
|
|
32
|
+
},
|
|
33
|
+
"overrides": [
|
|
34
|
+
{
|
|
35
|
+
"files": ["**/__tests__/**", "**/*.test.*", "**/*.spec.*", "**/test/**"],
|
|
36
|
+
"rules": {
|
|
37
|
+
"nextkit/no-crypto-random-uuid": "off"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
]
|
|
32
41
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ingram-tech/nk-dev",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "The nextkit dev toolchain in one package: the `nk` CLI plus shared oxlint/oxfmt, TypeScript, and Vitest config, the format-on-commit hook, and the AI agent guide. `nk init` scaffolds a site to use it.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|