diff --git a/.eslintrc.json b/.eslintrc.json index d56654ad3..09821ff5c 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,7 +1,39 @@ { "root": true, - "plugins": ["@typescript-eslint", "@nx"], + "plugins": ["@typescript-eslint", "@nx", "header"], "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "excludedFiles": [ + "**/*.spec.ts", + "**/*.spec.tsx", + "**/*.test.ts", + "**/*.test.tsx", + "**/*.spec.js", + "**/*.spec.jsx", + "**/*.test.js", + "**/*.test.jsx", + "**/*.test.mjs", + "**/*.spec.mjs", + "**/vite*.config.ts", + "**/vitest.setup.ts", + "**/playwright.config.ts", + "**/_polyfills/**", + "tools/**" + ], + "rules": { + "header/header": [ + "warn", + "block", + [ + { + "pattern": "[\\s\\S]*Copyright[\\s\\S]*Ping Identity[\\s\\S]*", + "template": "\n * Copyright (c) Ping Identity Corporation. All rights reserved.\n * This software may be modified and distributed under the terms\n * of the MIT license. See the LICENSE file for details.\n " + } + ] + ] + } + }, { "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], "rules": { diff --git a/.husky/pre-commit b/.husky/pre-commit index e75ce0cb6..6a163a88e 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,4 +1,4 @@ #!/bin/sh . "$(dirname "$0")/_/husky.sh" -npx lint-staged && npx nx affected:lint && npx nx affected:build +node tools/copyright/sync-header-years.mjs && npx lint-staged && npx nx affected:lint && npx nx affected:build diff --git a/e2e/autoscript-apps/src/index.ts b/e2e/autoscript-apps/src/index.ts index be15a9f01..4d153b792 100644 --- a/e2e/autoscript-apps/src/index.ts +++ b/e2e/autoscript-apps/src/index.ts @@ -1,2 +1,11 @@ +/* + * @forgerock/javascript-sdk + * + * index.ts + * + * Copyright (c) 2021 - 2026 Ping Identity Corporation. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ import 'core-js/stable'; import 'regenerator-runtime'; diff --git a/e2e/autoscript-suites/config.ts b/e2e/autoscript-suites/config.ts index 97fcc1e94..3d9c6bd98 100644 --- a/e2e/autoscript-suites/config.ts +++ b/e2e/autoscript-suites/config.ts @@ -1,3 +1,8 @@ +/* + * Copyright (c) 2021 - 2026 Ping Identity Corporation. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ import { workspaceRoot } from '@nx/devkit'; import { PlaywrightTestConfig } from '@playwright/test'; import { baseConfig } from './playwright.config'; diff --git a/e2e/mock-api/src/environments/environment.prod.ts b/e2e/mock-api/src/environments/environment.prod.ts index c9669790b..95d059420 100644 --- a/e2e/mock-api/src/environments/environment.prod.ts +++ b/e2e/mock-api/src/environments/environment.prod.ts @@ -1,3 +1,8 @@ +/* + * Copyright (c) 2021 - 2026 Ping Identity Corporation. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ export const environment = { production: true, }; diff --git a/e2e/mock-api/src/environments/environment.ts b/e2e/mock-api/src/environments/environment.ts index a24f6ba2c..cd0e59d7b 100644 --- a/e2e/mock-api/src/environments/environment.ts +++ b/e2e/mock-api/src/environments/environment.ts @@ -1,3 +1,8 @@ +/* + * Copyright (c) 2021 - 2026 Ping Identity Corporation. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ export const environment = { AM_URL: 'https://openam-crbrl-01.forgeblocks.com/am/', REALM_PATH: 'alpha', diff --git a/e2e/token-vault-suites/teardown.ts b/e2e/token-vault-suites/teardown.ts index 09b49e70b..4290bcfb2 100644 --- a/e2e/token-vault-suites/teardown.ts +++ b/e2e/token-vault-suites/teardown.ts @@ -1,3 +1,8 @@ +/* + * Copyright (c) 2021 - 2026 Ping Identity Corporation. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ export default () => { console.log('tests finished'); }; diff --git a/package.json b/package.json index 76ce17a3e..0813b36e6 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,8 @@ "ci:version": "changeset version && pnpm install --no-frozen-lockfile && pnpm nx format:write --uncommitted", "changeset": "changeset", "commit": "git cz", + "copyright:check": "node ./tools/copyright/sync-header-years.mjs --check", + "copyright:sync": "node ./tools/copyright/sync-header-years.mjs", "docs": "nx affected --target=typedoc", "e2e": "CI=true nx affected:e2e", "format:staged": "pretty-quick --staged", @@ -83,6 +85,7 @@ "esbuild": "^0.19.2", "eslint": "8.57.0", "eslint-config-prettier": "9.1.0", + "eslint-plugin-header": "^3.1.1", "eslint-plugin-import": "2.27.5", "eslint-plugin-playwright": "^1.5.1", "eslint-plugin-prettier": "^5.1.3", diff --git a/packages/javascript-sdk/src/device-client/mock-data/msw-mock-data.ts b/packages/javascript-sdk/src/device-client/mock-data/msw-mock-data.ts index c204c0fa8..118e2f5d5 100644 --- a/packages/javascript-sdk/src/device-client/mock-data/msw-mock-data.ts +++ b/packages/javascript-sdk/src/device-client/mock-data/msw-mock-data.ts @@ -1,3 +1,12 @@ +/* + * @forgerock/javascript-sdk + * + * msw-mock-data.ts + * + * Copyright (c) 2025 - 2026 Ping Identity Corporation. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ import { GeneralResponse } from '../services/index.js'; import type { OathResponse, diff --git a/packages/javascript-sdk/src/fr-recovery-codes/script-text.mock.data.ts b/packages/javascript-sdk/src/fr-recovery-codes/script-text.mock.data.ts index f5eda7b2a..a9ceb18ec 100644 --- a/packages/javascript-sdk/src/fr-recovery-codes/script-text.mock.data.ts +++ b/packages/javascript-sdk/src/fr-recovery-codes/script-text.mock.data.ts @@ -1,13 +1,13 @@ -/* eslint-disable no-useless-escape */ /* * @forgerock/javascript-sdk * * script-text.mock.data.ts * - * Copyright (c) 2020 - 2025 Ping Identity Corporation. All rights reserved. + * Copyright (c) 2020 - 2026 Ping Identity Corporation. All rights reserved. * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ +/* eslint-disable no-useless-escape */ import type { CallbackType } from '../auth/enums'; diff --git a/packages/javascript-sdk/src/fr-webauthn/script-parser.ts b/packages/javascript-sdk/src/fr-webauthn/script-parser.ts index 2b58927c1..997dc76ef 100644 --- a/packages/javascript-sdk/src/fr-webauthn/script-parser.ts +++ b/packages/javascript-sdk/src/fr-webauthn/script-parser.ts @@ -1,13 +1,13 @@ -/* eslint-disable no-useless-escape */ /* * @forgerock/javascript-sdk * * script-parser.ts * - * Copyright (c) 2020 - 2025 Ping Identity Corporation. All rights reserved. + * Copyright (c) 2020 - 2026 Ping Identity Corporation. All rights reserved. * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ +/* eslint-disable no-useless-escape */ import { WebAuthnOutcomeType } from './enums'; import { ensureArray, getIndexOne, parsePubKeyArray, parseCredentials } from './helpers'; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 46583f484..308a09931 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -154,6 +154,9 @@ importers: eslint-config-prettier: specifier: 9.1.0 version: 9.1.0(eslint@8.57.0) + eslint-plugin-header: + specifier: ^3.1.1 + version: 3.1.1(eslint@8.57.0) eslint-plugin-import: specifier: 2.27.5 version: 2.27.5(@typescript-eslint/parser@7.16.1(eslint@8.57.0)(typescript@5.6.3))(eslint@8.57.0) @@ -6522,6 +6525,14 @@ packages: eslint-import-resolver-webpack: optional: true + eslint-plugin-header@3.1.1: + resolution: + { + integrity: sha512-9vlKxuJ4qf793CmeeSrZUvVClw6amtpghq3CuWcB5cUNnWHQhgcqy5eF8oVKFk1G3Y/CbchGfEaw3wiIJaNmVg==, + } + peerDependencies: + eslint: '>=7.7.0' + eslint-plugin-import@2.27.5: resolution: { @@ -7259,6 +7270,7 @@ packages: integrity: sha512-ICsMM1Wk8xSGMowkOmPrzo2Fgmfo4bMHLNX6ytHjajRJUqvHOw/TFapQ+QG75c3X/tTDDhOSRPGC52dDbNM8FQ==, } engines: { node: '>=16' } + deprecated: This package is no longer maintained. For the JavaScript API, please use @conventional-changelog/git-client instead. hasBin: true glob-parent@5.1.2: @@ -7286,6 +7298,7 @@ packages: { integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==, } + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me hasBin: true glob@6.0.4: @@ -7293,14 +7306,14 @@ packages: { integrity: sha512-MKZeRNyYZAVVVG1oZeLaWie1uweH40m9AZwIwxyPbTSX4hHrVYSzLg0Ro5Z5R7XKkIX+Cc6oD1rqeDJnwsB8/A==, } - deprecated: Glob versions prior to v9 are no longer supported + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me glob@7.2.3: resolution: { integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==, } - deprecated: Glob versions prior to v9 are no longer supported + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me global-agent@3.0.0: resolution: @@ -12918,6 +12931,7 @@ packages: integrity: sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==, } engines: { node: '>=12' } + deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation whatwg-mimetype@3.0.0: resolution: @@ -17704,6 +17718,10 @@ snapshots: transitivePeerDependencies: - supports-color + eslint-plugin-header@3.1.1(eslint@8.57.0): + dependencies: + eslint: 8.57.0 + eslint-plugin-import@2.27.5(@typescript-eslint/parser@7.16.1(eslint@8.57.0)(typescript@5.6.3))(eslint@8.57.0): dependencies: array-includes: 3.1.8 diff --git a/tools/copyright/sync-header-years.mjs b/tools/copyright/sync-header-years.mjs new file mode 100644 index 000000000..d1831af3f --- /dev/null +++ b/tools/copyright/sync-header-years.mjs @@ -0,0 +1,191 @@ +#!/usr/bin/env node + +import { execFileSync } from 'node:child_process'; +import { readFileSync, statSync, writeFileSync } from 'node:fs'; +import { resolve } from 'node:path'; +import { pathToFileURL } from 'node:url'; + +function isCliExecution() { + return process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href; +} + +function run() { + const args = new Set(process.argv.slice(2)); + const checkOnly = args.has('--check'); + const currentYear = new Date().getFullYear(); + + const stagedFiles = getStagedFiles(); + const stagedFileData = []; + const invalidFiles = []; + const changedFiles = []; + + for (const file of stagedFiles) { + if (!isFile(file) || isExcluded(file)) { + continue; + } + const absolutePath = resolve(process.cwd(), file); + const original = safeReadUtf8(absolutePath); + if (original === null) { + continue; + } + stagedFileData.push({ file, absolutePath, original }); + if (hasInvalidPingCopyrightHeader(original)) { + invalidFiles.push(file); + } + } + + if (invalidFiles.length > 0) { + console.error('Invalid Ping copyright header year format in staged files:'); + for (const file of invalidFiles) { + console.error(`- ${file}`); + } + process.exit(1); + } + + const missingHeaderFiles = []; + for (const { file, original } of stagedFileData) { + if (SOURCE_FILE_PATTERN.test(file) && !hasPingCopyrightHeader(original)) { + missingHeaderFiles.push(file); + } + } + + if (missingHeaderFiles.length > 0) { + console.error('Missing Ping copyright header in staged files:'); + for (const file of missingHeaderFiles) { + console.error(`- ${file}`); + } + process.exit(1); + } + + for (const { file, absolutePath, original } of stagedFileData) { + const updated = updateCopyrightYears(original, currentYear); + if (updated === original) { + continue; + } + changedFiles.push(file); + if (!checkOnly) { + writeFileSync(absolutePath, updated, 'utf8'); + } + } + + if (!checkOnly && changedFiles.length > 0) { + execFileSync('git', ['add', '--', ...changedFiles], { stdio: 'inherit' }); + } + + if (checkOnly && changedFiles.length > 0) { + console.error('Stale Ping copyright years found in staged files:'); + for (const file of changedFiles) { + console.error(`- ${file}`); + } + process.exit(1); + } +} + +function getStagedFiles() { + const output = execFileSync('git', ['diff', '--cached', '--name-only', '--diff-filter=ACMR'], { + encoding: 'utf8', + }).trim(); + + if (!output) { + return []; + } + return output.split('\n').filter(Boolean); +} + +export function isExcluded(filePath) { + return EXCLUDE_PATTERNS.some((pattern) => pattern.test(filePath)); +} + +const EXCLUDE_PATTERNS = [ + /\.test\.[cm]?[jt]sx?$/i, + /\.spec\.[cm]?[jt]sx?$/i, + /(^|[/\\])dist[/\\]/, + /(^|[/\\])vendor[/\\]/, + /(^|[/\\])node_modules[/\\]/, + /(^|[/\\])tools[/\\]/, + /(^|[/\\])_polyfills[/\\]/, + /(^|[/\\])vite[^/\\]*\.config\.[cm]?[jt]sx?$/i, + /(^|[/\\])vitest\.setup\.[cm]?[jt]sx?$/i, + /(^|[/\\])playwright\.config\.[cm]?[jt]sx?$/i, +]; + +function isFile(filePath) { + try { + return statSync(filePath).isFile(); + } catch { + return false; + } +} + +function safeReadUtf8(filePath) { + try { + return readFileSync(filePath, 'utf8'); + } catch { + return null; + } +} + +export function updateCopyrightYears(content, year) { + const regex = + /(^.*(?:©\s*|©\s*)?Copyright(?:\s*\(c\))?\s+)(\d{4})(?:([ \t]*-[ \t]*)(\d{4}))?(\s+Ping Identity(?: Corporation)?\b.*$)/gim; + + return content.replace(regex, (_, prefix, startYear, separator, endYear, suffix) => { + const start = Number.parseInt(startYear, 10); + const end = endYear ? Number.parseInt(endYear, 10) : start; + + if (Number.isNaN(start) || Number.isNaN(end)) { + return `${prefix}${startYear}${endYear ? `${separator}${endYear}` : ''}${suffix}`; + } + + const resolvedEnd = end >= year ? end : year; + + if (!endYear) { + // Single year already current — no range needed + if (resolvedEnd === start) { + return `${prefix}${startYear}${suffix}`; + } + return `${prefix}${startYear} - ${resolvedEnd}${suffix}`; + } + + // Always normalize separator to ' - ' and bump end year when stale + return `${prefix}${startYear} - ${resolvedEnd}${suffix}`; + }); +} + +export function hasInvalidPingCopyrightHeader(content) { + const lines = content.split(/\r?\n/); + for (const line of lines) { + if (!MAYBE_PING_COPYRIGHT_LINE_REGEX.test(line)) { + continue; + } + if (!HEADER_COMMENT_LINE_REGEX.test(line)) { + continue; + } + if (!VALID_PING_COPYRIGHT_LINE_REGEX.test(line)) { + return true; + } + } + return false; +} + +export function hasPingCopyrightHeader(content) { + const lines = content.split(/\r?\n/); + for (const line of lines) { + if (MAYBE_PING_COPYRIGHT_LINE_REGEX.test(line) && HEADER_COMMENT_LINE_REGEX.test(line)) { + return true; + } + } + return false; +} + +const SOURCE_FILE_PATTERN = /\.[cm]?[jt]sx?$/i; + +const MAYBE_PING_COPYRIGHT_LINE_REGEX = + /(?:©\s*|©\s*)?Copyright(?:\s*\(c\))?.*Ping Identity(?: Corporation)?/i; +const HEADER_COMMENT_LINE_REGEX = /^\s*(?:\/\*+|\*+|\/\/+|#+|', + ].join('\n'); + const actual = updateCopyrightYears(input, 2026); + assert.equal( + actual, + [ + '/* © Copyright 2020 - 2026 Ping Identity. */', + '', + ].join('\n'), + ); +}); + +test('does not update non-Ping headers', () => { + const input = '/* Copyright 2020-2025 Example Corp. */'; + const actual = updateCopyrightYears(input, 2026); + assert.equal(actual, input); +}); + +test('updates Ping Identity Corporation ranges with spaces and (c)', () => { + const input = '/* Copyright (c) 2023 - 2024 Ping Identity Corporation. All right reserved. */'; + const actual = updateCopyrightYears(input, 2026); + assert.equal( + actual, + '/* Copyright (c) 2023 - 2026 Ping Identity Corporation. All right reserved. */', + ); +}); + +test('expands stale single year with (c) to a range for Ping Identity Corporation', () => { + const input = '/* Copyright (c) 2023 Ping Identity Corporation. All right reserved. */'; + const actual = updateCopyrightYears(input, 2026); + assert.equal( + actual, + '/* Copyright (c) 2023 - 2026 Ping Identity Corporation. All right reserved. */', + ); +}); + +test('flags Ping headers without a valid year', () => { + const input = '/* Copyright Ping Identity Corporation. All right reserved. */'; + assert.equal(hasInvalidPingCopyrightHeader(input), true); +}); + +test('flags Ping headers with a placeholder', () => { + const input = + '/* Copyright (c) Ping Identity Corporation. All rights reserved. */'; + assert.equal(hasInvalidPingCopyrightHeader(input), true); +}); + +test('does not flag valid Ping headers', () => { + const input = '/* Copyright (c) 2020 - 2026 Ping Identity Corporation. */'; + assert.equal(hasInvalidPingCopyrightHeader(input), false); +}); + +test('does not flag non-header Ping copyright text', () => { + const input = 'This document is Copyright Ping Identity Corporation.'; + assert.equal(hasInvalidPingCopyrightHeader(input), false); +}); + +test('excludes test files from processing', () => { + assert.equal(isExcluded('src/foo.test.ts'), true); + assert.equal(isExcluded('src/foo.test.mjs'), true); + assert.equal(isExcluded('src/foo.spec.js'), true); +}); + +test('excludes dist and vendor paths from processing', () => { + assert.equal(isExcluded('dist/foo.js'), true); + assert.equal(isExcluded('vendor/lib.js'), true); +}); + +test('excludes vite.config and vitest.setup files from processing', () => { + assert.equal(isExcluded('vite.config.ts'), true); + assert.equal(isExcluded('packages/foo/vite.config.ts'), true); + assert.equal(isExcluded('e2e/token-vault-app/vite.interceptor.config.ts'), true); + assert.equal(isExcluded('vitest.setup.ts'), true); + assert.equal(isExcluded('packages/foo/vitest.setup.ts'), true); +}); + +test('excludes playwright.config files from processing', () => { + assert.equal(isExcluded('e2e/autoscript-suites/playwright.config.ts'), true); + assert.equal(isExcluded('e2e/token-vault-suites/playwright.config.ts'), true); +}); + +test('excludes _polyfills/ directory from processing', () => { + assert.equal(isExcluded('e2e/autoscript-apps/src/_polyfills/fast-text-encoder.js'), true); +}); + +test('excludes tools/ directory from processing', () => { + assert.equal(isExcluded('tools/copyright/sync-header-years.mjs'), true); + assert.equal(isExcluded('tools/release/local.mjs'), true); +}); + +test('does not exclude regular source files', () => { + assert.equal(isExcluded('src/foo.ts'), false); + assert.equal(isExcluded('packages/sdk/src/index.ts'), false); +}); + +test('hasPingCopyrightHeader detects valid block comment header', () => { + const input = '/* Copyright (c) 2020 - 2026 Ping Identity Corporation. All rights reserved. */'; + assert.equal(hasPingCopyrightHeader(input), true); +}); + +test('hasPingCopyrightHeader detects header in multi-line block comment', () => { + const input = `/* + * @forgerock/javascript-sdk + * + * Copyright (c) 2020 - 2026 Ping Identity Corporation. All rights reserved. + */`; + assert.equal(hasPingCopyrightHeader(input), true); +}); + +test('hasPingCopyrightHeader returns false when no Ping copyright present', () => { + const input = `/* + * Some other library header + */ +export const x = 1;`; + assert.equal(hasPingCopyrightHeader(input), false); +}); + +test('hasPingCopyrightHeader returns false for non-comment Ping copyright text', () => { + const input = 'This document is Copyright Ping Identity Corporation.'; + assert.equal(hasPingCopyrightHeader(input), false); +});