@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.
Files changed (66) hide show
  1. package/.nvmrc +1 -0
  2. package/.oxfmtrc.json +4 -0
  3. package/.oxlintrc.json +14 -0
  4. package/.releaserc +30 -0
  5. package/ARCHITECTURE.md +8 -0
  6. package/CHANGELOG.md +115 -0
  7. package/CONTRIBUTING.md +15 -5
  8. package/README.md +150 -11
  9. package/android/build.gradle +33 -10
  10. package/android/libs/repo/co/algorand/dP256Android/1.0.0/dP256Android-1.0.0.pom +9 -0
  11. package/android/src/main/AndroidManifest.xml +9 -3
  12. package/android/src/main/java/co/algorand/passkeyautofill/CreatePasskeyActivity.kt +396 -44
  13. package/android/src/main/java/co/algorand/passkeyautofill/GetPasskeyActivity.kt +583 -44
  14. package/android/src/main/java/co/algorand/passkeyautofill/Prf.kt +184 -0
  15. package/android/src/main/java/co/algorand/passkeyautofill/ReactNativePasskeyAutofillModule.kt +190 -2
  16. package/android/src/main/java/co/algorand/passkeyautofill/WebAuthn.kt +135 -0
  17. package/android/src/main/java/co/algorand/passkeyautofill/auth/BiometricRequirement.kt +86 -0
  18. package/android/src/main/java/co/algorand/passkeyautofill/credentials/Credential.kt +2 -1
  19. package/android/src/main/java/co/algorand/passkeyautofill/credentials/CredentialRepository.kt +314 -53
  20. package/android/src/main/java/co/algorand/passkeyautofill/service/PasskeyAutofillCredentialProviderService.kt +152 -38
  21. package/android/src/main/java/co/algorand/passkeyautofill/utils/PasskeyUtils.kt +119 -0
  22. package/android/src/main/res/values/styles.xml +10 -0
  23. package/app.plugin.js +593 -60
  24. package/assets/banner.png +0 -0
  25. package/build/ReactNativePasskeyAutofill.types.d.ts +41 -1
  26. package/build/ReactNativePasskeyAutofill.types.d.ts.map +1 -1
  27. package/build/ReactNativePasskeyAutofill.types.js +3 -1
  28. package/build/ReactNativePasskeyAutofill.types.js.map +1 -1
  29. package/build/ReactNativePasskeyAutofillModule.d.ts +41 -4
  30. package/build/ReactNativePasskeyAutofillModule.d.ts.map +1 -1
  31. package/build/ReactNativePasskeyAutofillModule.js +21 -2
  32. package/build/ReactNativePasskeyAutofillModule.js.map +1 -1
  33. package/build/index.d.ts +2 -2
  34. package/build/index.d.ts.map +1 -1
  35. package/build/index.js +2 -2
  36. package/build/index.js.map +1 -1
  37. package/e2e/README.md +155 -0
  38. package/e2e/driver/adb.ts +73 -0
  39. package/e2e/driver/capabilities.ts +106 -0
  40. package/e2e/driver/credentialProvider.ts +104 -0
  41. package/e2e/driver/fingerprint.ts +885 -0
  42. package/e2e/driver/selectors.ts +103 -0
  43. package/e2e/driver/session.ts +25 -0
  44. package/e2e/jest.config.ts +20 -0
  45. package/e2e/package.json +11 -0
  46. package/e2e/tests/passkey.test.ts +190 -0
  47. package/e2e/tsconfig.json +16 -0
  48. package/ios/AutofillCredentialProvider/BiometricRequirement.swift +34 -0
  49. package/ios/AutofillCredentialProvider/CredentialProviderViewController.swift +570 -0
  50. package/ios/AutofillCredentialProvider/PasskeyAutofillCredentialProvider-Bridging-Header.h +1 -0
  51. package/ios/AutofillCredentialProvider/PasskeyCredentialStore.swift +612 -0
  52. package/ios/AutofillCredentialProvider/PasskeyKeystoreMMKV.h +25 -0
  53. package/ios/AutofillCredentialProvider/PasskeyKeystoreMMKV.mm +86 -0
  54. package/ios/AutofillCredentialProvider/Prf.swift +89 -0
  55. package/ios/AutofillCredentialProvider/WebAuthn.swift +186 -0
  56. package/ios/ReactNativePasskeyAutofill.podspec +11 -2
  57. package/ios/ReactNativePasskeyAutofillModule.swift +170 -15
  58. package/lefthook.yml +14 -0
  59. package/package.json +115 -25
  60. package/pnpm-workspace.yaml +36 -0
  61. package/src/ReactNativePasskeyAutofill.types.ts +38 -0
  62. package/src/ReactNativePasskeyAutofillModule.ts +66 -3
  63. package/src/index.ts +2 -2
  64. package/tsconfig.json +2 -2
  65. package/.eslintrc.js +0 -5
  66. /package/android/libs/{dP256Android-release.aar → repo/co/algorand/dP256Android/1.0.0/dP256Android-1.0.0.aar} +0 -0
Binary file
@@ -1,2 +1,42 @@
1
- export type ReactNativePasskeyAutofillModuleEvents = {};
1
+ export type ReactNativePasskeyAutofillModuleEvents = {
2
+ onPasskeyAdded: (event: {
3
+ success: boolean;
4
+ }) => void;
5
+ onPasskeyAuthenticated: (event: {
6
+ success: boolean;
7
+ credentialId?: string;
8
+ }) => void;
9
+ };
10
+ export type PasskeyAutofillCredentialIdentity = {
11
+ credentialId: string;
12
+ relyingPartyIdentifier?: string;
13
+ rpId?: string;
14
+ origin?: string;
15
+ userName?: string;
16
+ name?: string;
17
+ userHandle: string;
18
+ userId?: string;
19
+ privateKey?: string;
20
+ privateKeyBase64?: string;
21
+ publicKey?: string;
22
+ publicKeyBase64?: string;
23
+ createdAt?: number;
24
+ lastUsedAt?: number;
25
+ parentKeyId?: string;
26
+ };
27
+ /**
28
+ * Capabilities advertised by this credential provider. Exposed so the
29
+ * surrounding wallet UI / RP-side wallet can know which WebAuthn extensions
30
+ * are wired up natively before issuing a passkey request.
31
+ */
32
+ export type PasskeyAutofillCapabilities = {
33
+ /**
34
+ * Whether the WebAuthn `prf` extension (a.k.a. `hmac-secret`) is supported
35
+ * by this credential provider. PRF outputs are derived deterministically
36
+ * from the wallet HD root secret so that restoring the wallet seed
37
+ * reproduces the same secrets on another device.
38
+ */
39
+ prf: boolean;
40
+ };
41
+ export declare const PASSKEY_AUTOFILL_CAPABILITIES: PasskeyAutofillCapabilities;
2
42
  //# sourceMappingURL=ReactNativePasskeyAutofill.types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ReactNativePasskeyAutofill.types.d.ts","sourceRoot":"","sources":["../src/ReactNativePasskeyAutofill.types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,sCAAsC,GAAG,EAEpD,CAAC"}
1
+ {"version":3,"file":"ReactNativePasskeyAutofill.types.d.ts","sourceRoot":"","sources":["../src/ReactNativePasskeyAutofill.types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,sCAAsC,GAAG;IACnD,cAAc,EAAE,CAAC,KAAK,EAAE;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,KAAK,IAAI,CAAC;IACtD,sBAAsB,EAAE,CAAC,KAAK,EAAE;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,YAAY,CAAC,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;CACtF,CAAC;AAEF,MAAM,MAAM,iCAAiC,GAAG;IAC9C,YAAY,EAAE,MAAM,CAAC;IACrB,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF;;;;GAIG;AACH,MAAM,MAAM,2BAA2B,GAAG;IACxC;;;;;OAKG;IACH,GAAG,EAAE,OAAO,CAAC;CACd,CAAC;AAEF,eAAO,MAAM,6BAA6B,EAAE,2BAE3C,CAAC"}
@@ -1,2 +1,4 @@
1
- export {};
1
+ export const PASSKEY_AUTOFILL_CAPABILITIES = {
2
+ prf: true,
3
+ };
2
4
  //# sourceMappingURL=ReactNativePasskeyAutofill.types.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"ReactNativePasskeyAutofill.types.js","sourceRoot":"","sources":["../src/ReactNativePasskeyAutofill.types.ts"],"names":[],"mappings":"","sourcesContent":["export type ReactNativePasskeyAutofillModuleEvents = {\n\n};\n"]}
1
+ {"version":3,"file":"ReactNativePasskeyAutofill.types.js","sourceRoot":"","sources":["../src/ReactNativePasskeyAutofill.types.ts"],"names":[],"mappings":"AAsCA,MAAM,CAAC,MAAM,6BAA6B,GAAgC;IACxE,GAAG,EAAE,IAAI;CACV,CAAC","sourcesContent":["export type ReactNativePasskeyAutofillModuleEvents = {\n onPasskeyAdded: (event: { success: boolean }) => void;\n onPasskeyAuthenticated: (event: { success: boolean; credentialId?: string }) => void;\n};\n\nexport type PasskeyAutofillCredentialIdentity = {\n credentialId: string;\n relyingPartyIdentifier?: string;\n rpId?: string;\n origin?: string;\n userName?: string;\n name?: string;\n userHandle: string;\n userId?: string;\n privateKey?: string;\n privateKeyBase64?: string;\n publicKey?: string;\n publicKeyBase64?: string;\n createdAt?: number;\n lastUsedAt?: number;\n parentKeyId?: string;\n};\n\n/**\n * Capabilities advertised by this credential provider. Exposed so the\n * surrounding wallet UI / RP-side wallet can know which WebAuthn extensions\n * are wired up natively before issuing a passkey request.\n */\nexport type PasskeyAutofillCapabilities = {\n /**\n * Whether the WebAuthn `prf` extension (a.k.a. `hmac-secret`) is supported\n * by this credential provider. PRF outputs are derived deterministically\n * from the wallet HD root secret so that restoring the wallet seed\n * reproduces the same secrets on another device.\n */\n prf: boolean;\n};\n\nexport const PASSKEY_AUTOFILL_CAPABILITIES: PasskeyAutofillCapabilities = {\n prf: true,\n};\n"]}
@@ -1,12 +1,49 @@
1
- import { NativeModule } from 'expo';
2
- import { ReactNativePasskeyAutofillModuleEvents } from './ReactNativePasskeyAutofill.types';
1
+ import { NativeModule } from "expo";
2
+ import { PasskeyAutofillCredentialIdentity, ReactNativePasskeyAutofillModuleEvents } from "./ReactNativePasskeyAutofill.types";
3
3
  declare class ReactNativePasskeyAutofillModule extends NativeModule<ReactNativePasskeyAutofillModuleEvents> {
4
4
  setMasterKey(secret: string): Promise<void>;
5
5
  setHdRootKeyId(id: string): Promise<void>;
6
6
  getHdRootKeyId(): Promise<string | null>;
7
7
  configureIntentActions(getPasskeyAction: string, createPasskeyAction: string): Promise<void>;
8
8
  clearCredentials(): Promise<void>;
9
+ deleteCredential(credentialId: string): Promise<void>;
10
+ /**
11
+ * iOS: returns the identities currently published to the AutoFill
12
+ * `ASCredentialIdentityStore`. Android: no-op returning `[]` because the
13
+ * native Credential Provider service reads credentials directly from MMKV
14
+ * and there is no separate identity store to query.
15
+ */
16
+ getStoredCredentials(): Promise<PasskeyAutofillCredentialIdentity[]>;
17
+ /**
18
+ * iOS: returns diagnostic strings from the AutoFill extension.
19
+ * Android: no-op returning `[]`.
20
+ */
21
+ getDiagnostics(): Promise<string[]>;
22
+ /**
23
+ * Replaces the iOS AutoFill passkey identity store with credentials
24
+ * available to the native Credential Provider extension. Each item must
25
+ * include a credential id, relying party (`relyingPartyIdentifier`,
26
+ * `rpId`, or `origin`), user handle, and P-256 private key material.
27
+ * Android ignores this method (no-op) because the provider reads MMKV
28
+ * directly.
29
+ */
30
+ replaceCredentialIdentities(credentials: PasskeyAutofillCredentialIdentity[]): Promise<void>;
31
+ refreshCredentialIdentities(): Promise<void>;
32
+ /**
33
+ * Resolves to `true` when this app is registered as the active
34
+ * credential/autofill provider on the current device (Android 14+
35
+ * credential provider or iOS AutoFill). Useful for gating passkey UI
36
+ * and for E2E tests that need to confirm the prompt that appears
37
+ * belongs to this provider and not a third party.
38
+ */
39
+ isProviderActive(): Promise<boolean>;
40
+ /**
41
+ * Opens the OS credential/autofill provider settings screen so the
42
+ * user can enable this app as the active provider. Resolves to `true`
43
+ * if a settings screen could be launched, `false` otherwise.
44
+ */
45
+ openProviderSettings(): Promise<boolean>;
9
46
  }
10
- declare const _default: ReactNativePasskeyAutofillModule;
11
- export default _default;
47
+ declare const nativeModule: ReactNativePasskeyAutofillModule;
48
+ export default nativeModule;
12
49
  //# sourceMappingURL=ReactNativePasskeyAutofillModule.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ReactNativePasskeyAutofillModule.d.ts","sourceRoot":"","sources":["../src/ReactNativePasskeyAutofillModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAuB,MAAM,MAAM,CAAC;AAEzD,OAAO,EAAE,sCAAsC,EAAE,MAAM,oCAAoC,CAAC;AAE5F,OAAO,OAAO,gCAAiC,SAAQ,YAAY,CAAC,sCAAsC,CAAC;IACzG,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAC3C,cAAc,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IACzC,cAAc,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IACxC,sBAAsB,CAAC,gBAAgB,EAAE,MAAM,EAAE,mBAAmB,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAC5F,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC;CAClC;;AAGD,wBAAmG"}
1
+ {"version":3,"file":"ReactNativePasskeyAutofillModule.d.ts","sourceRoot":"","sources":["../src/ReactNativePasskeyAutofillModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAuB,MAAM,MAAM,CAAC;AAGzD,OAAO,EACL,iCAAiC,EACjC,sCAAsC,EACvC,MAAM,oCAAoC,CAAC;AAE5C,OAAO,OAAO,gCAAiC,SAAQ,YAAY,CAAC,sCAAsC,CAAC;IACzG,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAC3C,cAAc,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IACzC,cAAc,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IACxC,sBAAsB,CAAC,gBAAgB,EAAE,MAAM,EAAE,mBAAmB,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAC5F,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC;IACjC,gBAAgB,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IACrD;;;;;OAKG;IACH,oBAAoB,IAAI,OAAO,CAAC,iCAAiC,EAAE,CAAC;IACpE;;;OAGG;IACH,cAAc,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;IACnC;;;;;;;OAOG;IACH,2BAA2B,CAAC,WAAW,EAAE,iCAAiC,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAC5F,2BAA2B,IAAI,OAAO,CAAC,IAAI,CAAC;IAC5C;;;;;;OAMG;IACH,gBAAgB,IAAI,OAAO,CAAC,OAAO,CAAC;IACpC;;;;OAIG;IACH,oBAAoB,IAAI,OAAO,CAAC,OAAO,CAAC;CACzC;AAGD,QAAA,MAAM,YAAY,kCAEjB,CAAC;AAoBF,eAAe,YAAY,CAAC"}
@@ -1,4 +1,23 @@
1
- import { requireNativeModule } from 'expo';
1
+ import { requireNativeModule } from "expo";
2
+ import { Platform } from "react-native";
2
3
  // This call loads the native module object from the JSI.
3
- export default requireNativeModule('ReactNativePasskeyAutofill');
4
+ const nativeModule = requireNativeModule("ReactNativePasskeyAutofill");
5
+ // A handful of methods are only implemented in the iOS native module because
6
+ // they interact with `ASCredentialIdentityStore`, which has no Android
7
+ // equivalent (the Android Credential Provider service reads credentials
8
+ // directly from MMKV on each request).
9
+ if (Platform.OS === "android") {
10
+ const noops = {
11
+ replaceCredentialIdentities: () => Promise.resolve(),
12
+ refreshCredentialIdentities: () => Promise.resolve(),
13
+ getStoredCredentials: () => Promise.resolve([]),
14
+ getDiagnostics: () => Promise.resolve([]),
15
+ };
16
+ for (const name of Object.keys(noops)) {
17
+ if (typeof nativeModule[name] !== "function") {
18
+ nativeModule[name] = noops[name];
19
+ }
20
+ }
21
+ }
22
+ export default nativeModule;
4
23
  //# sourceMappingURL=ReactNativePasskeyAutofillModule.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"ReactNativePasskeyAutofillModule.js","sourceRoot":"","sources":["../src/ReactNativePasskeyAutofillModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,mBAAmB,EAAE,MAAM,MAAM,CAAC;AAYzD,yDAAyD;AACzD,eAAe,mBAAmB,CAAmC,4BAA4B,CAAC,CAAC","sourcesContent":["import { NativeModule, requireNativeModule } from 'expo';\n\nimport { ReactNativePasskeyAutofillModuleEvents } from './ReactNativePasskeyAutofill.types';\n\ndeclare class ReactNativePasskeyAutofillModule extends NativeModule<ReactNativePasskeyAutofillModuleEvents> {\n setMasterKey(secret: string): Promise<void>;\n setHdRootKeyId(id: string): Promise<void>;\n getHdRootKeyId(): Promise<string | null>;\n configureIntentActions(getPasskeyAction: string, createPasskeyAction: string): Promise<void>;\n clearCredentials(): Promise<void>;\n}\n\n// This call loads the native module object from the JSI.\nexport default requireNativeModule<ReactNativePasskeyAutofillModule>('ReactNativePasskeyAutofill');\n"]}
1
+ {"version":3,"file":"ReactNativePasskeyAutofillModule.js","sourceRoot":"","sources":["../src/ReactNativePasskeyAutofillModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,mBAAmB,EAAE,MAAM,MAAM,CAAC;AACzD,OAAO,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAoDxC,yDAAyD;AACzD,MAAM,YAAY,GAAG,mBAAmB,CACtC,4BAA4B,CAC7B,CAAC;AAEF,6EAA6E;AAC7E,uEAAuE;AACvE,wEAAwE;AACxE,uCAAuC;AACvC,IAAI,QAAQ,CAAC,EAAE,KAAK,SAAS,EAAE,CAAC;IAC9B,MAAM,KAAK,GAA6D;QACtE,2BAA2B,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,OAAO,EAAE;QACpD,2BAA2B,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,OAAO,EAAE;QACpD,oBAAoB,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;QAC/C,cAAc,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;KAC1C,CAAC;IACF,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;QACtC,IAAI,OAAQ,YAAmD,CAAC,IAAI,CAAC,KAAK,UAAU,EAAE,CAAC;YACpF,YAAmD,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC;QAC3E,CAAC;IACH,CAAC;AACH,CAAC;AAED,eAAe,YAAY,CAAC","sourcesContent":["import { NativeModule, requireNativeModule } from \"expo\";\nimport { Platform } from \"react-native\";\n\nimport {\n PasskeyAutofillCredentialIdentity,\n ReactNativePasskeyAutofillModuleEvents,\n} from \"./ReactNativePasskeyAutofill.types\";\n\ndeclare class ReactNativePasskeyAutofillModule extends NativeModule<ReactNativePasskeyAutofillModuleEvents> {\n setMasterKey(secret: string): Promise<void>;\n setHdRootKeyId(id: string): Promise<void>;\n getHdRootKeyId(): Promise<string | null>;\n configureIntentActions(getPasskeyAction: string, createPasskeyAction: string): Promise<void>;\n clearCredentials(): Promise<void>;\n deleteCredential(credentialId: string): Promise<void>;\n /**\n * iOS: returns the identities currently published to the AutoFill\n * `ASCredentialIdentityStore`. Android: no-op returning `[]` because the\n * native Credential Provider service reads credentials directly from MMKV\n * and there is no separate identity store to query.\n */\n getStoredCredentials(): Promise<PasskeyAutofillCredentialIdentity[]>;\n /**\n * iOS: returns diagnostic strings from the AutoFill extension.\n * Android: no-op returning `[]`.\n */\n getDiagnostics(): Promise<string[]>;\n /**\n * Replaces the iOS AutoFill passkey identity store with credentials\n * available to the native Credential Provider extension. Each item must\n * include a credential id, relying party (`relyingPartyIdentifier`,\n * `rpId`, or `origin`), user handle, and P-256 private key material.\n * Android ignores this method (no-op) because the provider reads MMKV\n * directly.\n */\n replaceCredentialIdentities(credentials: PasskeyAutofillCredentialIdentity[]): Promise<void>;\n refreshCredentialIdentities(): Promise<void>;\n /**\n * Resolves to `true` when this app is registered as the active\n * credential/autofill provider on the current device (Android 14+\n * credential provider or iOS AutoFill). Useful for gating passkey UI\n * and for E2E tests that need to confirm the prompt that appears\n * belongs to this provider and not a third party.\n */\n isProviderActive(): Promise<boolean>;\n /**\n * Opens the OS credential/autofill provider settings screen so the\n * user can enable this app as the active provider. Resolves to `true`\n * if a settings screen could be launched, `false` otherwise.\n */\n openProviderSettings(): Promise<boolean>;\n}\n\n// This call loads the native module object from the JSI.\nconst nativeModule = requireNativeModule<ReactNativePasskeyAutofillModule>(\n \"ReactNativePasskeyAutofill\",\n);\n\n// A handful of methods are only implemented in the iOS native module because\n// they interact with `ASCredentialIdentityStore`, which has no Android\n// equivalent (the Android Credential Provider service reads credentials\n// directly from MMKV on each request).\nif (Platform.OS === \"android\") {\n const noops: Record<string, (...args: unknown[]) => Promise<unknown>> = {\n replaceCredentialIdentities: () => Promise.resolve(),\n refreshCredentialIdentities: () => Promise.resolve(),\n getStoredCredentials: () => Promise.resolve([]),\n getDiagnostics: () => Promise.resolve([]),\n };\n for (const name of Object.keys(noops)) {\n if (typeof (nativeModule as unknown as Record<string, unknown>)[name] !== \"function\") {\n (nativeModule as unknown as Record<string, unknown>)[name] = noops[name];\n }\n }\n}\n\nexport default nativeModule;\n"]}
package/build/index.d.ts CHANGED
@@ -1,3 +1,3 @@
1
- export { default } from './ReactNativePasskeyAutofillModule';
2
- export * from './ReactNativePasskeyAutofill.types';
1
+ export { default } from "./ReactNativePasskeyAutofillModule";
2
+ export * from "./ReactNativePasskeyAutofill.types";
3
3
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,OAAO,EAAE,MAAM,oCAAoC,CAAC;AAC7D,cAAe,oCAAoC,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,OAAO,EAAE,MAAM,oCAAoC,CAAC;AAC7D,cAAc,oCAAoC,CAAC"}
package/build/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // Reexport the native module. On web, it will be resolved to ReactNativePasskeyAutofillModule.web.ts
2
2
  // and on native platforms to ReactNativePasskeyAutofillModule.ts
3
- export { default } from './ReactNativePasskeyAutofillModule';
4
- export * from './ReactNativePasskeyAutofill.types';
3
+ export { default } from "./ReactNativePasskeyAutofillModule";
4
+ export * from "./ReactNativePasskeyAutofill.types";
5
5
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,qGAAqG;AACrG,iEAAiE;AACjE,OAAO,EAAE,OAAO,EAAE,MAAM,oCAAoC,CAAC;AAC7D,cAAe,oCAAoC,CAAC","sourcesContent":["// Reexport the native module. On web, it will be resolved to ReactNativePasskeyAutofillModule.web.ts\n// and on native platforms to ReactNativePasskeyAutofillModule.ts\nexport { default } from './ReactNativePasskeyAutofillModule';\nexport * from './ReactNativePasskeyAutofill.types';\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,qGAAqG;AACrG,iEAAiE;AACjE,OAAO,EAAE,OAAO,EAAE,MAAM,oCAAoC,CAAC;AAC7D,cAAc,oCAAoC,CAAC","sourcesContent":["// Reexport the native module. On web, it will be resolved to ReactNativePasskeyAutofillModule.web.ts\n// and on native platforms to ReactNativePasskeyAutofillModule.ts\nexport { default } from \"./ReactNativePasskeyAutofillModule\";\nexport * from \"./ReactNativePasskeyAutofill.types\";\n"]}
package/e2e/README.md ADDED
@@ -0,0 +1,155 @@
1
+ # E2E — Appium + WebdriverIO
2
+
3
+ End-to-end tests driving the `example/` app with [Appium 2](https://appium.io)
4
+ through [WebdriverIO](https://webdriver.io). Tests are written in TypeScript
5
+ and executed with Jest (to stay consistent with the rest of the repo).
6
+
7
+ The happy-path test mirrors the example app: it mints a local XHD Ed25519
8
+ key, creates a passkey against `https://debug.liquidauth.com`, and then
9
+ re-uses it for an assertion round-trip.
10
+
11
+ ## Layout
12
+
13
+ ```
14
+ e2e/
15
+ ├── driver/
16
+ │ ├── capabilities.ts # Android (UiAutomator2) + iOS (XCUITest) caps
17
+ │ ├── selectors.ts # testID + system-dialog helpers
18
+ │ └── session.ts # createDriver() — wraps webdriverio.remote()
19
+ ├── tests/
20
+ │ └── passkey.test.ts # smoke + passkey create/use against debug.liquidauth.com
21
+ ├── jest.config.ts
22
+ ├── package.json
23
+ └── tsconfig.json
24
+ ```
25
+
26
+ All app-level taps use React Native `testID`s (exposed as
27
+ `accessibility id` on both platforms) via `byTestId("...")`. System
28
+ WebAuthn/biometric dialogs are auto-confirmed by `confirmSystemPrompt()`.
29
+
30
+ ## Provider-active precondition
31
+
32
+ The example app exposes a provider-status banner so the suite (and
33
+ human testers) can confirm that _this_ package is the user-selected
34
+ system credential/autofill provider before exercising passkey flows —
35
+ otherwise another installed password manager could silently serve the
36
+ system WebAuthn prompts. The example exposes:
37
+
38
+ - A `testID="provider-status"` label whose text reads `active`,
39
+ `inactive`, or `unknown`.
40
+ - A `testID="open-provider-settings"` CTA that deep-links to the OS
41
+ settings (Android: credential provider picker; iOS: AutoFill
42
+ settings).
43
+
44
+ The first suite step (`surfaces the initial provider status`) is a
45
+ smoke read only. The authoritative assertion runs after the passkey
46
+ creation step: once Android has routed a
47
+ `BeginCreate/BeginGetCredentialRequest` to our
48
+ `PasskeyAutofillCredentialProviderService`, the service stamps an MMKV
49
+ key that `ReactNativePasskeyAutofill.isProviderActive()` picks up and
50
+ the banner flips to `active`. This two-signal design sidesteps the
51
+ fact that `Settings.Secure("credential_service")` is `@hide`-restricted
52
+ on Android 12+ and cannot be read by a regular app.
53
+
54
+ If the passkey flow never reaches the stamp, enable the app manually
55
+ on the device:
56
+
57
+ - **Android 14+**: Settings → Passwords, passkeys & accounts →
58
+ Additional providers → _Passkey AutoFill Example_.
59
+ - **iOS 17+**: Settings → Passwords → Password Options → enable
60
+ _Passkey AutoFill Example_.
61
+
62
+ ## Biometric prompts
63
+
64
+ The passkey create/use flow ultimately routes through either the
65
+ system's Single-Tap bottom sheet (when the provider attaches
66
+ `BiometricPromptData` + `CryptoObject`) or the fallback
67
+ `CreatePasskeyActivity` / `GetPasskeyActivity` screens. Both require a
68
+ biometric confirmation from the user. On emulators the suite automates
69
+ this by synthesising finger touches via `adb` (Android) or the
70
+ `enrollBiometric` extension (iOS).
71
+
72
+ ## Prerequisites
73
+
74
+ - Node.js 22+, pnpm 10+
75
+ - **Android**: Android SDK, an AVD (API 34 recommended). Biometrics
76
+ are automatically enrolled by the suite on first run.
77
+ - **iOS** (macOS only): Xcode 16.1+, an iOS 17+ Simulator. Face ID is
78
+ automatically enrolled by the suite.
79
+ - Appium drivers. The driver packages are declared as regular `devDependencies`
80
+ and installed by `pnpm install`, so they are version-locked alongside the
81
+ rest of the workspace. The `appium:setup*` scripts then register them with
82
+ the Appium CLI from their local `node_modules` path (no `npm` shell-out,
83
+ no global `~/.appium` mutation required beyond Appium's own manifest):
84
+
85
+ ```bash
86
+ pnpm install # installs appium-uiautomator2-driver & appium-xcuitest-driver
87
+ pnpm run appium:setup # register both drivers with Appium
88
+ pnpm run appium:setup:android # Android only
89
+ pnpm run appium:setup:ios # iOS only (macOS)
90
+ ```
91
+
92
+ ## Running locally
93
+
94
+ 1. Install repo deps:
95
+
96
+ ```bash
97
+ pnpm install
98
+ ```
99
+
100
+ 2. Build the example app for the target platform:
101
+
102
+ ```bash
103
+ pnpm run e2e:build:android # produces example/android/app/build/outputs/apk/release/app-release.apk
104
+ pnpm run e2e:build:ios # produces example/ios/build/Build/Products/Release-iphonesimulator/PasskeyAutofillExample.app
105
+ ```
106
+
107
+ 3. Boot an emulator/simulator, then run:
108
+
109
+ ```bash
110
+ pnpm run e2e:android
111
+ # or
112
+ pnpm run e2e:ios
113
+ ```
114
+
115
+ These scripts start Appium via `start-server-and-test`, wait for
116
+ `http://127.0.0.1:4723/status`, execute the Jest suite, and shut
117
+ Appium down afterwards.
118
+
119
+ ### Overrides
120
+
121
+ | Env var | Purpose |
122
+ | ---------------------------- | ------------------------------------------------ |
123
+ | `E2E_PLATFORM` | `android` (default) or `ios` |
124
+ | `E2E_APP_PATH` | Absolute path to a prebuilt `.apk` / `.app` |
125
+ | `APPIUM_HOST`, `APPIUM_PORT` | Appium server (defaults `127.0.0.1:4723`) |
126
+ | `ANDROID_PLATFORM_VERSION` | e.g. `"14"` |
127
+ | `ANDROID_DEVICE_NAME` | e.g. `"Pixel_6_API_34"` |
128
+ | `IOS_PLATFORM_VERSION` | e.g. `"17.5"` |
129
+ | `IOS_DEVICE_NAME` | e.g. `"iPhone 15"` |
130
+ | `WDIO_LOG_LEVEL` | `error` \| `warn` (default) \| `info` \| `debug` |
131
+
132
+ ## CI
133
+
134
+ The [`E2E` workflow](../.github/workflows/e2e.yml) runs on
135
+ `workflow_dispatch` or when a PR is labelled `e2e`:
136
+
137
+ - **Android** job builds the release APK, installs the UiAutomator2 driver,
138
+ and runs the suite inside `reactivecircus/android-emulator-runner`.
139
+ - **iOS** job (currently disabled) builds the `.app` for the iPhone Simulator,
140
+ installs the XCUITest driver, boots a simulator, and runs the suite.
141
+
142
+ Screenshots of the test run are uploaded as workflow artifacts for debugging.
143
+
144
+ ## Authoring new tests
145
+
146
+ 1. Add a stable `testID` to the target React Native component.
147
+ 2. Import the helpers:
148
+
149
+ ```ts
150
+ import { createDriver } from "../driver/session";
151
+ import { byTestId, tap, confirmSystemPrompt } from "../driver/selectors";
152
+ ```
153
+
154
+ 3. Drive the app with `tap(driver, byTestId("..."))`. Use
155
+ `confirmSystemPrompt()` for OS-level WebAuthn/biometric dialogs.
@@ -0,0 +1,73 @@
1
+ import { execFile, execFileSync } from "node:child_process";
2
+
3
+ /**
4
+ * Common ADB helpers to ensure consistent device targeting across the E2E suite.
5
+ */
6
+
7
+ export function getAdbSerial(): string {
8
+ if (process.env.ANDROID_UDID) return process.env.ANDROID_UDID;
9
+
10
+ // Try to find the first connected emulator or device.
11
+ try {
12
+ const devices = execFileSync("adb", ["devices"], {
13
+ encoding: "utf8",
14
+ timeout: 5000,
15
+ })
16
+ .split("\n")
17
+ .slice(1)
18
+ .filter((l) => l.includes("\tdevice"))
19
+ .map((l) => l.split("\t")[0].trim());
20
+
21
+ if (devices.length === 1) return devices[0];
22
+ if (devices.length > 1) {
23
+ const emulators = devices.filter((d) => d.startsWith("emulator-"));
24
+ if (emulators.length > 0) return emulators[0];
25
+ return devices[0];
26
+ }
27
+ } catch (err) {
28
+ // eslint-disable-next-line no-console
29
+ console.warn("[e2e:adb] failed to list devices via adb:", (err as Error).message);
30
+ }
31
+
32
+ return "emulator-5554";
33
+ }
34
+
35
+ /**
36
+ * Wrap adb so we can swap serials / run headless without inheriting
37
+ * stdin from the caller (Jest runners hate dangling fds).
38
+ */
39
+ export function adb(args: string[], opts: { timeout?: number } = {}): string {
40
+ const serial = getAdbSerial();
41
+ try {
42
+ return execFileSync("adb", ["-s", serial, ...args], {
43
+ encoding: "utf8",
44
+ stdio: ["ignore", "pipe", "pipe"],
45
+ timeout: opts.timeout ?? 15_000,
46
+ }).trim();
47
+ } catch (err) {
48
+ const error = err as Error & { stderr?: string };
49
+ const msg = error.stderr ? `${error.message}\n${error.stderr}` : error.message;
50
+ throw new Error(`adb command failed: adb -s ${serial} ${args.join(" ")}\n${msg}`);
51
+ }
52
+ }
53
+
54
+ export function adbSilent(args: string[]): string {
55
+ try {
56
+ const serial = getAdbSerial();
57
+ return execFileSync("adb", ["-s", serial, ...args], {
58
+ encoding: "utf8",
59
+ stdio: ["ignore", "pipe", "pipe"],
60
+ timeout: 15_000,
61
+ }).trim();
62
+ } catch {
63
+ return "";
64
+ }
65
+ }
66
+
67
+ export function adbAsync(args: string[]): void {
68
+ // Fire-and-forget, used for the finger-touch spam where we don't
69
+ // want to block the test thread on adb's per-call latency (~50ms).
70
+ const serial = getAdbSerial();
71
+ const child = execFile("adb", ["-s", serial, ...args], () => undefined);
72
+ child.stdin?.end();
73
+ }
@@ -0,0 +1,106 @@
1
+ import path from "node:path";
2
+
3
+ /**
4
+ * Platform target for the E2E run. Selected via the `E2E_PLATFORM`
5
+ * environment variable (defaults to `android`).
6
+ */
7
+ export type Platform = "android" | "ios";
8
+
9
+ export const PLATFORM: Platform = (process.env.E2E_PLATFORM as Platform | undefined) ?? "android";
10
+
11
+ /** Example app identifiers (must match `example/app.json`). */
12
+ export const ANDROID_PACKAGE = "co.algorand.auth.example";
13
+ export const IOS_BUNDLE_ID = "co.algorand.auth.example";
14
+
15
+ /**
16
+ * Absolute paths to the built example app binaries. Override with
17
+ * `E2E_APP_PATH` to point at a prebuilt artifact in CI. When omitted
18
+ * the helpers below fall back to the default expo-built location.
19
+ */
20
+ const repoRoot = path.resolve(__dirname, "..", "..");
21
+ const defaultAndroidApk = path.join(
22
+ repoRoot,
23
+ "example",
24
+ "android",
25
+ "app",
26
+ "build",
27
+ "outputs",
28
+ "apk",
29
+ "release",
30
+ "app-release.apk",
31
+ );
32
+ const defaultIosApp = path.join(
33
+ repoRoot,
34
+ "example",
35
+ "ios",
36
+ "build",
37
+ "Build",
38
+ "Products",
39
+ "Release-iphonesimulator",
40
+ "PasskeyAutofillExample.app",
41
+ );
42
+
43
+ export const ANDROID_APP_PATH = process.env.E2E_APP_PATH ?? defaultAndroidApk;
44
+ export const IOS_APP_PATH = process.env.E2E_APP_PATH ?? defaultIosApp;
45
+
46
+ export const APPIUM_HOST = process.env.APPIUM_HOST ?? "127.0.0.1";
47
+ export const APPIUM_PORT = Number(process.env.APPIUM_PORT ?? 4723);
48
+
49
+ export interface Capabilities {
50
+ [key: string]: unknown;
51
+ }
52
+
53
+ /**
54
+ * Android (UiAutomator2) capabilities. `noReset: false` ensures a clean
55
+ * keystore/passkey state between runs so tests are idempotent.
56
+ */
57
+ export const androidCapabilities: Capabilities = {
58
+ platformName: "Android",
59
+ "appium:automationName": "UiAutomator2",
60
+ // `platformVersion` is intentionally only set when explicitly provided:
61
+ // when omitted, UiAutomator2 attaches to whichever device is currently
62
+ // connected via `adb`, which is the common case on a developer laptop.
63
+ ...(process.env.ANDROID_PLATFORM_VERSION
64
+ ? { "appium:platformVersion": process.env.ANDROID_PLATFORM_VERSION }
65
+ : {}),
66
+ // Pin the target device when set (matches `ANDROID_UDID` used by the adb
67
+ // helpers) so a run is deterministic when multiple devices are attached.
68
+ ...(process.env.ANDROID_UDID ? { "appium:udid": process.env.ANDROID_UDID } : {}),
69
+ "appium:deviceName": process.env.ANDROID_DEVICE_NAME ?? "Android Emulator",
70
+ "appium:app": ANDROID_APP_PATH,
71
+ "appium:appPackage": ANDROID_PACKAGE,
72
+ "appium:autoGrantPermissions": true,
73
+ "appium:noReset": false,
74
+ "appium:fullReset": false,
75
+ "appium:newCommandTimeout": 240,
76
+ // Bumped to absorb cold-AVD session-install flakes: on a fresh
77
+ // `google_apis_playstore` AVD, the UiAutomator2 server APK + the
78
+ // `io.appium.settings` helper app can occasionally take longer than
79
+ // the 30s default to install and start, producing
80
+ // `Appium Settings app is not running after 30000ms` at session
81
+ // creation. These knobs are the documented escape hatches.
82
+ "appium:uiautomator2ServerInstallTimeout": 120_000,
83
+ "appium:uiautomator2ServerLaunchTimeout": 120_000,
84
+ "appium:uiautomator2ServerReadTimeout": 240_000,
85
+ "appium:adbExecTimeout": 60_000,
86
+ "appium:appWaitActivity": "*",
87
+ "appium:appWaitDuration": 60_000,
88
+ };
89
+
90
+ /**
91
+ * iOS (XCUITest) capabilities. Targets the simulator.
92
+ */
93
+ export const iosCapabilities: Capabilities = {
94
+ platformName: "iOS",
95
+ "appium:automationName": "XCUITest",
96
+ "appium:platformVersion": process.env.IOS_PLATFORM_VERSION ?? "17.5",
97
+ "appium:deviceName": process.env.IOS_DEVICE_NAME ?? "iPhone 15",
98
+ "appium:app": IOS_APP_PATH,
99
+ "appium:bundleId": IOS_BUNDLE_ID,
100
+ "appium:noReset": false,
101
+ "appium:newCommandTimeout": 240,
102
+ };
103
+
104
+ export function capabilitiesForPlatform(platform: Platform = PLATFORM): Capabilities {
105
+ return platform === "ios" ? iosCapabilities : androidCapabilities;
106
+ }
@@ -0,0 +1,104 @@
1
+ import { PLATFORM, ANDROID_PACKAGE } from "./capabilities";
2
+ import { adbSilent } from "./adb";
3
+
4
+ /**
5
+ * Credential Manager provider registration helper (Android only).
6
+ *
7
+ * Why this file exists
8
+ * --------------------
9
+ * On Android 14 the Credential Manager only routes `BeginCreate*` /
10
+ * `BeginGet*` requests to credential-provider services that the user has
11
+ * explicitly enabled in Settings → Passwords, passkeys & accounts →
12
+ * Additional providers. A freshly booted AVD has **no** providers
13
+ * enabled, so:
14
+ *
15
+ * - `handleCreatePasskey` opens a Credential Manager sheet that lists
16
+ * zero providers, the consent screen is dismissed by the system, and
17
+ * our service never receives a `BeginCreateCredentialRequest` — so
18
+ * `provider-status` stays `inactive` forever.
19
+ * - Every test after "creates an XHD Ed25519 key" then fails because
20
+ * the passkey flow deadlocks on an empty system UI.
21
+ *
22
+ * Settings → Additional providers writes the enabled component names to
23
+ * the secure setting `credential_service` (comma-separated) and the
24
+ * primary provider to `credential_service_primary`. We can do the same
25
+ * thing via `adb shell settings put secure ...` — the `adb shell` user
26
+ * already holds `WRITE_SECURE_SETTINGS`, so no root / magisk required.
27
+ *
28
+ * This helper is idempotent and a no-op on iOS / when the
29
+ * target APK isn't installed.
30
+ */
31
+
32
+ function isAndroidTarget(): boolean {
33
+ return PLATFORM === "android";
34
+ }
35
+
36
+ let registered = false;
37
+
38
+ /**
39
+ * Enable our passkey provider service as the system's credential
40
+ * provider so Credential Manager actually routes requests to it.
41
+ *
42
+ * Returns `true` if the `credential_service` secure setting ends up
43
+ * containing our component. Safe to call repeatedly; no-ops on iOS.
44
+ */
45
+ // Must stay in sync with `android/src/main/AndroidManifest.xml` and
46
+ // `android/build.gradle` (`namespace`). The component name is:
47
+ // <applicationId>/<library-namespace>.service.PasskeyAutofillCredentialProviderService
48
+ // i.e. the host app id + the FQCN of the service declared by the
49
+ // library whose manifest is merged into the example app.
50
+ const SERVICE_FQCN = "co.algorand.passkeyautofill.service.PasskeyAutofillCredentialProviderService";
51
+
52
+ export async function ensureAndroidCredentialProviderEnabled(): Promise<boolean> {
53
+ if (!isAndroidTarget()) return false;
54
+ if (registered) return true;
55
+
56
+ const component = `${ANDROID_PACKAGE}/${SERVICE_FQCN}`;
57
+
58
+ // Guard: the service must actually be installed — otherwise the
59
+ // Credential Manager silently drops the component on first use and
60
+ // our `settings put` is effectively a no-op.
61
+ const pmCheck = adbSilent(["shell", "pm", "list", "packages", ANDROID_PACKAGE]);
62
+ if (!pmCheck.includes(ANDROID_PACKAGE)) {
63
+ // eslint-disable-next-line no-console
64
+ console.warn(
65
+ `[e2e] credential-provider: ${ANDROID_PACKAGE} not installed yet; ` +
66
+ "skipping provider registration.",
67
+ );
68
+ return false;
69
+ }
70
+
71
+ // Write both the primary slot and the list slot. Different AOSP
72
+ // builds read one or the other (Android 14 stock uses
73
+ // `credential_service_primary` for the default, `credential_service`
74
+ // for additional providers); setting both makes us the only
75
+ // configured provider either way.
76
+ adbSilent(["shell", "settings", "put", "secure", "credential_service", component]);
77
+ adbSilent(["shell", "settings", "put", "secure", "credential_service_primary", component]);
78
+
79
+ // Some OEM Settings UIs also persist the enabled providers into
80
+ // `autofill_service` when Autofill & Credential Manager share the
81
+ // provider. Best-effort mirror; failure is fine.
82
+ adbSilent(["shell", "settings", "put", "secure", "autofill_service", component]);
83
+
84
+ // Verify what the system actually stored. `settings get` returns the
85
+ // literal value or "null" if unset.
86
+ const got = adbSilent(["shell", "settings", "get", "secure", "credential_service"]);
87
+ const gotPrimary = adbSilent([
88
+ "shell",
89
+ "settings",
90
+ "get",
91
+ "secure",
92
+ "credential_service_primary",
93
+ ]);
94
+
95
+ const ok = got.includes(component) || gotPrimary.includes(component);
96
+ // eslint-disable-next-line no-console
97
+ console.log(
98
+ `[e2e] credential-provider: component=${component} registered=${ok} ` +
99
+ `(credential_service=${got || "null"}, primary=${gotPrimary || "null"})`,
100
+ );
101
+
102
+ registered = ok;
103
+ return ok;
104
+ }