@algorandfoundation/react-native-passkey-autofill 1.0.0-canary.2 → 1.0.0-canary.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.nvmrc +1 -0
- package/.oxfmtrc.json +4 -0
- package/.oxlintrc.json +14 -0
- package/.releaserc +30 -0
- package/ARCHITECTURE.md +8 -0
- package/CHANGELOG.md +115 -0
- package/CONTRIBUTING.md +15 -5
- package/README.md +150 -11
- package/android/build.gradle +33 -10
- package/android/libs/repo/co/algorand/dP256Android/1.0.0/dP256Android-1.0.0.pom +9 -0
- package/android/src/main/AndroidManifest.xml +9 -3
- package/android/src/main/java/co/algorand/passkeyautofill/CreatePasskeyActivity.kt +396 -44
- package/android/src/main/java/co/algorand/passkeyautofill/GetPasskeyActivity.kt +583 -44
- package/android/src/main/java/co/algorand/passkeyautofill/Prf.kt +184 -0
- package/android/src/main/java/co/algorand/passkeyautofill/ReactNativePasskeyAutofillModule.kt +190 -2
- package/android/src/main/java/co/algorand/passkeyautofill/WebAuthn.kt +135 -0
- package/android/src/main/java/co/algorand/passkeyautofill/auth/BiometricRequirement.kt +86 -0
- package/android/src/main/java/co/algorand/passkeyautofill/credentials/Credential.kt +2 -1
- package/android/src/main/java/co/algorand/passkeyautofill/credentials/CredentialRepository.kt +314 -53
- package/android/src/main/java/co/algorand/passkeyautofill/service/PasskeyAutofillCredentialProviderService.kt +152 -38
- package/android/src/main/java/co/algorand/passkeyautofill/utils/PasskeyUtils.kt +119 -0
- package/android/src/main/res/values/styles.xml +10 -0
- package/app.plugin.js +593 -60
- package/assets/banner.png +0 -0
- package/build/ReactNativePasskeyAutofill.types.d.ts +41 -1
- package/build/ReactNativePasskeyAutofill.types.d.ts.map +1 -1
- package/build/ReactNativePasskeyAutofill.types.js +3 -1
- package/build/ReactNativePasskeyAutofill.types.js.map +1 -1
- package/build/ReactNativePasskeyAutofillModule.d.ts +41 -4
- package/build/ReactNativePasskeyAutofillModule.d.ts.map +1 -1
- package/build/ReactNativePasskeyAutofillModule.js +21 -2
- package/build/ReactNativePasskeyAutofillModule.js.map +1 -1
- package/build/index.d.ts +2 -2
- package/build/index.d.ts.map +1 -1
- package/build/index.js +2 -2
- package/build/index.js.map +1 -1
- package/e2e/README.md +155 -0
- package/e2e/driver/adb.ts +73 -0
- package/e2e/driver/capabilities.ts +106 -0
- package/e2e/driver/credentialProvider.ts +104 -0
- package/e2e/driver/fingerprint.ts +885 -0
- package/e2e/driver/selectors.ts +103 -0
- package/e2e/driver/session.ts +25 -0
- package/e2e/jest.config.ts +20 -0
- package/e2e/package.json +11 -0
- package/e2e/tests/passkey.test.ts +190 -0
- package/e2e/tsconfig.json +16 -0
- package/ios/AutofillCredentialProvider/BiometricRequirement.swift +34 -0
- package/ios/AutofillCredentialProvider/CredentialProviderViewController.swift +570 -0
- package/ios/AutofillCredentialProvider/PasskeyAutofillCredentialProvider-Bridging-Header.h +1 -0
- package/ios/AutofillCredentialProvider/PasskeyCredentialStore.swift +612 -0
- package/ios/AutofillCredentialProvider/PasskeyKeystoreMMKV.h +25 -0
- package/ios/AutofillCredentialProvider/PasskeyKeystoreMMKV.mm +86 -0
- package/ios/AutofillCredentialProvider/Prf.swift +89 -0
- package/ios/AutofillCredentialProvider/WebAuthn.swift +186 -0
- package/ios/ReactNativePasskeyAutofill.podspec +11 -2
- package/ios/ReactNativePasskeyAutofillModule.swift +170 -15
- package/lefthook.yml +14 -0
- package/package.json +115 -25
- package/pnpm-workspace.yaml +36 -0
- package/src/ReactNativePasskeyAutofill.types.ts +38 -0
- package/src/ReactNativePasskeyAutofillModule.ts +66 -3
- package/src/index.ts +2 -2
- package/tsconfig.json +2 -2
- package/.eslintrc.js +0 -5
- /package/android/libs/{dP256Android-release.aar → repo/co/algorand/dP256Android/1.0.0/dP256Android-1.0.0.aar} +0 -0
package/app.plugin.js
CHANGED
|
@@ -1,14 +1,507 @@
|
|
|
1
1
|
const {
|
|
2
2
|
withAndroidManifest,
|
|
3
|
+
withEntitlementsPlist,
|
|
4
|
+
withInfoPlist,
|
|
3
5
|
withStringsXml,
|
|
6
|
+
withAppBuildGradle,
|
|
4
7
|
withProjectBuildGradle,
|
|
5
8
|
withMainApplication,
|
|
6
9
|
withDangerousMod,
|
|
10
|
+
withXcodeProject,
|
|
7
11
|
AndroidConfig,
|
|
8
12
|
} = require("@expo/config-plugins");
|
|
9
13
|
const path = require("path");
|
|
10
14
|
const fs = require("fs");
|
|
11
15
|
|
|
16
|
+
const IOS_EXTENSION_NAME = "PasskeyAutofillCredentialProvider";
|
|
17
|
+
const IOS_EXTENSION_FILES = [
|
|
18
|
+
"CredentialProviderViewController.swift",
|
|
19
|
+
"PasskeyKeystoreMMKV.h",
|
|
20
|
+
"PasskeyKeystoreMMKV.mm",
|
|
21
|
+
"PasskeyAutofillCredentialProvider-Bridging-Header.h",
|
|
22
|
+
"PasskeyCredentialStore.swift",
|
|
23
|
+
"WebAuthn.swift",
|
|
24
|
+
"BiometricRequirement.swift",
|
|
25
|
+
"Prf.swift",
|
|
26
|
+
];
|
|
27
|
+
const IOS_EXTENSION_SOURCE_FILES = IOS_EXTENSION_FILES.filter((file) => !file.endsWith(".h"));
|
|
28
|
+
const DETERMINISTIC_P256_PACKAGE_URL =
|
|
29
|
+
"https://github.com/algorandfoundation/deterministic-P256-swift/";
|
|
30
|
+
const DETERMINISTIC_P256_PRODUCT_NAME = "deterministicP256-swift";
|
|
31
|
+
|
|
32
|
+
const getAssociatedDomain = (site) => {
|
|
33
|
+
try {
|
|
34
|
+
return new URL(site).host;
|
|
35
|
+
} catch {
|
|
36
|
+
return site.replace(/^https?:\/\//, "").split("/")[0];
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const getIosBundleIdentifier = (config) =>
|
|
41
|
+
config.ios?.bundleIdentifier ||
|
|
42
|
+
config.ios?.bundleIdentifierOverride ||
|
|
43
|
+
"co.algorand.auth.example";
|
|
44
|
+
|
|
45
|
+
const getAppGroup = (config, props) =>
|
|
46
|
+
props.appGroup || `group.${getIosBundleIdentifier(config)}.passkey-autofill`;
|
|
47
|
+
|
|
48
|
+
const AAGUID_REGEX =
|
|
49
|
+
/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
|
|
50
|
+
|
|
51
|
+
// Optional authenticator AAGUID. When set, both platforms embed it in attestation
|
|
52
|
+
// responses so the credential provider presents a consistent identity to relying parties.
|
|
53
|
+
const getAaguid = (props) => {
|
|
54
|
+
if (!props.aaguid) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
const value = String(props.aaguid).trim();
|
|
58
|
+
if (!AAGUID_REGEX.test(value)) {
|
|
59
|
+
throw new Error(
|
|
60
|
+
`react-native-passkey-autofill: "aaguid" must be a UUID string, received "${props.aaguid}".`,
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
return value;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const BIOMETRIC_REQUIREMENT_VALUES = ["strong", "strongOrCredential", "weakOrCredential"];
|
|
67
|
+
const DEFAULT_BIOMETRIC_REQUIREMENT = "strongOrCredential";
|
|
68
|
+
// Must match `META_DATA_KEY` in android/.../auth/BiometricRequirement.kt.
|
|
69
|
+
const ANDROID_BIOMETRIC_META_DATA_NAME = "co.algorand.passkeyautofill.BIOMETRIC_REQUIREMENT";
|
|
70
|
+
// Must match `infoDictionaryKey` in ios/AutofillCredentialProvider/BiometricRequirement.swift.
|
|
71
|
+
const IOS_BIOMETRIC_INFO_PLIST_KEY = "ReactNativePasskeyAutofillBiometricRequirement";
|
|
72
|
+
|
|
73
|
+
// Authenticators a passkey operation will accept. Default is intentionally more permissive than
|
|
74
|
+
// strong-only: it leaves iOS unchanged (passcode still allowed) and lets Android accept the device
|
|
75
|
+
// credential. See docs/superpowers/specs/2026-05-28-configurable-biometric-requirement-design.md.
|
|
76
|
+
const getBiometricRequirement = (props) => {
|
|
77
|
+
if (props.biometricRequirement == null) {
|
|
78
|
+
return DEFAULT_BIOMETRIC_REQUIREMENT;
|
|
79
|
+
}
|
|
80
|
+
const value = String(props.biometricRequirement).trim();
|
|
81
|
+
if (!BIOMETRIC_REQUIREMENT_VALUES.includes(value)) {
|
|
82
|
+
throw new Error(
|
|
83
|
+
`react-native-passkey-autofill: "biometricRequirement" must be one of ${BIOMETRIC_REQUIREMENT_VALUES.join(
|
|
84
|
+
", ",
|
|
85
|
+
)}, received "${props.biometricRequirement}".`,
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
return value;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const normalizeXcodeName = (name) => String(name || "").replace(/^"|"$/g, "");
|
|
92
|
+
|
|
93
|
+
const getExtensionTarget = (project) => {
|
|
94
|
+
const targets = project.pbxNativeTargetSection();
|
|
95
|
+
const targetUuid = Object.keys(targets).find((uuid) => {
|
|
96
|
+
const target = targets[uuid];
|
|
97
|
+
return (
|
|
98
|
+
target?.isa === "PBXNativeTarget" &&
|
|
99
|
+
target.productType === '"com.apple.product-type.app-extension"' &&
|
|
100
|
+
normalizeXcodeName(target.name) === IOS_EXTENSION_NAME
|
|
101
|
+
);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
return targetUuid ? { uuid: targetUuid, pbxNativeTarget: targets[targetUuid] } : null;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const getDevelopmentTeam = (project) => {
|
|
108
|
+
const buildConfigs = project.pbxXCBuildConfigurationSection();
|
|
109
|
+
const config = Object.keys(buildConfigs)
|
|
110
|
+
.filter((key) => !key.endsWith("_comment"))
|
|
111
|
+
.map((key) => buildConfigs[key])
|
|
112
|
+
.find((entry) => {
|
|
113
|
+
const team = entry?.buildSettings?.DEVELOPMENT_TEAM;
|
|
114
|
+
return team && team !== '"$(DEVELOPMENT_TEAM)"' && team !== "$(DEVELOPMENT_TEAM)";
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
return config?.buildSettings?.DEVELOPMENT_TEAM;
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const writePlist = (filePath, body) => {
|
|
121
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
122
|
+
fs.writeFileSync(filePath, body);
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const extensionInfoPlist = ({
|
|
126
|
+
label,
|
|
127
|
+
supportedDomains,
|
|
128
|
+
aaguid,
|
|
129
|
+
biometricRequirement,
|
|
130
|
+
}) => `<?xml version="1.0" encoding="UTF-8"?>
|
|
131
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
132
|
+
<plist version="1.0">
|
|
133
|
+
<dict>
|
|
134
|
+
<key>CFBundleDisplayName</key>
|
|
135
|
+
<string>${label}</string>
|
|
136
|
+
<key>CFBundleExecutable</key>
|
|
137
|
+
<string>$(EXECUTABLE_NAME)</string>
|
|
138
|
+
<key>CFBundleIdentifier</key>
|
|
139
|
+
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
|
140
|
+
<key>CFBundleName</key>
|
|
141
|
+
<string>$(PRODUCT_NAME)</string>
|
|
142
|
+
<key>CFBundlePackageType</key>
|
|
143
|
+
<string>XPC!</string>
|
|
144
|
+
<key>CFBundleShortVersionString</key>
|
|
145
|
+
<string>$(MARKETING_VERSION)</string>
|
|
146
|
+
<key>CFBundleVersion</key>
|
|
147
|
+
<string>$(CURRENT_PROJECT_VERSION)</string>
|
|
148
|
+
<key>ReactNativePasskeyAutofillAppGroup</key>
|
|
149
|
+
<string>$(PASSKEY_AUTOFILL_APP_GROUP)</string>
|
|
150
|
+
<key>AppGroupIdentifier</key>
|
|
151
|
+
<string>$(PASSKEY_AUTOFILL_APP_GROUP)</string>
|
|
152
|
+
${aaguid ? ` <key>ReactNativePasskeyAutofillAAGUID</key>\n <string>${aaguid}</string>\n` : ""} <key>${IOS_BIOMETRIC_INFO_PLIST_KEY}</key>
|
|
153
|
+
<string>${biometricRequirement}</string>
|
|
154
|
+
<key>NSFaceIDUsageDescription</key>
|
|
155
|
+
<string>Rocca uses Face ID to create and use passkeys.</string>
|
|
156
|
+
<key>NSExtension</key>
|
|
157
|
+
<dict>
|
|
158
|
+
<key>ASCredentialProviderExtensionSupportedDomains</key>
|
|
159
|
+
<array>
|
|
160
|
+
${supportedDomains.map((domain) => ` <string>${domain}</string>`).join("\n")}
|
|
161
|
+
</array>
|
|
162
|
+
<key>CFBundleDisplayName</key>
|
|
163
|
+
<string>${label}</string>
|
|
164
|
+
<key>NSExtensionAttributes</key>
|
|
165
|
+
<dict>
|
|
166
|
+
<key>ASCredentialProviderExtensionCapabilities</key>
|
|
167
|
+
<dict>
|
|
168
|
+
<key>ProvidesOneTimeCodes</key>
|
|
169
|
+
<false/>
|
|
170
|
+
<key>ProvidesPasskeys</key>
|
|
171
|
+
<true/>
|
|
172
|
+
<key>ProvidesPasswords</key>
|
|
173
|
+
<false/>
|
|
174
|
+
<key>ProvidesTextToInsert</key>
|
|
175
|
+
<false/>
|
|
176
|
+
<key>ShowsConfigurationUI</key>
|
|
177
|
+
<false/>
|
|
178
|
+
</dict>
|
|
179
|
+
</dict>
|
|
180
|
+
<key>NSExtensionPointIdentifier</key>
|
|
181
|
+
<string>com.apple.authentication-services-credential-provider-ui</string>
|
|
182
|
+
<key>NSExtensionPrincipalClass</key>
|
|
183
|
+
<string>$(PRODUCT_MODULE_NAME).CredentialProviderViewController</string>
|
|
184
|
+
</dict>
|
|
185
|
+
</dict>
|
|
186
|
+
</plist>
|
|
187
|
+
`;
|
|
188
|
+
|
|
189
|
+
const extensionEntitlementsPlist = ({ appGroup }) => `<?xml version="1.0" encoding="UTF-8"?>
|
|
190
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
191
|
+
<plist version="1.0">
|
|
192
|
+
<dict>
|
|
193
|
+
<key>com.apple.developer.authentication-services.autofill-credential-provider</key>
|
|
194
|
+
<true/>
|
|
195
|
+
<key>com.apple.security.application-groups</key>
|
|
196
|
+
<array>
|
|
197
|
+
<string>${appGroup}</string>
|
|
198
|
+
</array>
|
|
199
|
+
</dict>
|
|
200
|
+
</plist>
|
|
201
|
+
`;
|
|
202
|
+
|
|
203
|
+
const withIosPasskeyAutofill = (config, props = {}) => {
|
|
204
|
+
const site = props.site || "https://debug.liquidauth.com";
|
|
205
|
+
const label = props.label || "My Credential Provider";
|
|
206
|
+
const associatedDomain = getAssociatedDomain(site);
|
|
207
|
+
const supportedDomains = props.supportedDomains || [associatedDomain];
|
|
208
|
+
const appGroup = getAppGroup(config, props);
|
|
209
|
+
const aaguid = getAaguid(props);
|
|
210
|
+
const biometricRequirement = getBiometricRequirement(props);
|
|
211
|
+
|
|
212
|
+
config = withInfoPlist(config, (config) => {
|
|
213
|
+
config.modResults.ReactNativePasskeyAutofillAppGroup = appGroup;
|
|
214
|
+
config.modResults.AppGroupIdentifier = appGroup;
|
|
215
|
+
return config;
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
config = withEntitlementsPlist(config, (config) => {
|
|
219
|
+
const domains = new Set(config.modResults["com.apple.developer.associated-domains"] || []);
|
|
220
|
+
domains.add(`webcredentials:${associatedDomain}`);
|
|
221
|
+
config.modResults["com.apple.developer.associated-domains"] = [...domains];
|
|
222
|
+
config.modResults["com.apple.developer.authentication-services.autofill-credential-provider"] =
|
|
223
|
+
true;
|
|
224
|
+
|
|
225
|
+
const appGroups = new Set(config.modResults["com.apple.security.application-groups"] || []);
|
|
226
|
+
appGroups.add(appGroup);
|
|
227
|
+
config.modResults["com.apple.security.application-groups"] = [...appGroups];
|
|
228
|
+
return config;
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
config = withDangerousMod(config, [
|
|
232
|
+
"ios",
|
|
233
|
+
async (config) => {
|
|
234
|
+
const iosRoot = config.modRequest.platformProjectRoot;
|
|
235
|
+
const extensionRoot = path.join(iosRoot, IOS_EXTENSION_NAME);
|
|
236
|
+
const sourceRoot = path.join(__dirname, "ios/AutofillCredentialProvider");
|
|
237
|
+
|
|
238
|
+
fs.mkdirSync(extensionRoot, { recursive: true });
|
|
239
|
+
for (const file of IOS_EXTENSION_FILES) {
|
|
240
|
+
fs.copyFileSync(path.join(sourceRoot, file), path.join(extensionRoot, file));
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
writePlist(
|
|
244
|
+
path.join(extensionRoot, `${IOS_EXTENSION_NAME}-Info.plist`),
|
|
245
|
+
extensionInfoPlist({ label, supportedDomains, aaguid, biometricRequirement }),
|
|
246
|
+
);
|
|
247
|
+
writePlist(
|
|
248
|
+
path.join(extensionRoot, `${IOS_EXTENSION_NAME}.entitlements`),
|
|
249
|
+
extensionEntitlementsPlist({ appGroup }),
|
|
250
|
+
);
|
|
251
|
+
return config;
|
|
252
|
+
},
|
|
253
|
+
]);
|
|
254
|
+
|
|
255
|
+
config = withXcodeProject(config, (config) => {
|
|
256
|
+
const project = config.modResults;
|
|
257
|
+
const bundleIdentifier = `${getIosBundleIdentifier(config)}.PasskeyAutofillCredentialProvider`;
|
|
258
|
+
const buildNumber = config.ios?.buildNumber || config.versionCode || "1";
|
|
259
|
+
const rawDevelopmentTeam =
|
|
260
|
+
props.developmentTeam || props.appleTeamId || getDevelopmentTeam(project);
|
|
261
|
+
const developmentTeam = rawDevelopmentTeam
|
|
262
|
+
? rawDevelopmentTeam.startsWith('"')
|
|
263
|
+
? rawDevelopmentTeam
|
|
264
|
+
: `"${rawDevelopmentTeam}"`
|
|
265
|
+
: '"$(DEVELOPMENT_TEAM)"';
|
|
266
|
+
let target =
|
|
267
|
+
getExtensionTarget(project) ||
|
|
268
|
+
project.addTarget(IOS_EXTENSION_NAME, "app_extension", IOS_EXTENSION_NAME, bundleIdentifier);
|
|
269
|
+
|
|
270
|
+
if (target?.uuid) {
|
|
271
|
+
ensureSourceFilesInTarget(
|
|
272
|
+
project,
|
|
273
|
+
target.uuid,
|
|
274
|
+
IOS_EXTENSION_SOURCE_FILES.map((file) => `${IOS_EXTENSION_NAME}/${file}`),
|
|
275
|
+
);
|
|
276
|
+
if (!hasBuildPhase(project, target.uuid, "PBXFrameworksBuildPhase")) {
|
|
277
|
+
project.addBuildPhase(
|
|
278
|
+
["AuthenticationServices.framework", "CryptoKit.framework"],
|
|
279
|
+
"PBXFrameworksBuildPhase",
|
|
280
|
+
"Frameworks",
|
|
281
|
+
target.uuid,
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
addSwiftPackageProduct(project, {
|
|
285
|
+
targetUuid: target.uuid,
|
|
286
|
+
packageName: "deterministic-P256-swift",
|
|
287
|
+
productName: DETERMINISTIC_P256_PRODUCT_NAME,
|
|
288
|
+
repositoryURL: props.deterministicP256PackageURL || DETERMINISTIC_P256_PACKAGE_URL,
|
|
289
|
+
branch: props.deterministicP256PackageBranch || "main",
|
|
290
|
+
});
|
|
291
|
+
setExtensionBuildSettings(project, target.uuid, {
|
|
292
|
+
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME: '"AccentColor"',
|
|
293
|
+
APPLICATION_EXTENSION_API_ONLY: "YES",
|
|
294
|
+
CODE_SIGN_ENTITLEMENTS: `"${IOS_EXTENSION_NAME}/${IOS_EXTENSION_NAME}.entitlements"`,
|
|
295
|
+
CURRENT_PROJECT_VERSION: `"${buildNumber}"`,
|
|
296
|
+
DEVELOPMENT_TEAM: developmentTeam,
|
|
297
|
+
IPHONEOS_DEPLOYMENT_TARGET: "17.0",
|
|
298
|
+
MARKETING_VERSION: `"${config.version || "1.0.0"}"`,
|
|
299
|
+
PASSKEY_AUTOFILL_APP_GROUP: `"${appGroup}"`,
|
|
300
|
+
PRODUCT_BUNDLE_IDENTIFIER: `"${bundleIdentifier}"`,
|
|
301
|
+
SWIFT_ACTIVE_COMPILATION_CONDITIONS: "PASSKEY_AUTOFILL_EXTENSION",
|
|
302
|
+
HEADER_SEARCH_PATHS: [
|
|
303
|
+
'"$(inherited)"',
|
|
304
|
+
'"$(SRCROOT)/Pods/Headers/Public"',
|
|
305
|
+
'"$(SRCROOT)/Pods/Headers/Public/MMKVCore"',
|
|
306
|
+
],
|
|
307
|
+
LIBRARY_SEARCH_PATHS: [
|
|
308
|
+
'"$(inherited)"',
|
|
309
|
+
'"$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/MMKVCore"',
|
|
310
|
+
],
|
|
311
|
+
OTHER_LDFLAGS: ['"$(inherited)"', '"-lMMKVCore"', '"-lc++"', '"-lz"'],
|
|
312
|
+
SWIFT_OBJC_BRIDGING_HEADER: `"${IOS_EXTENSION_NAME}/PasskeyAutofillCredentialProvider-Bridging-Header.h"`,
|
|
313
|
+
SWIFT_VERSION: "5.9",
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return config;
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
return config;
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
const addSwiftPackageProduct = (
|
|
324
|
+
project,
|
|
325
|
+
{ targetUuid, packageName, productName, repositoryURL, branch },
|
|
326
|
+
) => {
|
|
327
|
+
const objects = project.hash.project.objects;
|
|
328
|
+
objects.XCRemoteSwiftPackageReference = objects.XCRemoteSwiftPackageReference || {};
|
|
329
|
+
objects.XCSwiftPackageProductDependency = objects.XCSwiftPackageProductDependency || {};
|
|
330
|
+
objects.PBXBuildFile = objects.PBXBuildFile || {};
|
|
331
|
+
|
|
332
|
+
const packageComment = `XCRemoteSwiftPackageReference "${packageName}"`;
|
|
333
|
+
let packageUuid = Object.keys(objects.XCRemoteSwiftPackageReference).find(
|
|
334
|
+
(key) =>
|
|
335
|
+
!key.endsWith("_comment") &&
|
|
336
|
+
objects.XCRemoteSwiftPackageReference[key].repositoryURL === `"${repositoryURL}"`,
|
|
337
|
+
);
|
|
338
|
+
if (!packageUuid) {
|
|
339
|
+
packageUuid = project.generateUuid();
|
|
340
|
+
objects.XCRemoteSwiftPackageReference[packageUuid] = {
|
|
341
|
+
isa: "XCRemoteSwiftPackageReference",
|
|
342
|
+
repositoryURL: `"${repositoryURL}"`,
|
|
343
|
+
requirement: {
|
|
344
|
+
branch,
|
|
345
|
+
kind: "branch",
|
|
346
|
+
},
|
|
347
|
+
};
|
|
348
|
+
objects.XCRemoteSwiftPackageReference[`${packageUuid}_comment`] = packageComment;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const firstProject = project.getFirstProject().firstProject;
|
|
352
|
+
firstProject.packageReferences = firstProject.packageReferences || [];
|
|
353
|
+
if (!firstProject.packageReferences.some((reference) => reference.value === packageUuid)) {
|
|
354
|
+
firstProject.packageReferences.push({ value: packageUuid, comment: packageComment });
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
let productUuid = Object.keys(objects.XCSwiftPackageProductDependency).find(
|
|
358
|
+
(key) =>
|
|
359
|
+
!key.endsWith("_comment") &&
|
|
360
|
+
objects.XCSwiftPackageProductDependency[key].package === packageUuid &&
|
|
361
|
+
objects.XCSwiftPackageProductDependency[key].productName === `"${productName}"`,
|
|
362
|
+
);
|
|
363
|
+
if (!productUuid) {
|
|
364
|
+
productUuid = project.generateUuid();
|
|
365
|
+
objects.XCSwiftPackageProductDependency[productUuid] = {
|
|
366
|
+
isa: "XCSwiftPackageProductDependency",
|
|
367
|
+
package: packageUuid,
|
|
368
|
+
productName: `"${productName}"`,
|
|
369
|
+
};
|
|
370
|
+
objects.XCSwiftPackageProductDependency[`${productUuid}_comment`] = productName;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const target = project.pbxNativeTargetSection()[targetUuid];
|
|
374
|
+
target.packageProductDependencies = target.packageProductDependencies || [];
|
|
375
|
+
if (!target.packageProductDependencies.some((dependency) => dependency.value === productUuid)) {
|
|
376
|
+
target.packageProductDependencies.push({ value: productUuid, comment: productName });
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const frameworksPhase = target.buildPhases
|
|
380
|
+
.map((phase) => ({ phase, value: objects.PBXFrameworksBuildPhase?.[phase.value] }))
|
|
381
|
+
.find(({ value }) => value)?.value;
|
|
382
|
+
if (!frameworksPhase) {
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
const alreadyLinked = frameworksPhase.files.some(
|
|
386
|
+
(file) => file.comment === `${productName} in Frameworks`,
|
|
387
|
+
);
|
|
388
|
+
if (!alreadyLinked) {
|
|
389
|
+
const buildFileUuid = project.generateUuid();
|
|
390
|
+
objects.PBXBuildFile[buildFileUuid] = {
|
|
391
|
+
isa: "PBXBuildFile",
|
|
392
|
+
productRef: productUuid,
|
|
393
|
+
productRef_comment: productName,
|
|
394
|
+
};
|
|
395
|
+
objects.PBXBuildFile[`${buildFileUuid}_comment`] = `${productName} in Frameworks`;
|
|
396
|
+
frameworksPhase.files.push({
|
|
397
|
+
value: buildFileUuid,
|
|
398
|
+
comment: `${productName} in Frameworks`,
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
const hasBuildPhase = (project, targetUuid, phaseType) => {
|
|
404
|
+
const target = project.pbxNativeTargetSection()[targetUuid];
|
|
405
|
+
return target.buildPhases.some((phase) => project.hash.project.objects[phaseType]?.[phase.value]);
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
const getBuildPhase = (project, targetUuid, phaseType) => {
|
|
409
|
+
const target = project.pbxNativeTargetSection()[targetUuid];
|
|
410
|
+
const phases = project.hash.project.objects[phaseType] || {};
|
|
411
|
+
const phaseRef = target.buildPhases.find((phase) => phases[phase.value]);
|
|
412
|
+
return phaseRef ? phases[phaseRef.value] : null;
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
const getFileType = (filePath) => {
|
|
416
|
+
if (filePath.endsWith(".swift")) return "sourcecode.swift";
|
|
417
|
+
if (filePath.endsWith(".mm")) return "sourcecode.cpp.objcpp";
|
|
418
|
+
if (filePath.endsWith(".m")) return "sourcecode.c.objc";
|
|
419
|
+
if (filePath.endsWith(".h")) return "sourcecode.c.h";
|
|
420
|
+
return "text";
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
const getOrCreateFileReference = (project, filePath) => {
|
|
424
|
+
const fileReferences = project.pbxFileReferenceSection();
|
|
425
|
+
const existingUuid = Object.keys(fileReferences).find((uuid) => {
|
|
426
|
+
if (uuid.endsWith("_comment")) return false;
|
|
427
|
+
const refPath = fileReferences[uuid]?.path;
|
|
428
|
+
return refPath === filePath || refPath === `"${filePath}"`;
|
|
429
|
+
});
|
|
430
|
+
if (existingUuid) {
|
|
431
|
+
return existingUuid;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
const uuid = project.generateUuid();
|
|
435
|
+
const basename = path.basename(filePath);
|
|
436
|
+
fileReferences[uuid] = {
|
|
437
|
+
isa: "PBXFileReference",
|
|
438
|
+
fileEncoding: 4,
|
|
439
|
+
lastKnownFileType: getFileType(filePath),
|
|
440
|
+
name: basename,
|
|
441
|
+
path: filePath,
|
|
442
|
+
sourceTree: '"<group>"',
|
|
443
|
+
};
|
|
444
|
+
fileReferences[`${uuid}_comment`] = basename;
|
|
445
|
+
return uuid;
|
|
446
|
+
};
|
|
447
|
+
|
|
448
|
+
const getOrCreateBuildFile = (project, fileRefUuid, basename) => {
|
|
449
|
+
const buildFiles = project.pbxBuildFileSection();
|
|
450
|
+
const existingUuid = Object.keys(buildFiles).find((uuid) => {
|
|
451
|
+
if (uuid.endsWith("_comment")) return false;
|
|
452
|
+
return buildFiles[uuid]?.fileRef === fileRefUuid;
|
|
453
|
+
});
|
|
454
|
+
if (existingUuid) {
|
|
455
|
+
return existingUuid;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
const uuid = project.generateUuid();
|
|
459
|
+
buildFiles[uuid] = {
|
|
460
|
+
isa: "PBXBuildFile",
|
|
461
|
+
fileRef: fileRefUuid,
|
|
462
|
+
fileRef_comment: basename,
|
|
463
|
+
};
|
|
464
|
+
buildFiles[`${uuid}_comment`] = `${basename} in Sources`;
|
|
465
|
+
return uuid;
|
|
466
|
+
};
|
|
467
|
+
|
|
468
|
+
const ensureSourceFilesInTarget = (project, targetUuid, files) => {
|
|
469
|
+
const sourcesPhase = getBuildPhase(project, targetUuid, "PBXSourcesBuildPhase");
|
|
470
|
+
if (!sourcesPhase) {
|
|
471
|
+
project.addBuildPhase(files, "PBXSourcesBuildPhase", "Sources", targetUuid);
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
for (const filePath of files) {
|
|
476
|
+
const basename = path.basename(filePath);
|
|
477
|
+
const alreadyInSources = sourcesPhase.files.some(
|
|
478
|
+
(file) => file.comment === `${basename} in Sources`,
|
|
479
|
+
);
|
|
480
|
+
if (alreadyInSources) {
|
|
481
|
+
continue;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
const fileRefUuid = getOrCreateFileReference(project, filePath);
|
|
485
|
+
const buildFileUuid = getOrCreateBuildFile(project, fileRefUuid, basename);
|
|
486
|
+
sourcesPhase.files.push({
|
|
487
|
+
value: buildFileUuid,
|
|
488
|
+
comment: `${basename} in Sources`,
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
};
|
|
492
|
+
|
|
493
|
+
const setExtensionBuildSettings = (project, targetUuid, settings) => {
|
|
494
|
+
const target = project.pbxNativeTargetSection()[targetUuid];
|
|
495
|
+
const configurationList = project.pbxXCConfigurationList()[target.buildConfigurationList];
|
|
496
|
+
for (const config of configurationList.buildConfigurations) {
|
|
497
|
+
const buildConfig = project.pbxXCBuildConfigurationSection()[config.value];
|
|
498
|
+
buildConfig.buildSettings = {
|
|
499
|
+
...buildConfig.buildSettings,
|
|
500
|
+
...settings,
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
};
|
|
504
|
+
|
|
12
505
|
const withAndroidCookieModule = (config) => {
|
|
13
506
|
return withDangerousMod(config, [
|
|
14
507
|
"android",
|
|
@@ -16,11 +509,7 @@ const withAndroidCookieModule = (config) => {
|
|
|
16
509
|
const projectRoot = config.modRequest.projectRoot;
|
|
17
510
|
const packageId = config.android?.package || "co.algorand.auth.example";
|
|
18
511
|
const packagePath = packageId.replace(/\./g, "/");
|
|
19
|
-
const targetDir = path.join(
|
|
20
|
-
projectRoot,
|
|
21
|
-
"android/app/src/main/java",
|
|
22
|
-
packagePath
|
|
23
|
-
);
|
|
512
|
+
const targetDir = path.join(projectRoot, "android/app/src/main/java", packagePath);
|
|
24
513
|
|
|
25
514
|
// Ensure the directory exists
|
|
26
515
|
fs.mkdirSync(targetDir, { recursive: true });
|
|
@@ -80,14 +569,8 @@ class CookiePackage : ReactPackage {
|
|
|
80
569
|
}
|
|
81
570
|
`;
|
|
82
571
|
|
|
83
|
-
fs.writeFileSync(
|
|
84
|
-
|
|
85
|
-
cookieModuleContent
|
|
86
|
-
);
|
|
87
|
-
fs.writeFileSync(
|
|
88
|
-
path.join(targetDir, "CookiePackage.kt"),
|
|
89
|
-
cookiePackageContent
|
|
90
|
-
);
|
|
572
|
+
fs.writeFileSync(path.join(targetDir, "CookieModule.kt"), cookieModuleContent);
|
|
573
|
+
fs.writeFileSync(path.join(targetDir, "CookiePackage.kt"), cookiePackageContent);
|
|
91
574
|
|
|
92
575
|
return config;
|
|
93
576
|
},
|
|
@@ -112,10 +595,7 @@ const withUserAgent = (config) => {
|
|
|
112
595
|
|
|
113
596
|
imports.forEach((imp) => {
|
|
114
597
|
if (!content.includes(imp)) {
|
|
115
|
-
content = content.replace(
|
|
116
|
-
/package .*\n/,
|
|
117
|
-
(match) => `${match}${imp}\n`
|
|
118
|
-
);
|
|
598
|
+
content = content.replace(/package .*\n/, (match) => `${match}${imp}\n`);
|
|
119
599
|
}
|
|
120
600
|
});
|
|
121
601
|
|
|
@@ -145,17 +625,14 @@ const withUserAgent = (config) => {
|
|
|
145
625
|
`;
|
|
146
626
|
|
|
147
627
|
if (!content.includes("OkHttpClientProvider.setOkHttpClientFactory")) {
|
|
148
|
-
content = content.replace(
|
|
149
|
-
/super\.onCreate\(\)/,
|
|
150
|
-
`super.onCreate()${okHttpClientCode}`
|
|
151
|
-
);
|
|
628
|
+
content = content.replace(/super\.onCreate\(\)/, `super.onCreate()${okHttpClientCode}`);
|
|
152
629
|
}
|
|
153
630
|
|
|
154
631
|
// Register CookiePackage if it exists
|
|
155
632
|
if (!content.includes("add(CookiePackage())")) {
|
|
156
633
|
content = content.replace(
|
|
157
634
|
/PackageList\(this\)\.packages\.apply \{/,
|
|
158
|
-
`PackageList(this).packages.apply {\n add(CookiePackage())
|
|
635
|
+
`PackageList(this).packages.apply {\n add(CookiePackage())`,
|
|
159
636
|
);
|
|
160
637
|
}
|
|
161
638
|
|
|
@@ -165,9 +642,11 @@ const withUserAgent = (config) => {
|
|
|
165
642
|
};
|
|
166
643
|
|
|
167
644
|
const withPasskeyAutofill = (config, props = {}) => {
|
|
168
|
-
const site = props.site || "https://
|
|
645
|
+
const site = props.site || "https://debug.liquidauth.com";
|
|
169
646
|
const label = props.label || "My Credential Provider";
|
|
647
|
+
const aaguid = getAaguid(props);
|
|
170
648
|
|
|
649
|
+
config = withIosPasskeyAutofill(config, props);
|
|
171
650
|
config = withAndroidCookieModule(config);
|
|
172
651
|
config = withUserAgent(config);
|
|
173
652
|
|
|
@@ -175,17 +654,13 @@ const withPasskeyAutofill = (config, props = {}) => {
|
|
|
175
654
|
// 1. Add asset_statements meta-data to MainActivity
|
|
176
655
|
config = withAndroidManifest(config, async (config) => {
|
|
177
656
|
const mainActivity = config.modResults.manifest.application[0].activity.find(
|
|
178
|
-
(a) => a["$"]["android:name"] === ".MainActivity"
|
|
657
|
+
(a) => a["$"]["android:name"] === ".MainActivity",
|
|
179
658
|
);
|
|
180
659
|
if (mainActivity) {
|
|
181
660
|
if (!mainActivity["meta-data"]) {
|
|
182
661
|
mainActivity["meta-data"] = [];
|
|
183
662
|
}
|
|
184
|
-
if (
|
|
185
|
-
!mainActivity["meta-data"].find(
|
|
186
|
-
(m) => m["$"]["android:name"] === "asset_statements"
|
|
187
|
-
)
|
|
188
|
-
) {
|
|
663
|
+
if (!mainActivity["meta-data"].find((m) => m["$"]["android:name"] === "asset_statements")) {
|
|
189
664
|
mainActivity["meta-data"].push({
|
|
190
665
|
$: {
|
|
191
666
|
"android:name": "asset_statements",
|
|
@@ -197,48 +672,104 @@ const withPasskeyAutofill = (config, props = {}) => {
|
|
|
197
672
|
return config;
|
|
198
673
|
});
|
|
199
674
|
|
|
675
|
+
// Expose the configured biometric requirement to the native provider/activities.
|
|
676
|
+
config = withAndroidManifest(config, (config) => {
|
|
677
|
+
const application = config.modResults.manifest.application[0];
|
|
678
|
+
if (!application["meta-data"]) {
|
|
679
|
+
application["meta-data"] = [];
|
|
680
|
+
}
|
|
681
|
+
const name = ANDROID_BIOMETRIC_META_DATA_NAME;
|
|
682
|
+
const existing = application["meta-data"].find((m) => m["$"]["android:name"] === name);
|
|
683
|
+
const value = getBiometricRequirement(props);
|
|
684
|
+
if (existing) {
|
|
685
|
+
existing["$"]["android:value"] = value;
|
|
686
|
+
} else {
|
|
687
|
+
application["meta-data"].push({ $: { "android:name": name, "android:value": value } });
|
|
688
|
+
}
|
|
689
|
+
return config;
|
|
690
|
+
});
|
|
691
|
+
|
|
200
692
|
// 2. Add asset_statements string to strings.xml
|
|
201
693
|
config = withStringsXml(config, (config) => {
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
{
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
site: site,
|
|
215
|
-
},
|
|
694
|
+
const stringItems = [
|
|
695
|
+
{
|
|
696
|
+
$: { name: "asset_statements", translatable: "false" },
|
|
697
|
+
_: JSON.stringify([
|
|
698
|
+
{
|
|
699
|
+
relation: [
|
|
700
|
+
"delegate_permission/common.handle_all_urls",
|
|
701
|
+
"delegate_permission/common.get_login_creds",
|
|
702
|
+
],
|
|
703
|
+
target: {
|
|
704
|
+
namespace: "web",
|
|
705
|
+
site: site,
|
|
216
706
|
},
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
)
|
|
707
|
+
},
|
|
708
|
+
]),
|
|
709
|
+
},
|
|
710
|
+
{
|
|
711
|
+
$: { name: "passkey_autofill_label", translatable: "true" },
|
|
712
|
+
_: label,
|
|
713
|
+
},
|
|
714
|
+
];
|
|
715
|
+
if (aaguid) {
|
|
716
|
+
stringItems.push({
|
|
717
|
+
$: { name: "passkey_autofill_aaguid", translatable: "false" },
|
|
718
|
+
_: aaguid,
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
config.modResults = AndroidConfig.Strings.setStringItem(stringItems, config.modResults);
|
|
722
|
+
return config;
|
|
723
|
+
});
|
|
724
|
+
|
|
725
|
+
// 3. Inject `pickFirst '**/libmmkv.so'` into the consumer app's
|
|
726
|
+
// `android/app/build.gradle`. The native `libmmkv.so` is bundled by both
|
|
727
|
+
// this module (via `io.github.zhongwuzw:mmkv`) and `react-native-mmkv`
|
|
728
|
+
// (which also depends on the same artifact, sometimes pinned to a different
|
|
729
|
+
// version). When two copies end up on the merged-libs path, AGP fails
|
|
730
|
+
// `mergeDebugNativeLibs` with: "2 files found with path
|
|
731
|
+
// 'lib/<abi>/libmmkv.so'". Picking the first occurrence is safe because
|
|
732
|
+
// the `com.tencent.mmkv.MMKV` Java surface we touch is ABI-stable across
|
|
733
|
+
// the 2.x range.
|
|
734
|
+
config = withAppBuildGradle(config, (config) => {
|
|
735
|
+
const marker = "pickFirst '**/libmmkv.so'";
|
|
736
|
+
if (config.modResults.contents.includes(marker)) {
|
|
737
|
+
return config;
|
|
738
|
+
}
|
|
739
|
+
let contents = config.modResults.contents;
|
|
740
|
+
if (/packagingOptions\s*\{/.test(contents)) {
|
|
741
|
+
contents = contents.replace(/packagingOptions\s*\{/, `packagingOptions {\n ${marker}`);
|
|
742
|
+
} else if (/android\s*\{/.test(contents)) {
|
|
743
|
+
contents = contents.replace(
|
|
744
|
+
/android\s*\{/,
|
|
745
|
+
`android {\n packagingOptions {\n ${marker}\n }\n`,
|
|
746
|
+
);
|
|
747
|
+
}
|
|
748
|
+
config.modResults.contents = contents;
|
|
226
749
|
return config;
|
|
227
750
|
});
|
|
228
751
|
|
|
229
|
-
//
|
|
752
|
+
// 4. Add local Maven repository for local AAR
|
|
230
753
|
config = withProjectBuildGradle(config, (config) => {
|
|
231
|
-
if (config.modResults.contents.includes("
|
|
754
|
+
if (config.modResults.contents.includes("android/libs/repo")) {
|
|
232
755
|
return config;
|
|
233
756
|
}
|
|
234
|
-
|
|
757
|
+
// Dynamically find the path to the library's android/libs/repo directory
|
|
758
|
+
const projectRoot = config.modRequest.projectRoot;
|
|
759
|
+
const libraryRepoPath = path.join(__dirname, "android/libs/repo");
|
|
760
|
+
const relativeRepoPath = path.relative(projectRoot, libraryRepoPath).replace(/\\/g, "/");
|
|
761
|
+
|
|
762
|
+
// In Gradle, rootDir is the android directory of the app.
|
|
763
|
+
// So the path to the repo relative to rootDir is ../<relativeRepoPath>
|
|
764
|
+
const repoInjectedCode = `allprojects {
|
|
765
|
+
repositories {
|
|
766
|
+
maven {
|
|
767
|
+
url = uri("\${rootDir}/../${relativeRepoPath}")
|
|
768
|
+
}`;
|
|
769
|
+
|
|
235
770
|
config.modResults.contents = config.modResults.contents.replace(
|
|
236
771
|
/allprojects\s*{[\s\n]*repositories\s*{/,
|
|
237
|
-
|
|
238
|
-
repositories {
|
|
239
|
-
flatDir {
|
|
240
|
-
dirs "${libsDir}"
|
|
241
|
-
}`
|
|
772
|
+
repoInjectedCode,
|
|
242
773
|
);
|
|
243
774
|
return config;
|
|
244
775
|
});
|
|
@@ -247,3 +778,5 @@ const withPasskeyAutofill = (config, props = {}) => {
|
|
|
247
778
|
};
|
|
248
779
|
|
|
249
780
|
module.exports = withPasskeyAutofill;
|
|
781
|
+
module.exports.getBiometricRequirement = getBiometricRequirement;
|
|
782
|
+
module.exports.getAaguid = getAaguid;
|