mirror of
https://github.com/actualbudget/actual.git
synced 2026-05-08 04:49:45 -05:00
Compare commits
1 Commits
master
...
claude/pla
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fc54c3e856 |
2
.github/workflows/check.yml
vendored
2
.github/workflows/check.yml
vendored
@@ -39,6 +39,8 @@ jobs:
|
||||
run: yarn constraints
|
||||
- name: Check tsconfig project references are in sync
|
||||
run: yarn check:tsconfig-references
|
||||
- name: Check secure-context API deny list is up to date
|
||||
run: yarn workspace eslint-plugin-actual run check:secure-context-list
|
||||
lint:
|
||||
needs: setup
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
"actual/prefer-const": "error",
|
||||
"actual/no-anchor-tag": "error",
|
||||
"actual/no-react-default-import": "error",
|
||||
"actual/no-secure-context-apis": "error",
|
||||
"actual/prefer-subpath-imports": "error",
|
||||
"actual/enforce-boundaries": "error",
|
||||
"actual/no-extraneous-dependencies": "error",
|
||||
@@ -371,6 +372,7 @@
|
||||
"rules": {
|
||||
"actual/no-untranslated-strings": "off",
|
||||
"actual/prefer-logger-over-console": "off",
|
||||
"actual/no-secure-context-apis": "off",
|
||||
"typescript/unbound-method": "off"
|
||||
}
|
||||
},
|
||||
@@ -434,6 +436,58 @@
|
||||
"typescript/no-unsafe-unary-minus": "error",
|
||||
"typescript/no-unsafe-type-assertion": "error"
|
||||
}
|
||||
},
|
||||
// Server-side / Node-only code runs in environments where secure-context
|
||||
// restrictions don't apply.
|
||||
{
|
||||
"files": [
|
||||
"packages/sync-server/**/*",
|
||||
"packages/api/**/*",
|
||||
"packages/cli/**/*",
|
||||
"packages/ci-actions/**/*",
|
||||
"packages/loot-core/src/server/**/*",
|
||||
"packages/loot-core/src/platform/server/**/*",
|
||||
"packages/loot-core/migrations/**/*",
|
||||
"packages/api/migrations/**/*",
|
||||
"packages/desktop-electron/**/*",
|
||||
"packages/eslint-plugin-actual/**/*"
|
||||
],
|
||||
"rules": {
|
||||
"actual/no-secure-context-apis": "off"
|
||||
}
|
||||
},
|
||||
// Grandfathered violators of `actual/no-secure-context-apis`. These
|
||||
// files predate the rule and use secure-context-only Web APIs directly;
|
||||
// adding them to this list keeps existing code working while the rule
|
||||
// prevents *new* call sites. Migrate a file off the list by replacing
|
||||
// the offending API with a safe wrapper, then remove its entry.
|
||||
{
|
||||
"files": [
|
||||
"packages/crdt/src/crdt/timestamp.ts",
|
||||
"packages/desktop-client/src/accounts/mutations.ts",
|
||||
"packages/desktop-client/src/budget/mutations.ts",
|
||||
"packages/desktop-client/src/components/accounts/Account.tsx",
|
||||
"packages/desktop-client/src/components/formula/QueryManager.tsx",
|
||||
"packages/desktop-client/src/components/rules/RuleEditor.tsx",
|
||||
"packages/desktop-client/src/components/settings/Encryption.tsx",
|
||||
"packages/desktop-client/src/notes/DesktopLinkedNotes.tsx",
|
||||
"packages/desktop-client/src/notes/MobileLinkedNotes.tsx",
|
||||
"packages/desktop-client/src/notifications/notificationsSlice.ts",
|
||||
"packages/desktop-client/src/payees/location-adapters.ts",
|
||||
"packages/desktop-client/src/payees/mutations.ts",
|
||||
"packages/desktop-client/src/reports/mutations.ts",
|
||||
"packages/desktop-client/src/tags/mutations.ts",
|
||||
"packages/desktop-client/src/util/ruleUtils.ts",
|
||||
"packages/loot-core/src/mocks/budget.ts",
|
||||
"packages/loot-core/src/mocks/index.ts",
|
||||
"packages/loot-core/src/platform/client/connection/index.electron.ts",
|
||||
"packages/loot-core/src/platform/client/connection/index.ts",
|
||||
"packages/loot-core/src/platform/client/undo/index.ts",
|
||||
"packages/loot-core/src/shared/transactions.ts"
|
||||
],
|
||||
"rules": {
|
||||
"actual/no-secure-context-apis": "off"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -341,6 +341,7 @@ Always maintain newlines between import groups.
|
||||
- Import from `uuid` without destructuring: use `import { v4 as uuidv4 } from 'uuid'`
|
||||
- Import colors directly - use theme instead
|
||||
- Import `@actual-app/web/*` in `loot-core`
|
||||
- Use Web APIs that are only available in secure contexts (e.g. `crypto.randomUUID`, `crypto.subtle`, `navigator.clipboard`, `navigator.serviceWorker`) in browser-side code. Actual must run over plain HTTP from non-localhost hosts. The `actual/no-secure-context-apis` lint rule enforces this; the deny list is generated from `@mdn/browser-compat-data` by `packages/eslint-plugin-actual/lib/data/generate-secure-context-apis.mjs`.
|
||||
|
||||
**Git Commands:**
|
||||
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
#!/usr/bin/env node
|
||||
// Generates secure-context-apis.json — the deny list consumed by the
|
||||
// `actual/no-secure-context-apis` lint rule.
|
||||
//
|
||||
// Source 1: @mdn/browser-compat-data. Each BCD feature can carry a
|
||||
// `secure_context_required` *subfeature* (a child key whose presence means
|
||||
// "this feature is gated to secure contexts"). See
|
||||
// https://github.com/mdn/browser-compat-data/issues/190. We walk `api.*`
|
||||
// and collect entries that have one, translating their BCD path into a JS
|
||||
// global access path via the SEEDS map below.
|
||||
//
|
||||
// Source 2: a small manual augment list. BCD's secure-context coverage is
|
||||
// incomplete (issue tracked at https://github.com/mdn/browser-compat-data/issues/4696
|
||||
// since 2020). Notably, `crypto.randomUUID` — the API that motivated this
|
||||
// rule (actualbudget/actual#7715) — is not currently tagged. The augment
|
||||
// list covers known gaps until BCD catches up.
|
||||
//
|
||||
// Usage:
|
||||
// node generate-secure-context-apis.mjs # write the file
|
||||
// node generate-secure-context-apis.mjs --check # exit 1 if file would change
|
||||
|
||||
import { readFileSync, writeFileSync } from 'node:fs';
|
||||
import { createRequire } from 'node:module';
|
||||
import { dirname, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const bcd = require('@mdn/browser-compat-data');
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const OUTPUT_PATH = resolve(__dirname, 'secure-context-apis.json');
|
||||
|
||||
// Map a BCD interface name (the first segment after `api.`) to the JS
|
||||
// global access path through which it is reached. Interfaces NOT in this
|
||||
// map are skipped — most BCD interfaces aren't directly reachable from a
|
||||
// browser global (they're return values of other APIs) and would produce
|
||||
// false positives.
|
||||
const SEEDS = {
|
||||
// Top-level globals
|
||||
caches: 'caches',
|
||||
Cache: 'Cache',
|
||||
Notification: 'Notification',
|
||||
PaymentRequest: 'PaymentRequest',
|
||||
PaymentManager: 'PaymentManager',
|
||||
PublicKeyCredential: 'PublicKeyCredential',
|
||||
SharedWorker: 'SharedWorker',
|
||||
PushManager: 'PushManager',
|
||||
PushSubscription: 'PushSubscription',
|
||||
EyeDropper: 'EyeDropper',
|
||||
Gamepad: 'Gamepad',
|
||||
GamepadButton: 'GamepadButton',
|
||||
GamepadEvent: 'GamepadEvent',
|
||||
GamepadHapticActuator: 'GamepadHapticActuator',
|
||||
GamepadPose: 'GamepadPose',
|
||||
NDEFMessage: 'NDEFMessage',
|
||||
NDEFReader: 'NDEFReader',
|
||||
NDEFReadingEvent: 'NDEFReadingEvent',
|
||||
NDEFRecord: 'NDEFRecord',
|
||||
PaymentRequestUpdateEvent: 'PaymentRequestUpdateEvent',
|
||||
PresentationRequest: 'PresentationRequest',
|
||||
BatteryManager: 'BatteryManager',
|
||||
ServiceWorker: 'ServiceWorker',
|
||||
ServiceWorkerRegistration: 'ServiceWorkerRegistration',
|
||||
|
||||
// Reached through `crypto.*`
|
||||
Crypto: 'crypto',
|
||||
SubtleCrypto: 'crypto.subtle',
|
||||
|
||||
// Reached through `navigator.*`
|
||||
Navigator: 'navigator',
|
||||
Clipboard: 'navigator.clipboard',
|
||||
ClipboardItem: 'ClipboardItem',
|
||||
ServiceWorkerContainer: 'navigator.serviceWorker',
|
||||
MediaDevices: 'navigator.mediaDevices',
|
||||
Bluetooth: 'navigator.bluetooth',
|
||||
USB: 'navigator.usb',
|
||||
HID: 'navigator.hid',
|
||||
Serial: 'navigator.serial',
|
||||
WakeLock: 'navigator.wakeLock',
|
||||
CredentialsContainer: 'navigator.credentials',
|
||||
StorageManager: 'navigator.storage',
|
||||
LockManager: 'navigator.locks',
|
||||
Geolocation: 'navigator.geolocation',
|
||||
GeolocationCoordinates: 'navigator.geolocation',
|
||||
GeolocationPosition: 'navigator.geolocation',
|
||||
GeolocationPositionError: 'navigator.geolocation',
|
||||
};
|
||||
|
||||
// Manual augment list for APIs that ARE secure-context-only per spec/MDN
|
||||
// but aren't yet tagged in BCD. Keep this list short; prefer fixing BCD
|
||||
// upstream when possible. Each entry must be a JS global access path that
|
||||
// matches the lint rule's chain matcher.
|
||||
const MANUAL_AUGMENT = [
|
||||
// crypto.randomUUID requires a secure context per the WebCrypto spec; the
|
||||
// entire reason this rule exists. https://github.com/actualbudget/actual/issues/7715
|
||||
'crypto.randomUUID',
|
||||
// The Async Clipboard API requires a secure context.
|
||||
'navigator.clipboard',
|
||||
// ServiceWorkerContainer access requires a secure context.
|
||||
'navigator.serviceWorker',
|
||||
// Web Locks API.
|
||||
'navigator.locks',
|
||||
// Storage Manager (estimate/persist/persisted).
|
||||
'navigator.storage',
|
||||
// Credential Management.
|
||||
'navigator.credentials',
|
||||
// Wake Lock.
|
||||
'navigator.wakeLock',
|
||||
// WebHID, WebUSB, WebSerial, Web Bluetooth — covered by BCD interface
|
||||
// tags via SEEDS but listed here too in case a member is accessed in a
|
||||
// way that bypasses the interface root match.
|
||||
'navigator.bluetooth',
|
||||
'navigator.usb',
|
||||
'navigator.hid',
|
||||
'navigator.serial',
|
||||
];
|
||||
|
||||
const IDENT_RE = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
||||
|
||||
function hasSecureContextRequired(node) {
|
||||
return Boolean(
|
||||
node && typeof node === 'object' && node.secure_context_required,
|
||||
);
|
||||
}
|
||||
|
||||
// Walk a BCD subtree starting at `node`. `bcdPathSegments` is the path so
|
||||
// far (excluding the `api` root and the interface root). Calls `visit` for
|
||||
// every entry that requires a secure context.
|
||||
function walk(node, bcdPathSegments, visit) {
|
||||
if (!node || typeof node !== 'object') return;
|
||||
|
||||
if (hasSecureContextRequired(node)) {
|
||||
visit(bcdPathSegments);
|
||||
}
|
||||
|
||||
for (const key of Object.keys(node)) {
|
||||
if (key === '__compat') continue;
|
||||
if (key === 'secure_context_required') continue;
|
||||
if (key.endsWith('_event') || key.endsWith('_static')) continue;
|
||||
if (!IDENT_RE.test(key)) continue;
|
||||
|
||||
const child = node[key];
|
||||
if (!child || typeof child !== 'object') continue;
|
||||
walk(child, [...bcdPathSegments, key], visit);
|
||||
}
|
||||
}
|
||||
|
||||
function generate() {
|
||||
const output = new Set(MANUAL_AUGMENT);
|
||||
|
||||
for (const [iface, accessPath] of Object.entries(SEEDS)) {
|
||||
const ifaceNode = bcd.api[iface];
|
||||
if (!ifaceNode) continue;
|
||||
|
||||
walk(ifaceNode, [], segments => {
|
||||
const memberPath = segments.length
|
||||
? `${accessPath}.${segments.join('.')}`
|
||||
: accessPath;
|
||||
output.add(memberPath);
|
||||
});
|
||||
}
|
||||
|
||||
return [...output].sort();
|
||||
}
|
||||
|
||||
function format(list) {
|
||||
return JSON.stringify(list, null, 2) + '\n';
|
||||
}
|
||||
|
||||
const generated = format(generate());
|
||||
|
||||
if (process.argv.includes('--check')) {
|
||||
let existing = '';
|
||||
try {
|
||||
existing = readFileSync(OUTPUT_PATH, 'utf8');
|
||||
} catch {
|
||||
// file missing => drift
|
||||
}
|
||||
if (existing !== generated) {
|
||||
process.stderr.write(
|
||||
`secure-context-apis.json is out of date.\n` +
|
||||
`Run \`yarn workspace eslint-plugin-actual run generate:secure-context-list\` and commit the result.\n`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
} else {
|
||||
writeFileSync(OUTPUT_PATH, generated);
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
[
|
||||
"BatteryManager",
|
||||
"EyeDropper",
|
||||
"Gamepad",
|
||||
"GamepadButton",
|
||||
"GamepadEvent",
|
||||
"GamepadHapticActuator",
|
||||
"GamepadPose",
|
||||
"NDEFMessage",
|
||||
"NDEFReader",
|
||||
"NDEFReadingEvent",
|
||||
"NDEFRecord",
|
||||
"Notification",
|
||||
"PaymentManager",
|
||||
"PaymentRequestUpdateEvent",
|
||||
"PresentationRequest",
|
||||
"caches",
|
||||
"crypto.randomUUID",
|
||||
"crypto.subtle",
|
||||
"navigator.activeVRDisplays",
|
||||
"navigator.bluetooth",
|
||||
"navigator.clipboard",
|
||||
"navigator.credentials",
|
||||
"navigator.geolocation",
|
||||
"navigator.getBattery",
|
||||
"navigator.getGamepads",
|
||||
"navigator.hid",
|
||||
"navigator.locks",
|
||||
"navigator.mediaDevices",
|
||||
"navigator.mediaDevices.getUserMedia",
|
||||
"navigator.registerProtocolHandler",
|
||||
"navigator.requestMIDIAccess",
|
||||
"navigator.serial",
|
||||
"navigator.serviceWorker",
|
||||
"navigator.storage",
|
||||
"navigator.usb",
|
||||
"navigator.userAgentData",
|
||||
"navigator.wakeLock"
|
||||
]
|
||||
@@ -14,6 +14,7 @@ module.exports = eslintCompatPlugin({
|
||||
'prefer-const': require('./rules/prefer-const'),
|
||||
'no-anchor-tag': require('./rules/no-anchor-tag'),
|
||||
'no-react-default-import': require('./rules/no-react-default-import'),
|
||||
'no-secure-context-apis': require('./rules/no-secure-context-apis'),
|
||||
'prefer-subpath-imports': require('./rules/prefer-subpath-imports'),
|
||||
'enforce-boundaries': require('./rules/enforce-boundaries'),
|
||||
'no-extraneous-dependencies': require('./rules/no-extraneous-dependencies'),
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
import { runClassic } from 'eslint-vitest-rule-tester';
|
||||
|
||||
import plugin from '../../index';
|
||||
const rule = plugin.rules['no-secure-context-apis'];
|
||||
|
||||
void runClassic(
|
||||
'no-secure-context-apis',
|
||||
rule,
|
||||
{
|
||||
valid: [
|
||||
// Different `crypto` member that doesn't require a secure context.
|
||||
'crypto.getRandomValues(new Uint8Array(16));',
|
||||
// Locally-named property access shouldn't false-positive.
|
||||
'const obj = { crypto: { randomUUID: () => 1 } }; obj.crypto.randomUUID();',
|
||||
// Importing a UUID library is the suggested replacement.
|
||||
"import { v4 as uuidv4 } from 'uuid'; uuidv4();",
|
||||
// Computed access — we deliberately don't try to resolve dynamic keys.
|
||||
"crypto['randomUUID'];",
|
||||
// Unrelated bare identifier with the same name as a deny entry but used
|
||||
// as a local variable (declarations are not flagged).
|
||||
'function f(Notification) { return Notification; }',
|
||||
],
|
||||
invalid: [
|
||||
{
|
||||
code: 'crypto.randomUUID();',
|
||||
output: null,
|
||||
errors: [
|
||||
{
|
||||
messageId: 'secureContextOnly',
|
||||
data: { path: 'crypto.randomUUID' },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: 'const id = crypto.randomUUID();',
|
||||
output: null,
|
||||
errors: [
|
||||
{
|
||||
messageId: 'secureContextOnly',
|
||||
data: { path: 'crypto.randomUUID' },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "await navigator.clipboard.writeText('hi');",
|
||||
output: null,
|
||||
errors: [
|
||||
{
|
||||
messageId: 'secureContextOnly',
|
||||
data: { path: 'navigator.clipboard' },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: 'crypto.subtle.encrypt(algo, key, data);',
|
||||
output: null,
|
||||
errors: [
|
||||
{
|
||||
messageId: 'secureContextOnly',
|
||||
data: { path: 'crypto.subtle' },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "new Notification('title');",
|
||||
output: null,
|
||||
errors: [
|
||||
{
|
||||
messageId: 'secureContextOnly',
|
||||
data: { path: 'Notification' },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "caches.open('cache-v1');",
|
||||
output: null,
|
||||
errors: [
|
||||
{
|
||||
messageId: 'secureContextOnly',
|
||||
data: { path: 'caches' },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "navigator.serviceWorker.register('/sw.js');",
|
||||
output: null,
|
||||
errors: [
|
||||
{
|
||||
messageId: 'secureContextOnly',
|
||||
data: { path: 'navigator.serviceWorker' },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
parserOptions: {
|
||||
ecmaVersion: 2022,
|
||||
sourceType: 'module',
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,113 @@
|
||||
// Forbids usage of Web APIs that are only defined in secure contexts
|
||||
// (HTTPS or localhost). Actual must keep working when self-hosted over
|
||||
// plain HTTP, so introducing new direct uses of these APIs is a regression
|
||||
// risk. The deny list is generated from `@mdn/browser-compat-data` by
|
||||
// `lib/data/generate-secure-context-apis.mjs`.
|
||||
//
|
||||
// The rule fires on member access whose chain prefix matches a deny entry,
|
||||
// and on `new Foo(...)` for constructors named in the deny list.
|
||||
|
||||
const denyPaths = require('../data/secure-context-apis.json');
|
||||
|
||||
// Index deny paths by their root identifier so we don't re-scan the whole
|
||||
// list on every node. Each entry stores the parsed segments for prefix
|
||||
// comparison.
|
||||
const indexByRoot = new Map();
|
||||
for (const entry of denyPaths) {
|
||||
const segments = entry.split('.');
|
||||
const [root] = segments;
|
||||
if (!indexByRoot.has(root)) indexByRoot.set(root, []);
|
||||
indexByRoot.get(root).push({ path: entry, segments });
|
||||
}
|
||||
|
||||
function getMemberChain(node) {
|
||||
const parts = [];
|
||||
let cur = node;
|
||||
while (cur && cur.type === 'MemberExpression') {
|
||||
if (cur.computed) return null;
|
||||
if (!cur.property || cur.property.type !== 'Identifier') return null;
|
||||
parts.unshift(cur.property.name);
|
||||
cur = cur.object;
|
||||
}
|
||||
if (!cur || cur.type !== 'Identifier') return null;
|
||||
parts.unshift(cur.name);
|
||||
return parts;
|
||||
}
|
||||
|
||||
function findMatch(chain) {
|
||||
const candidates = indexByRoot.get(chain[0]);
|
||||
if (!candidates) return null;
|
||||
for (const candidate of candidates) {
|
||||
if (candidate.segments.length > chain.length) continue;
|
||||
let matches = true;
|
||||
for (let i = 0; i < candidate.segments.length; i++) {
|
||||
if (candidate.segments[i] !== chain[i]) {
|
||||
matches = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (matches) return candidate.path;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description:
|
||||
'Forbid Web APIs that are only available in secure contexts; Actual must run over plain HTTP',
|
||||
},
|
||||
fixable: null,
|
||||
schema: [],
|
||||
messages: {
|
||||
secureContextOnly:
|
||||
'`{{path}}` is only available in secure contexts (HTTPS or localhost) and throws on plain HTTP. Actual must work in insecure contexts; do not introduce new direct uses.',
|
||||
},
|
||||
},
|
||||
|
||||
createOnce(context) {
|
||||
return {
|
||||
MemberExpression(node) {
|
||||
// Only fire on the outermost MemberExpression of a chain so we don't
|
||||
// double-report nested accesses (e.g. navigator.clipboard.writeText
|
||||
// matches both `navigator.clipboard` and the longer chain).
|
||||
if (
|
||||
node.parent &&
|
||||
node.parent.type === 'MemberExpression' &&
|
||||
node.parent.object === node
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const chain = getMemberChain(node);
|
||||
if (!chain) return;
|
||||
|
||||
const match = findMatch(chain);
|
||||
if (match) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: 'secureContextOnly',
|
||||
data: { path: match },
|
||||
});
|
||||
}
|
||||
},
|
||||
NewExpression(node) {
|
||||
// `new Notification(...)`, `new PaymentRequest(...)`, etc. — a
|
||||
// bare-identifier constructor whose name is in the deny list.
|
||||
if (!node.callee || node.callee.type !== 'Identifier') return;
|
||||
const candidates = indexByRoot.get(node.callee.name);
|
||||
if (!candidates) return;
|
||||
const match = candidates.find(c => c.segments.length === 1);
|
||||
if (match) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: 'secureContextOnly',
|
||||
data: { path: match.path },
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -5,9 +5,12 @@
|
||||
"main": "./lib/index.js",
|
||||
"exports": "./lib/index.js",
|
||||
"scripts": {
|
||||
"test": "vitest --run"
|
||||
"test": "vitest --run",
|
||||
"generate:secure-context-list": "node lib/data/generate-secure-context-apis.mjs",
|
||||
"check:secure-context-list": "node lib/data/generate-secure-context-apis.mjs --check"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@mdn/browser-compat-data": "^6.0.0",
|
||||
"@oxlint/plugins": "^1.60.0",
|
||||
"eslint-vitest-rule-tester": "^3.1.0",
|
||||
"vitest": "^4.1.2"
|
||||
|
||||
@@ -5029,6 +5029,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@mdn/browser-compat-data@npm:^6.0.0":
|
||||
version: 6.1.5
|
||||
resolution: "@mdn/browser-compat-data@npm:6.1.5"
|
||||
checksum: 10/4fb08f12a8471d9b1b3f10aa0b674c0ab24edc2aa10abf284a4c407c2bd6d55b5ed469e21f465677ecc08eac288c360d751fb02e8147722282609943a2417959
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@mdx-js/mdx@npm:^3.0.0":
|
||||
version: 3.1.1
|
||||
resolution: "@mdx-js/mdx@npm:3.1.1"
|
||||
@@ -15631,6 +15638,7 @@ __metadata:
|
||||
version: 0.0.0-use.local
|
||||
resolution: "eslint-plugin-actual@workspace:packages/eslint-plugin-actual"
|
||||
dependencies:
|
||||
"@mdn/browser-compat-data": "npm:^6.0.0"
|
||||
"@oxlint/plugins": "npm:^1.60.0"
|
||||
eslint-vitest-rule-tester: "npm:^3.1.0"
|
||||
vitest: "npm:^4.1.2"
|
||||
|
||||
Reference in New Issue
Block a user