@capgo/capacitor-passkey 8.2.0

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.
Files changed (37) hide show
  1. package/CapgoCapacitorPasskey.podspec +17 -0
  2. package/LICENSE +373 -0
  3. package/Package.swift +28 -0
  4. package/README.md +692 -0
  5. package/android/build.gradle +61 -0
  6. package/android/src/main/AndroidManifest.xml +2 -0
  7. package/android/src/main/java/app/capgo/passkey/CapacitorPasskey.java +21 -0
  8. package/android/src/main/java/app/capgo/passkey/CapacitorPasskeyPlugin.java +299 -0
  9. package/android/src/main/res/.gitkeep +0 -0
  10. package/dist/docs.json +1034 -0
  11. package/dist/esm/definitions.d.ts +531 -0
  12. package/dist/esm/definitions.js +2 -0
  13. package/dist/esm/definitions.js.map +1 -0
  14. package/dist/esm/index.d.ts +4 -0
  15. package/dist/esm/index.js +87 -0
  16. package/dist/esm/index.js.map +1 -0
  17. package/dist/esm/internal.d.ts +14 -0
  18. package/dist/esm/internal.js +2 -0
  19. package/dist/esm/internal.js.map +1 -0
  20. package/dist/esm/shim.d.ts +8 -0
  21. package/dist/esm/shim.js +108 -0
  22. package/dist/esm/shim.js.map +1 -0
  23. package/dist/esm/web.d.ts +12 -0
  24. package/dist/esm/web.js +69 -0
  25. package/dist/esm/web.js.map +1 -0
  26. package/dist/esm/webauthn.d.ts +23 -0
  27. package/dist/esm/webauthn.js +440 -0
  28. package/dist/esm/webauthn.js.map +1 -0
  29. package/dist/plugin.cjs.js +707 -0
  30. package/dist/plugin.cjs.js.map +1 -0
  31. package/dist/plugin.js +710 -0
  32. package/dist/plugin.js.map +1 -0
  33. package/ios/Sources/CapacitorPasskeyPlugin/CapacitorPasskey.swift +39 -0
  34. package/ios/Sources/CapacitorPasskeyPlugin/CapacitorPasskeyPlugin.swift +388 -0
  35. package/ios/Tests/CapacitorPasskeyPluginTests/CapacitorPasskeyPluginTests.swift +17 -0
  36. package/package.json +105 -0
  37. package/scripts/configure-native.js +263 -0
package/package.json ADDED
@@ -0,0 +1,105 @@
1
+ {
2
+ "name": "@capgo/capacitor-passkey",
3
+ "version": "8.2.0",
4
+ "description": "Capacitor passkey plugin with a WebAuthn-style shim for Capacitor apps.",
5
+ "main": "dist/plugin.cjs.js",
6
+ "module": "dist/esm/index.js",
7
+ "types": "dist/esm/index.d.ts",
8
+ "unpkg": "dist/plugin.js",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/esm/index.d.ts",
12
+ "import": "./dist/esm/index.js",
13
+ "require": "./dist/plugin.cjs.js",
14
+ "default": "./dist/esm/index.js"
15
+ },
16
+ "./auto": {
17
+ "types": "./dist/esm/auto.d.ts",
18
+ "import": "./dist/esm/auto.js",
19
+ "default": "./dist/esm/auto.js"
20
+ }
21
+ },
22
+ "files": [
23
+ "android/src/main/",
24
+ "android/build.gradle",
25
+ "dist/",
26
+ "ios/Sources",
27
+ "ios/Tests",
28
+ "Package.swift",
29
+ "CapgoCapacitorPasskey.podspec",
30
+ "scripts/configure-native.js"
31
+ ],
32
+ "author": "Martin Donadieu <martin@capgo.app>",
33
+ "license": "MPL-2.0",
34
+ "repository": {
35
+ "type": "git",
36
+ "url": "git+https://github.com/Cap-go/capacitor-passkey.git"
37
+ },
38
+ "bugs": {
39
+ "url": "https://github.com/Cap-go/capacitor-passkey/issues"
40
+ },
41
+ "homepage": "https://capgo.app/docs/plugins/passkey/",
42
+ "keywords": [
43
+ "capacitor",
44
+ "passkey",
45
+ "webauthn",
46
+ "credential-manager",
47
+ "authentication"
48
+ ],
49
+ "scripts": {
50
+ "capacitor:sync:after": "node scripts/configure-native.js",
51
+ "capacitor:update:after": "node scripts/configure-native.js",
52
+ "verify": "bun run verify:ios && bun run verify:android && bun run verify:web",
53
+ "verify:ios": "xcodebuild -scheme CapgoCapacitorPasskey -destination generic/platform=iOS",
54
+ "verify:android": "cd android && ./gradlew clean build test && cd ..",
55
+ "verify:web": "bun run build",
56
+ "lint": "bun run eslint && bun run prettier -- --check && bun run swiftlint -- lint",
57
+ "fmt": "bun run eslint -- --fix && bun run prettier -- --write && bun run swiftlint -- --fix --format",
58
+ "eslint": "eslint . --ext .ts",
59
+ "prettier": "prettier-pretty-check \"**/*.{css,html,ts,js,java}\" --plugin=prettier-plugin-java",
60
+ "swiftlint": "node-swiftlint",
61
+ "docgen": "docgen --api CapacitorPasskeyPlugin --output-readme README.md --output-json dist/docs.json",
62
+ "build": "bun run clean && bun run docgen && tsc && rollup -c rollup.config.mjs",
63
+ "clean": "rimraf ./dist",
64
+ "watch": "tsc --watch",
65
+ "prepublishOnly": "bun run build",
66
+ "check:wiring": "node scripts/check-capacitor-plugin-wiring.mjs"
67
+ },
68
+ "devDependencies": {
69
+ "@capacitor/android": "^8.0.0",
70
+ "@capacitor/cli": "^8.0.0",
71
+ "@capacitor/core": "^8.0.0",
72
+ "@capacitor/docgen": "^0.3.1",
73
+ "@capacitor/ios": "^8.0.0",
74
+ "@ionic/eslint-config": "^0.4.0",
75
+ "@ionic/prettier-config": "^4.0.0",
76
+ "@ionic/swiftlint-config": "^2.0.0",
77
+ "@types/node": "^24.10.1",
78
+ "eslint": "^8.57.1",
79
+ "eslint-plugin-import": "^2.31.0",
80
+ "husky": "^9.1.7",
81
+ "prettier": "^3.6.2",
82
+ "prettier-pretty-check": "^0.2.0",
83
+ "prettier-plugin-java": "^2.7.7",
84
+ "rimraf": "^6.1.0",
85
+ "rollup": "^4.53.2",
86
+ "swiftlint": "^2.0.0",
87
+ "typescript": "^5.9.3"
88
+ },
89
+ "peerDependencies": {
90
+ "@capacitor/core": ">=8.0.0"
91
+ },
92
+ "prettier": "@ionic/prettier-config",
93
+ "swiftlint": "@ionic/swiftlint-config",
94
+ "eslintConfig": {
95
+ "extends": "@ionic/eslint-config/recommended"
96
+ },
97
+ "capacitor": {
98
+ "ios": {
99
+ "src": "ios"
100
+ },
101
+ "android": {
102
+ "src": "android"
103
+ }
104
+ }
105
+ }
@@ -0,0 +1,263 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ const ROOT_DIR = process.env.CAPACITOR_ROOT_DIR;
7
+ const PLATFORM = process.env.CAPACITOR_PLATFORM_NAME;
8
+ const CONFIG_JSON = process.env.CAPACITOR_CONFIG;
9
+ const ANDROID_META_BLOCK = ` <!-- CapacitorPasskey auto-configuration -->\n <meta-data\n android:name="asset_statements"\n android:resource="@string/capacitor_passkey_asset_statements" />`;
10
+
11
+ function log(message) {
12
+ console.log(`[CapacitorPasskey] ${message}`);
13
+ }
14
+
15
+ function warn(message) {
16
+ console.warn(`[CapacitorPasskey] ${message}`);
17
+ }
18
+
19
+ function parseConfig() {
20
+ if (!CONFIG_JSON) {
21
+ return {
22
+ autoShim: true,
23
+ domains: [],
24
+ origin: undefined,
25
+ };
26
+ }
27
+
28
+ try {
29
+ const config = JSON.parse(CONFIG_JSON);
30
+ const pluginConfig = config?.plugins?.CapacitorPasskey ?? {};
31
+ const origin = normalizeOrigin(pluginConfig.origin);
32
+ return {
33
+ autoShim: pluginConfig.autoShim !== false,
34
+ domains: collectDomains(pluginConfig.domains, origin),
35
+ origin,
36
+ };
37
+ } catch (error) {
38
+ warn(`Could not parse CAPACITOR_CONFIG: ${error.message}`);
39
+ return {
40
+ autoShim: true,
41
+ domains: [],
42
+ origin: undefined,
43
+ };
44
+ }
45
+ }
46
+
47
+ function normalizeOrigin(value) {
48
+ if (typeof value !== 'string' || value.trim() === '') {
49
+ return undefined;
50
+ }
51
+
52
+ try {
53
+ const parsed = new URL(value);
54
+ if (parsed.protocol !== 'https:') {
55
+ warn(`Ignoring non-HTTPS origin "${value}". Passkey origin should be HTTPS.`);
56
+ return undefined;
57
+ }
58
+
59
+ return parsed.origin;
60
+ } catch {
61
+ warn(`Ignoring invalid origin "${value}".`);
62
+ return undefined;
63
+ }
64
+ }
65
+
66
+ function normalizeDomain(value) {
67
+ if (typeof value !== 'string' || value.trim() === '') {
68
+ return undefined;
69
+ }
70
+
71
+ try {
72
+ return new URL(value).hostname || undefined;
73
+ } catch {
74
+ return value.trim();
75
+ }
76
+ }
77
+
78
+ function collectDomains(configuredDomains, origin) {
79
+ const domains = new Set();
80
+
81
+ if (Array.isArray(configuredDomains)) {
82
+ for (const domain of configuredDomains) {
83
+ const normalized = normalizeDomain(domain);
84
+ if (normalized) {
85
+ domains.add(normalized);
86
+ }
87
+ }
88
+ }
89
+
90
+ if (origin) {
91
+ domains.add(new URL(origin).hostname);
92
+ }
93
+
94
+ return [...domains];
95
+ }
96
+
97
+ function ensureParentDirectory(filePath) {
98
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
99
+ }
100
+
101
+ function escapeXml(value) {
102
+ return value.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
103
+ }
104
+
105
+ function writeFileIfChanged(filePath, nextContent) {
106
+ const previousContent = fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf8') : undefined;
107
+ if (previousContent === nextContent) {
108
+ return false;
109
+ }
110
+
111
+ ensureParentDirectory(filePath);
112
+ fs.writeFileSync(filePath, nextContent, 'utf8');
113
+ return true;
114
+ }
115
+
116
+ function removeManagedAndroidMetadata(content) {
117
+ return content.replace(
118
+ /\n?\s*<!-- CapacitorPasskey auto-configuration -->\n\s*<meta-data\s+android:name="asset_statements"\s+android:resource="@string\/capacitor_passkey_asset_statements"\s*\/>\n?/m,
119
+ '\n',
120
+ );
121
+ }
122
+
123
+ function configureAndroid(rootDir, domains) {
124
+ const manifestPath = path.join(rootDir, 'android', 'app', 'src', 'main', 'AndroidManifest.xml');
125
+ const valuesPath = path.join(rootDir, 'android', 'app', 'src', 'main', 'res', 'values', 'capacitor-passkey.xml');
126
+
127
+ if (!fs.existsSync(manifestPath)) {
128
+ log('Skipping Android auto-configuration because AndroidManifest.xml was not found.');
129
+ return;
130
+ }
131
+
132
+ let manifest = fs.readFileSync(manifestPath, 'utf8');
133
+ manifest = removeManagedAndroidMetadata(manifest);
134
+
135
+ if (domains.length > 0) {
136
+ if (!manifest.includes('@string/capacitor_passkey_asset_statements')) {
137
+ manifest = manifest.replace('</application>', `${ANDROID_META_BLOCK}\n </application>`);
138
+ }
139
+
140
+ const includes = domains.map((domain) => ({
141
+ include: `https://${domain}/.well-known/assetlinks.json`,
142
+ }));
143
+ const xml = `<?xml version="1.0" encoding="utf-8"?>\n<resources>\n <string name="capacitor_passkey_asset_statements" translatable="false">${escapeXml(JSON.stringify(includes))}</string>\n</resources>\n`;
144
+
145
+ const valuesChanged = writeFileIfChanged(valuesPath, xml);
146
+ const manifestChanged = writeFileIfChanged(manifestPath, manifest);
147
+
148
+ if (manifestChanged || valuesChanged) {
149
+ log(`Updated Android passkey wiring for ${domains.join(', ')}.`);
150
+ }
151
+ return;
152
+ }
153
+
154
+ const manifestChanged = writeFileIfChanged(manifestPath, manifest);
155
+ if (fs.existsSync(valuesPath)) {
156
+ fs.unlinkSync(valuesPath);
157
+ }
158
+
159
+ if (manifestChanged) {
160
+ log('Removed Android passkey auto-configuration because no domains are configured.');
161
+ }
162
+ }
163
+
164
+ function createEntitlementsPlist() {
165
+ return `<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n<plist version="1.0">\n<dict>\n</dict>\n</plist>\n`;
166
+ }
167
+
168
+ function replaceEntitlementArray(content, key, values) {
169
+ const keyPattern = new RegExp(`\\s*<key>${key}</key>\\s*<array>[\\s\\S]*?<\\/array>\\s*`, 'm');
170
+ const replacement =
171
+ values.length > 0
172
+ ? `\n\t<key>${key}</key>\n\t<array>\n${values.map((value) => `\t\t<string>${value}</string>`).join('\n')}\n\t</array>\n`
173
+ : '\n';
174
+
175
+ if (keyPattern.test(content)) {
176
+ return content.replace(keyPattern, replacement);
177
+ }
178
+
179
+ if (values.length === 0) {
180
+ return content;
181
+ }
182
+
183
+ return content.replace('</dict>', `${replacement}</dict>`);
184
+ }
185
+
186
+ function ensureIosEntitlementsReference(projectContent, entitlementsRelativePath) {
187
+ if (projectContent.includes('CODE_SIGN_ENTITLEMENTS =')) {
188
+ return projectContent;
189
+ }
190
+
191
+ return projectContent.replace(
192
+ /(\s*INFOPLIST_FILE = App\/Info\.plist;\n)/g,
193
+ `\t\t\t\tCODE_SIGN_ENTITLEMENTS = ${entitlementsRelativePath};\n$1`,
194
+ );
195
+ }
196
+
197
+ function findExistingEntitlementsPath(projectContent) {
198
+ const match = projectContent.match(/CODE_SIGN_ENTITLEMENTS = ([^;]+);/);
199
+ return match?.[1]?.trim();
200
+ }
201
+
202
+ function configureIOS(rootDir, domains) {
203
+ const iosProjectRoot = path.join(rootDir, 'ios', 'App');
204
+ const pbxprojPath = path.join(iosProjectRoot, 'App.xcodeproj', 'project.pbxproj');
205
+
206
+ if (!fs.existsSync(pbxprojPath)) {
207
+ log('Skipping iOS auto-configuration because App.xcodeproj was not found.');
208
+ return;
209
+ }
210
+
211
+ let projectContent = fs.readFileSync(pbxprojPath, 'utf8');
212
+ const entitlementsRelativePath = findExistingEntitlementsPath(projectContent) ?? 'App/App.entitlements';
213
+ const entitlementsPath = path.join(iosProjectRoot, entitlementsRelativePath);
214
+ const associatedDomains = domains.map((domain) => `webcredentials:${domain}`);
215
+
216
+ const originalEntitlements = fs.existsSync(entitlementsPath)
217
+ ? fs.readFileSync(entitlementsPath, 'utf8')
218
+ : createEntitlementsPlist();
219
+ const nextEntitlements = replaceEntitlementArray(
220
+ originalEntitlements.includes('<dict>') ? originalEntitlements : createEntitlementsPlist(),
221
+ 'com.apple.developer.associated-domains',
222
+ associatedDomains,
223
+ );
224
+ const entitlementsChanged = writeFileIfChanged(entitlementsPath, nextEntitlements);
225
+
226
+ projectContent = ensureIosEntitlementsReference(projectContent, entitlementsRelativePath);
227
+ const projectChanged = writeFileIfChanged(pbxprojPath, projectContent);
228
+
229
+ if (entitlementsChanged || projectChanged) {
230
+ if (domains.length > 0) {
231
+ log(`Updated iOS associated domains for ${domains.join(', ')}.`);
232
+ } else {
233
+ log('Removed iOS associated domains because no passkey domains are configured.');
234
+ }
235
+ }
236
+ }
237
+
238
+ function run() {
239
+ if (!ROOT_DIR) {
240
+ warn('CAPACITOR_ROOT_DIR is not set. Skipping automatic native configuration.');
241
+ return;
242
+ }
243
+
244
+ const config = parseConfig();
245
+ const shouldConfigureAndroid = !PLATFORM || PLATFORM === 'android';
246
+ const shouldConfigureIOS = !PLATFORM || PLATFORM === 'ios';
247
+
248
+ if (shouldConfigureAndroid) {
249
+ configureAndroid(ROOT_DIR, config.domains);
250
+ }
251
+
252
+ if (shouldConfigureIOS) {
253
+ configureIOS(ROOT_DIR, config.domains);
254
+ }
255
+
256
+ if (config.domains.length === 0) {
257
+ log(
258
+ 'No passkey domains configured. Add plugins.CapacitorPasskey.origin or domains in capacitor.config.* to enable automatic native wiring.',
259
+ );
260
+ }
261
+ }
262
+
263
+ run();