diff --git a/CHANGELOG.md b/CHANGELOG.md index b237075..77a84c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## Next + +### Fixes + +- Bump `adm-zip`, the Config Plugin's only runtime dependency, to 0.6.1. This clears GHSA-xcpc-8h2w-3j85 (high) and GHSA-vwc7-r8mq-g2x9 (moderate) from consumers' `npm audit`. Neither was reachable: the plugin never extracts to disk, and it only parses the AAR it downloaded after checking its SHA-256, or its own cached copy under `node_modules`. +- **Android**: the Config Plugin matches the maven repository URL exactly when deciding whether `android/build.gradle` already declares it, and ignores declarations inside `//` and `/* */` comments. The substring check it replaces skipped adding the repository when the URL appeared only in a comment or as the prefix of a longer URL. It was also the CodeQL `js/incomplete-url-substring-sanitization` alert. + ## 2.0.0 Upgrading from 1.x: every failure now rejects with a `DocuSignError`. Check any code that branches on `error.code`, matches message text, handles `status: 'error'` from `presentCaptiveSigning*`, or reads `errorCode` in an `addSigningErrorListener` callback. Codes are the lowercase codes the README documents, on both platforms, where iOS previously emitted `ERR_`-prefixed variants and Android rejected most failures as `signing_failed`. The new Android `launchStrategy` is opt-in. diff --git a/jest.config.js b/jest.config.js index 796539b..04ea9e9 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,12 +1,18 @@ /** @type {import('jest').Config} */ module.exports = { preset: 'jest-expo', - testMatch: ['/src/**/*.test.ts', '/src/**/*.test.tsx'], + testMatch: [ + '/src/**/*.test.ts', + '/src/**/*.test.tsx', + '/plugin/src/**/*.test.ts', + ], setupFiles: ['/jest.setup.js'], collectCoverageFrom: [ 'src/**/*.{ts,tsx}', '!src/**/*.test.{ts,tsx}', '!src/DocuSignModule.ts', + 'plugin/src/**/*.ts', + '!plugin/src/**/*.test.ts', ], coverageThreshold: { 'src/useDocuSignSigning.ts': { diff --git a/package-lock.json b/package-lock.json index 6f16327..609594f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,14 +9,13 @@ "version": "2.0.0", "license": "MIT", "dependencies": { - "adm-zip": "^0.5.17" + "adm-zip": "^0.6.1" }, "devDependencies": { "@amplitude/analytics-react-native": "^1.8.0", "@react-native/jest-preset": "^0.85.2", "@sentry/react-native": "^8.26.0", "@testing-library/react-native": "^13.3.3", - "@types/adm-zip": "^0.5.8", "@types/jest": "^29.5.14", "@types/node": "^20.19.39", "eslint": "^9.39.5", @@ -4952,16 +4951,6 @@ "node": ">= 10" } }, - "node_modules/@types/adm-zip": { - "version": "0.5.8", - "resolved": "https://registry.npmjs.org/@types/adm-zip/-/adm-zip-0.5.8.tgz", - "integrity": "sha512-RVVH7QvZYbN+ihqZ4kX/dMiowf6o+Jk1fNwiSdx0NahBJLU787zkULhGhJM8mf/obmLGmgdMM0bXsQTmyfbR7Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -5482,12 +5471,12 @@ } }, "node_modules/adm-zip": { - "version": "0.5.17", - "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.17.tgz", - "integrity": "sha512-+Ut8d9LLqwEvHHJl1+PIHqoyDxFgVN847JTVM3Izi3xHDWPE4UtzzXysMZQs64DMcrJfBeS/uoEP4AD3HQHnQQ==", + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.6.1.tgz", + "integrity": "sha512-Xwrja8nx9e5o2N1my4DsKCeKpdrnACyr1wtbPxBDgGzKzKyE9kRtBFA8mWldI+RVlD7CBZNWY/wQ2+ydwOR6kQ==", "license": "MIT", "engines": { - "node": ">=12.0" + "node": ">=14.0" } }, "node_modules/agent-base": { diff --git a/package.json b/package.json index fa40d6a..7eac90a 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,6 @@ "@react-native/jest-preset": "^0.85.2", "@sentry/react-native": "^8.26.0", "@testing-library/react-native": "^13.3.3", - "@types/adm-zip": "^0.5.8", "@types/jest": "^29.5.14", "@types/node": "^20.19.39", "eslint": "^9.39.5", @@ -65,6 +64,6 @@ "react-native": "*" }, "dependencies": { - "adm-zip": "^0.5.17" + "adm-zip": "^0.6.1" } } diff --git a/plugin/src/index.test.ts b/plugin/src/index.test.ts new file mode 100644 index 0000000..183f920 --- /dev/null +++ b/plugin/src/index.test.ts @@ -0,0 +1,156 @@ +import { describe, expect, it } from '@jest/globals'; +import type { ExportedConfig } from 'expo/config-plugins'; + +import withDocuSign, { type DocuSignPluginProps } from './index'; + +const DOCUSIGN_REPO = 'https://docucdn-a.akamaihd.net/prod/docusignandroidsdk'; +const CUSTOM_REPO = 'https://maven.example.com/docusign'; + +function buildProjectGradle(repositories: string[]): string { + return [ + 'buildscript {', + ' repositories {', + ' google()', + ' }', + '}', + '', + 'allprojects {', + ' repositories {', + ...repositories.map((line) => ` ${line}`), + ' google()', + ' mavenCentral()', + ' }', + '}', + '', + ].join('\n'); +} + +async function applyProjectBuildGradleMods( + contents: string, + props?: DocuSignPluginProps, +): Promise { + const config: ExportedConfig = withDocuSign( + { name: 'app', slug: 'app' }, + props, + ); + const mod = config.mods?.android?.projectBuildGradle; + if (!mod) { + throw new Error('withDocuSign registered no projectBuildGradle mod'); + } + const result = await mod({ + ...config, + modRawConfig: config, + modResults: { path: 'android/build.gradle', language: 'groovy', contents }, + modRequest: { + projectRoot: '/app', + platformProjectRoot: '/app/android', + modName: 'projectBuildGradle', + platform: 'android', + introspect: false, + }, + }); + return result.modResults.contents; +} + +function countOccurrences(contents: string, needle: string): number { + return contents.split(needle).length - 1; +} + +describe('withDocuSign android maven repository', () => { + it('adds the DocuSign repository inside allprojects.repositories', async () => { + const contents = await applyProjectBuildGradleMods(buildProjectGradle([])); + + expect(contents).toContain( + `allprojects {\n repositories {\n maven { url "${DOCUSIGN_REPO}" }`, + ); + }); + + it('adds androidMavenRepo instead of the DocuSign repository when provided', async () => { + const contents = await applyProjectBuildGradleMods(buildProjectGradle([]), { + androidMavenRepo: CUSTOM_REPO, + }); + + expect(contents).toContain(`maven { url "${CUSTOM_REPO}" }`); + expect(countOccurrences(contents, DOCUSIGN_REPO)).toBe(0); + }); + + it.each([ + ['a double-quoted url', `maven { url "${DOCUSIGN_REPO}" }`], + ['a single-quoted url', `maven { url '${DOCUSIGN_REPO}' }`], + ['a uri() assignment', `maven { url = uri("${DOCUSIGN_REPO}") }`], + ['a trailing slash', `maven { url "${DOCUSIGN_REPO}/" }`], + ['a multi-line block', `maven {\n url "${DOCUSIGN_REPO}"\n }`], + ])( + 'does not add the repository when build.gradle declares it with %s', + async (_label, declaration) => { + const contents = await applyProjectBuildGradleMods( + buildProjectGradle([declaration]), + ); + + expect(countOccurrences(contents, DOCUSIGN_REPO)).toBe(1); + }, + ); + + it('does not add the repository when a declaration is followed by a line comment', async () => { + const contents = await applyProjectBuildGradleMods( + buildProjectGradle([`maven { url "${DOCUSIGN_REPO}" } // DocuSign SDK`]), + ); + + expect(countOccurrences(contents, DOCUSIGN_REPO)).toBe(1); + }); + + it('does not add the repository when an earlier string on its line contains an escaped quote', async () => { + const contents = await applyProjectBuildGradleMods( + buildProjectGradle([ + `def note = "a\\"b"; maven { url "${DOCUSIGN_REPO}" }`, + ]), + ); + + expect(countOccurrences(contents, DOCUSIGN_REPO)).toBe(1); + }); + + it('does not treat comment markers inside strings as comments', async () => { + const contents = await applyProjectBuildGradleMods( + buildProjectGradle([ + "flatDir { dirs 'libs/*' }", + `maven { url "${DOCUSIGN_REPO}" }`, + "flatDir { dirs 'vendor/**/' }", + ]), + ); + + expect(countOccurrences(contents, DOCUSIGN_REPO)).toBe(1); + }); + + it.each([ + ['a line comment', `// maven { url "${DOCUSIGN_REPO}" }`], + ['a block comment', `/* maven { url "${DOCUSIGN_REPO}" } */`], + [ + 'a multi-line block comment', + `/*\n maven { url "${DOCUSIGN_REPO}" }\n */`, + ], + ])( + 'adds the repository when the only declaration is inside %s', + async (_label, declaration) => { + const contents = await applyProjectBuildGradleMods( + buildProjectGradle([declaration]), + ); + + expect(countOccurrences(contents, DOCUSIGN_REPO)).toBe(2); + }, + ); + + it('adds the repository when build.gradle only declares a longer url with the same prefix', async () => { + const contents = await applyProjectBuildGradleMods( + buildProjectGradle([`maven { url "${DOCUSIGN_REPO}/legacy" }`]), + ); + + expect(countOccurrences(contents, DOCUSIGN_REPO)).toBe(2); + }); + + it('leaves build.gradle unchanged when the plugin runs again', async () => { + const firstRun = await applyProjectBuildGradleMods(buildProjectGradle([])); + const secondRun = await applyProjectBuildGradleMods(firstRun); + + expect(secondRun).toBe(firstRun); + }); +}); diff --git a/plugin/src/index.ts b/plugin/src/index.ts index ab465b2..0f7f52b 100644 --- a/plugin/src/index.ts +++ b/plugin/src/index.ts @@ -80,6 +80,23 @@ const withDocuSignAndroidPermissions: ConfigPlugin = (config) => { }); }; +function normalizeRepoUrl(url: string): string { + return url.trim().replace(/\/+$/, ''); +} + +const GRADLE_COMMENT_OR_STRING = + /\/\*[\s\S]*?\*\/|\/\/[^\n]*|"((?:[^"\\\n]|\\.)*)"|'((?:[^'\\\n]|\\.)*)'/g; + +function hasMavenRepo(contents: string, repo: string): boolean { + const target = normalizeRepoUrl(repo); + return Array.from(contents.matchAll(GRADLE_COMMENT_OR_STRING)).some( + ([, doubleQuoted, singleQuoted]) => { + const literal = doubleQuoted ?? singleQuoted; + return literal !== undefined && normalizeRepoUrl(literal) === target; + }, + ); +} + const withDocuSignAndroidMavenRepo: ConfigPlugin = ( config, props, @@ -97,7 +114,7 @@ const withDocuSignAndroidMavenRepo: ConfigPlugin = ( return cfg; } - if (cfg.modResults.contents.includes(repo)) { + if (hasMavenRepo(cfg.modResults.contents, repo)) { return cfg; } diff --git a/plugin/tsconfig.build.json b/plugin/tsconfig.build.json index 8d6a27e..e01a568 100644 --- a/plugin/tsconfig.build.json +++ b/plugin/tsconfig.build.json @@ -8,5 +8,11 @@ "sourceMap": true }, "include": ["./src"], - "exclude": ["**/__mocks__/*", "**/__tests__/*", "build", "node_modules"] + "exclude": [ + "**/__mocks__/*", + "**/__tests__/*", + "**/*.test.ts", + "build", + "node_modules" + ] }