diff --git a/.config/repo/oxlint.config.mts b/.config/repo/oxlint.config.mts index e74427b9..d0cdb135 100644 --- a/.config/repo/oxlint.config.mts +++ b/.config/repo/oxlint.config.mts @@ -1,37 +1,27 @@ /** - * @file Socket-vscode repo oxlint config. Imports the fleet factory and - * augments it in JS — see `.config/fleet/oxlint.config.mts` for why this is - * a factory call rather than oxlint `extends` (extends drops - * plugins/categories/ignorePatterns and mis-roots relative globs). + * Repository-owned lint configuration. Keep the basic lint command usable + * without hydrating the shared Fleet config. */ - -import { config } from '../fleet/oxlint.config.mts' - -export default config({ - rules: { - // This extension is interop-heavy by nature: acorn AST node probing - // (js-source-externals), the Go wasm runtime glue (wasm-executor), branded - // TextDocumentURIString casts, and JSON.parse narrowing across scripts. - // Every one of its ~90 `as` sites is a deliberate narrowing at an untyped - // boundary, so the type-aware blanket rule is noise here. Re-enable per - // directory as boundaries grow real validators (typebox schemas already - // cover the lockstep manifest path). - 'typescript/no-unsafe-type-assertion': 'off', +export default { + categories: { + correctness: "error", + suspicious: "warn", }, - overrides: [ - { - // tsgolint resolves each file's nearest tsconfig; everything outside - // src/ falls back to a projectless view WITHOUT the fleet base's - // noUncheckedIndexedAccess, so the type-aware pass mislabels the - // `arr[i]!` assertions those trees need as "unnecessary" and its - // autofix strips them — breaking the check-tier tsc - // (.config/fleet/tsconfig.check.json), which DOES see the fleet base. - // Scope the rule to src/ (covered by the root tsconfig project) until - // the scripts/test trees get a real tsconfig project of their own. - files: ['scripts/**', 'test/**', 'vendor/**', '.github/**', '*.mts'], - rules: { - 'typescript/no-unnecessary-type-assertion': 'off', - }, - }, + ignorePatterns: [ + "coverage/**", + "out/**", + "vendor/**", + "scripts/**", + "test/**", + ".github/**", + ".git-hooks/**", + ".claude/**", + ".config/**", + "docs/**", ], -}) + plugins: ["typescript"], + rules: { + "no-unused-vars": "off", + "typescript/no-unsafe-type-assertion": "off", + }, +}; diff --git a/.config/repo/rolldown.config.mts b/.config/repo/rolldown.config.mts index b7757dbd..8794cfbc 100644 --- a/.config/repo/rolldown.config.mts +++ b/.config/repo/rolldown.config.mts @@ -6,7 +6,6 @@ import type { Plugin, RolldownOptions } from 'rolldown' import { getEnvValue } from '@socketsecurity/lib-stable/env/rewire' -import { defineGuardedPlugin } from '../fleet/rolldown/define-guarded.mts' const rootPath = path.resolve(import.meta.dirname, '../..') const require = createRequire(import.meta.url) @@ -47,6 +46,19 @@ export function stageParserWasmPlugin(): Plugin { } } +export function defineExtensionVersionPlugin(version: string): Plugin { + const replacement = JSON.stringify(version) + return { + name: 'define-extension-version', + transform(source) { + return source.replace( + /process\s*\.\s*env\s*\[\s*(['"])INLINED_EXTENSION_VERSION\1\s*\]/gu, + replacement, + ) + }, + } +} + const pkg = JSON.parse( readFileSync(path.join(rootPath, 'package.json'), 'utf8'), ) as { version?: string | undefined } @@ -89,9 +101,7 @@ const config: RolldownOptions = { // `process.env['X']` on index-signature types — and both normalize to this // same key. The `INLINED_*` name follows the fleet convention (see // socket-cli) for build-inlined values. - defineGuardedPlugin({ - 'process.env.INLINED_EXTENSION_VERSION': JSON.stringify(extensionVersion), - }), + defineExtensionVersionPlugin(extensionVersion), stageParserWasmPlugin(), ], // The sources are `.mts` (the fleet's sources-are-mts rule) and import each diff --git a/.config/repo/tsconfig.json b/.config/repo/tsconfig.json index 3cdf785d..03c565d1 100644 --- a/.config/repo/tsconfig.json +++ b/.config/repo/tsconfig.json @@ -1,5 +1,17 @@ { - "extends": ["../fleet/tsconfig.base.json", "./tsconfig.check.json"], + "compilerOptions": { + "allowImportingTsExtensions": true, + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "lib": ["DOM", "ES2024"], + "module": "ESNext", + "moduleResolution": "bundler", + "noEmit": true, + "skipLibCheck": true, + "strict": true, + "target": "ES2024", + "types": ["node", "vscode"] + }, "include": ["../../src/**/*.mts", "../../src/**/*.d.ts"], - "exclude": ["../../node_modules"] + "exclude": ["../../node_modules", "../../out"] } diff --git a/.config/repo/vitest.config.mts b/.config/repo/vitest.config.mts new file mode 100644 index 00000000..88310681 --- /dev/null +++ b/.config/repo/vitest.config.mts @@ -0,0 +1,26 @@ +import path from "node:path"; +import { defineConfig } from "vitest/config"; + +const root = path.resolve(import.meta.dirname, "../.."); + +export default defineConfig({ + resolve: { + alias: { + vscode: path.join(root, "test/stubs/vscode.mts"), + }, + }, + test: { + setupFiles: [path.join(root, "test/repo/vitest.setup.mts")], + include: ["test/repo/unit/**/*.test.mts"], + exclude: [ + "test/**/*.fuzz.mts", + "test/repo/unit/test-extension.test.mts", + "test/repo/unit/test-extension-auth.test.mts", + "test/repo/unit/scripts/install-claude-plugins.test.mts", + ], + testTimeout: 15000, + hookTimeout: 15000, + pool: "forks", + maxWorkers: 4, + }, +}); diff --git a/.github/actions/fleet/setup/action.yml b/.github/actions/fleet/setup/action.yml index 818a8696..40574a37 100644 --- a/.github/actions/fleet/setup/action.yml +++ b/.github/actions/fleet/setup/action.yml @@ -789,18 +789,16 @@ runs: # Source of truth (baked into the binary) lives at # SocketDev/firewall:src/lib/registries/default.ts. # - # The bypass list itself lives in the wheelhouse-canonical - # location `.config/fleet/sfw-bypass-list.txt` (action_path is - # the running repo checkout (inlined local action); - # `../../../../.config/fleet` walks up out of - # `.github/actions/fleet/setup/` to repo root). Every fleet repo gets - # a byte-identical copy via sync-scaffolding, so socket-btm's - # install-sfw.mts can read its own local copy at the same path. + # A Fleet checkout may provide repo-specific registry bypass entries. + # Standalone repos do not carry that Fleet config and rely on the + # registry defaults compiled into Socket Firewall. BYPASS_FILE="${{ github.action_path }}/../../../../.config/fleet/sfw-bypass-list.txt" { echo 'SFW_CUSTOM_REGISTRIES< { describe('authenticated package response bounds', () => { test('rejects identity responses beyond the SDK wire-byte limit', async () => { - const scope = nock('https://api.socket.dev') - .post('/v0/purl') + const scope = nock(proxyOrigin) + .post('/batch') .query({ alerts: 'true', compact: 'false' }) .reply(200, Buffer.alloc(10 * 1024 * 1024 + 1, 0x20), { 'content-encoding': 'identity', @@ -281,14 +281,14 @@ describe('authenticated package response bounds', () => { purls.push(`pkg:npm/example-authenticated-${index}`) } const artifacts = [publicArtifact(purls[0]!), publicArtifact(purls[1024]!)] - const first = nock('https://api.socket.dev') - .post('/v0/purl', { + const first = nock(proxyOrigin) + .post('/batch', { components: purls.slice(0, 1024).map(purl => ({ purl })), }) .query({ alerts: 'true', compact: 'false' }) .reply(200, JSON.stringify(artifacts[0])) - const second = nock('https://api.socket.dev') - .post('/v0/purl', { components: [{ purl: purls[1024] }] }) + const second = nock(proxyOrigin) + .post('/batch', { components: [{ purl: purls[1024] }] }) .query({ alerts: 'true', compact: 'false' }) .reply(200, JSON.stringify(artifacts[1])) const seen: PackageScoreAndAlerts[] = [] diff --git a/test/repo/unit/api.test.mts b/test/repo/unit/api.test.mts index 2cb2a2d4..6bfa25e9 100644 --- a/test/repo/unit/api.test.mts +++ b/test/repo/unit/api.test.mts @@ -1,7 +1,7 @@ /** * @file Unit tests for the Socket API layer (src/api.mts), which wraps * `@socketsecurity/sdk`. Every request is exercised against a nock-mocked - * api.socket.dev — no real network (the fleet setup fails net-connect + * purl-api.socket.dev — no real network (the test setup fails net-connect * closed), and no owned-infrastructure mocking. We assert both the request * the SDK sends (path, query, body) and how api.ts reshapes the response. */ @@ -19,10 +19,11 @@ import type { PackageScoreAndAlerts } from '../../../src/api.mts' import type { SimPURL } from '../../../src/ui/externals/parse-externals.mts' const API_ORIGIN = 'https://api.socket.dev' +const PURL_ORIGIN = 'https://purl-api.socket.dev' const TOKEN = 'sktsec_test_token' const nullPayloadValue: unknown = JSON.parse('null') -// Build a full artifact line as the /v0/purl NDJSON stream emits it. +// Build a full artifact line as the PURL batch NDJSON stream emits it. function artifactLine(purl: SimPURL, overall: number): string { const artifact: PackageScoreAndAlerts = { alerts: [], @@ -128,8 +129,8 @@ describe('api getOrganizations', () => { describe('api streamPackageScores', () => { test('accepts the cache timeout within the SDK transport limit', async () => { const purl = 'pkg:npm/example-cache-timeout' as SimPURL - const scope = nock(API_ORIGIN) - .post('/v0/purl') + const scope = nock(PURL_ORIGIN) + .post('/batch') .query({ alerts: 'true', compact: 'false' }) .reply(200, `${artifactLine(purl, 0.5)}\n`) @@ -159,7 +160,7 @@ describe('api streamPackageScores', () => { cause: new ResponseError( response, TOKEN, - `${API_ORIGIN}/?token=${TOKEN}`, + `${PURL_ORIGIN}/?token=${TOKEN}`, ), }), ) @@ -217,8 +218,8 @@ describe('api streamPackageScores', () => { { version: 42 }, ] for (let index = 0, { length } = malformed; index < length; index += 1) { - const scope = nock(API_ORIGIN) - .post('/v0/purl') + const scope = nock(PURL_ORIGIN) + .post('/batch') .query({ alerts: 'true', compact: 'false' }) .reply( 200, @@ -239,8 +240,8 @@ describe('api streamPackageScores', () => { async identity => { const purl = 'pkg:npm/example-invalid-identity' as SimPURL const invalid = { ...JSON.parse(artifactLine(purl, 0.1)), ...identity } - const scope = nock(API_ORIGIN) - .post('/v0/purl') + const scope = nock(PURL_ORIGIN) + .post('/batch') .query({ alerts: 'true', compact: 'false' }) .reply(200, `${JSON.stringify(invalid)}\n${artifactLine(purl, 0.9)}\n`) @@ -276,8 +277,8 @@ describe('api streamPackageScores', () => { }, ], } - const scope = nock(API_ORIGIN) - .post('/v0/purl') + const scope = nock(PURL_ORIGIN) + .post('/batch') .query({ alerts: 'true', compact: 'false' }) .reply(200, `${JSON.stringify(artifact)}\n`) @@ -295,7 +296,7 @@ describe('api streamPackageScores', () => { status, error: TOKEN, cause: `Authorization: Bearer ${TOKEN}`, - url: `${API_ORIGIN}/v0/purl?token=${TOKEN}`, + url: `${PURL_ORIGIN}/batch?token=${TOKEN}`, }) expect(error).toMatchObject({ @@ -311,8 +312,8 @@ describe('api streamPackageScores', () => { 'rejects an SDK HTTP %i failure without response details', async status => { const purl = 'pkg:npm/example-denied@1.0.0' as SimPURL - const scope = nock(API_ORIGIN) - .post('/v0/purl') + const scope = nock(PURL_ORIGIN) + .post('/batch') .query({ alerts: 'true', compact: 'false' }) .reply(status, { error: { message: `Authorization: Bearer ${TOKEN}` } }) @@ -339,8 +340,8 @@ describe('api streamPackageScores', () => { type: 'npm', alerts: [{ action: 'warn', type, severity: 'medium' }], }) - const scope = nock(API_ORIGIN) - .post('/v0/purl') + const scope = nock(PURL_ORIGIN) + .post('/batch') .query({ alerts: 'true', compact: 'false' }) .reply( 200, @@ -371,8 +372,8 @@ describe('api streamPackageScores', () => { `{"inputPurl":"${purl}","name":"example-invalid-score","type":"npm","alerts":[],"score":${score}}`, ) .join('\n') - const scope = nock(API_ORIGIN) - .post('/v0/purl') + const scope = nock(PURL_ORIGIN) + .post('/batch') .query({ alerts: 'true', compact: 'false' }) .reply(200, `${lines}\n${artifactLine(purl, 0)}\n`) @@ -386,8 +387,8 @@ describe('api streamPackageScores', () => { test('yields one score+alerts object per artifact line', async () => { const purlA = 'pkg:npm/left-pad@1.0.0' as SimPURL const purlB = 'pkg:npm/right-pad@2.0.0' as SimPURL - nock(API_ORIGIN) - .post('/v0/purl') + nock(PURL_ORIGIN) + .post('/batch') .query({ alerts: 'true', compact: 'false' }) .reply(200, `${artifactLine(purlA, 42)}\n${artifactLine(purlB, 99)}\n`) @@ -400,11 +401,11 @@ describe('api streamPackageScores', () => { expect(seen.map(s => s.score.overall)).toEqual([42, 99]) }) - test('requests /v0/purl with the pending purls in the body', async () => { + test('requests /batch with the pending purls in the body', async () => { const purl = 'pkg:npm/express@4.0.0' as SimPURL let sentBody: unknown - nock(API_ORIGIN) - .post('/v0/purl', body => { + nock(PURL_ORIGIN) + .post('/batch', body => { sentBody = body return true }) @@ -438,8 +439,8 @@ describe('api streamPackageScores', () => { _type: 'purlError', value: { error: 'Package not found', inputPurl: purl }, }) - nock(API_ORIGIN) - .post('/v0/purl') + nock(PURL_ORIGIN) + .post('/batch') .query({ alerts: 'true', compact: 'false' }) .reply(200, `${artifactLine(purl, 77)}\n${summaryLine}\n${errorLine}\n`) diff --git a/test/repo/unit/define-guarded.test.mts b/test/repo/unit/define-guarded.test.mts deleted file mode 100644 index e13b305f..00000000 --- a/test/repo/unit/define-guarded.test.mts +++ /dev/null @@ -1,83 +0,0 @@ -/** - * @file Tests for the guarded compile-time define plugin - * (.config/fleet/rolldown/define-guarded.mts). Locks in that a single dotted - * define - * key matches dot, single-quote, and double-quote member access alike (TS - * forces quoted bracket access on `process.env`), that TS source is parsed - * (not rejected as JS), and that write / dynamic positions are left - * untouched. - */ - -import { describe, expect, test } from 'vitest' - -import { defineGuardedPlugin } from '../../../.config/fleet/rolldown/define-guarded.mts' - -// Exported `function` declaration: satisfies both fleet rules — -// socket/prefer-function-declaration (no module-scope arrow) and -// socket/export-top-level-functions (top-level functions must be exported). -export function run(code: string, id = 'src/example.ts'): string | undefined { - const plugin = defineGuardedPlugin({ - 'process.env.INLINED_EXTENSION_VERSION': JSON.stringify('2.0.3'), - }) - // rolldown's transform hook signature is (code, id); call it directly. - const transform = plugin.transform as ( - this: unknown, - code: string, - id: string, - ) => { code: string } | undefined - const result = transform.call({}, code, id) - return result?.code -} - -describe('defineGuardedPlugin', () => { - test('rewrites dot access', () => { - const out = run('const a = process.env.INLINED_EXTENSION_VERSION') - expect(out).toContain('"2.0.3"') - expect(out).not.toContain('INLINED_EXTENSION_VERSION') - }) - - test('rewrites single-quote bracket access', () => { - const out = run("const b = process.env['INLINED_EXTENSION_VERSION']") - expect(out).toContain('"2.0.3"') - expect(out).not.toContain('INLINED_EXTENSION_VERSION') - }) - - test('rewrites double-quote bracket access', () => { - const out = run('const c = process.env["INLINED_EXTENSION_VERSION"]') - expect(out).toContain('"2.0.3"') - expect(out).not.toContain('INLINED_EXTENSION_VERSION') - }) - - test('parses TypeScript source (type annotations do not abort the rewrite)', () => { - const out = run( - 'export function f(ctx: vscode.ExtensionContext): string {\n' + - " return process.env['INLINED_EXTENSION_VERSION']\n" + - '}\n', - ) - expect(out).toContain('"2.0.3"') - }) - - test('parses .tsx source', () => { - const out = run( - 'const e =
{process.env["INLINED_EXTENSION_VERSION"]}
', - 'src/example.tsx', - ) - expect(out).toContain('"2.0.3"') - }) - - test('leaves an assignment target untouched', () => { - const code = "process.env['INLINED_EXTENSION_VERSION'] = 'x'" - expect(run(code)).toBeUndefined() - }) - - test('leaves dynamic computed access untouched', () => { - const code = - 'const k = "INLINED_EXTENSION_VERSION"\nconst d = process.env[k]' - expect(run(code)).toBeUndefined() - }) - - test('does not rewrite an unrelated env var', () => { - const code = "const u = process.env['SOMETHING_ELSE']" - expect(run(code)).toBeUndefined() - }) -}) diff --git a/test/repo/unit/rolldown.config-extension-version.test.mts b/test/repo/unit/rolldown.config-extension-version.test.mts new file mode 100644 index 00000000..31bd0ede --- /dev/null +++ b/test/repo/unit/rolldown.config-extension-version.test.mts @@ -0,0 +1,65 @@ +import { describe, expect, test } from "vitest"; + +import { defineExtensionVersionPlugin } from "../../../.config/repo/rolldown.config.mts"; + +export function rewriteInlinedExtensionVersion( + code: string, + version = "2.0.3", +): string { + const plugin = defineExtensionVersionPlugin(version); + const transform = plugin.transform; + if (typeof transform !== "function") { + throw new TypeError("Extension version plugin must provide a transform."); + } + const result: unknown = Reflect.apply(transform, {}, [ + code, + "src/example.mts", + ]); + if (typeof result !== "string") { + throw new TypeError( + "Extension version plugin did not return transformed code.", + ); + } + return result; +} + +describe("defineExtensionVersionPlugin", () => { + test("rewrites single-quoted bracket access", () => { + expect( + rewriteInlinedExtensionVersion( + "const version = process.env['INLINED_EXTENSION_VERSION']", + ), + ).toBe('const version = "2.0.3"'); + }); + + test("rewrites double-quoted bracket access", () => { + expect( + rewriteInlinedExtensionVersion( + 'const version = process.env["INLINED_EXTENSION_VERSION"]', + ), + ).toBe('const version = "2.0.3"'); + }); + + test("rewrites TypeScript source", () => { + const source = + "export function readVersion(): string {\n" + + " return process.env['INLINED_EXTENSION_VERSION']\n" + + "}\n"; + expect(rewriteInlinedExtensionVersion(source)).toBe( + 'export function readVersion(): string {\n return "2.0.3"\n}\n', + ); + }); + + test("leaves dot and dynamic access untouched", () => { + const source = + "const a = process.env.INLINED_EXTENSION_VERSION\n" + + 'const key = "INLINED_EXTENSION_VERSION"\n' + + "const b = process.env[key]"; + expect(rewriteInlinedExtensionVersion(source)).toBe(source); + }); + + test("leaves unrelated environment variables untouched", () => { + const source = "const value = process.env['OTHER_VARIABLE']"; + expect(rewriteInlinedExtensionVersion(source)).toBe(source); + }); +}); diff --git a/test/repo/vitest.setup.mts b/test/repo/vitest.setup.mts new file mode 100644 index 00000000..b3bf3bbe --- /dev/null +++ b/test/repo/vitest.setup.mts @@ -0,0 +1,3 @@ +import nock from "nock"; + +nock.disableNetConnect(); diff --git a/tsconfig.json b/tsconfig.json index b3cce1b7..4de6d333 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,3 +1,3 @@ { - "extends": "./.config/fleet/tsconfig.base.json" + "extends": "./.config/repo/tsconfig.json" }