Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
8 changes: 7 additions & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
/** @type {import('jest').Config} */
module.exports = {
preset: 'jest-expo',
testMatch: ['<rootDir>/src/**/*.test.ts', '<rootDir>/src/**/*.test.tsx'],
testMatch: [
'<rootDir>/src/**/*.test.ts',
'<rootDir>/src/**/*.test.tsx',
'<rootDir>/plugin/src/**/*.test.ts',
],
setupFiles: ['<rootDir>/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': {
Expand Down
21 changes: 5 additions & 16 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -65,6 +64,6 @@
"react-native": "*"
},
"dependencies": {
"adm-zip": "^0.5.17"
"adm-zip": "^0.6.1"
}
}
156 changes: 156 additions & 0 deletions plugin/src/index.test.ts
Original file line number Diff line number Diff line change
@@ -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<string> {
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);
});
});
19 changes: 18 additions & 1 deletion plugin/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<DocuSignPluginProps> = (
config,
props,
Expand All @@ -97,7 +114,7 @@ const withDocuSignAndroidMavenRepo: ConfigPlugin<DocuSignPluginProps> = (
return cfg;
}

if (cfg.modResults.contents.includes(repo)) {
if (hasMavenRepo(cfg.modResults.contents, repo)) {
return cfg;
}

Expand Down
8 changes: 7 additions & 1 deletion plugin/tsconfig.build.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,11 @@
"sourceMap": true
},
"include": ["./src"],
"exclude": ["**/__mocks__/*", "**/__tests__/*", "build", "node_modules"]
"exclude": [
"**/__mocks__/*",
"**/__tests__/*",
"**/*.test.ts",
"build",
"node_modules"
]
}
Loading