Skip to content

Commit

Permalink
add web credential, and expo web compat
Browse files Browse the repository at this point in the history
  • Loading branch information
code-z2 committed Sep 30, 2024
1 parent c8b5e58 commit 846080e
Show file tree
Hide file tree
Showing 8 changed files with 406 additions and 92 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,13 @@ class Cosmr1CredentialHandlerModule : Module() {
}

Constants(
"timeout" to Constants.TIMEOUT,
"attestation" to Constants.ATTESTATION,
"authenticatorAttachment" to Constants.AUTHENTICATOR_ATTACHMENT,
"requireResidentKey" to Constants.REQUIRE_RESIDENT_KEY,
"residentKey" to Constants.RESIDENT_KEY,
"userVerification" to Constants.USER_VERIFICATION,
"pubKeyCredParam" to Constants.PUB_KEY_CRED_PARAM
"TIMEOUT" to Constants.TIMEOUT,
"ATTESTATION" to Constants.ATTESTATION,
"AUTHENTICATOR_ATTACHMENT" to Constants.AUTHENTICATOR_ATTACHMENT,
"REQUIRE_RESIDENT_KEY" to Constants.REQUIRE_RESIDENT_KEY,
"RESIDENT_KEY" to Constants.RESIDENT_KEY,
"USER_VERIFICATION" to Constants.USER_VERIFICATION,
"PUB_KEY_CRED_PARAM" to Constants.PUB_KEY_CRED_PARAM
)

Events(
Expand Down
3 changes: 2 additions & 1 deletion packages/cred-native/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@
"license": "MIT",
"homepage": "https://github.com/vaariance/cosm-r1#readme",
"dependencies": {
"shared-types": "*"
"@vaariance/shared-types": "*",
"@vaariance/cred-web": "*"
},
"devDependencies": {
"@types/react": "^18.0.25",
Expand Down
95 changes: 93 additions & 2 deletions packages/cred-native/src/Cosmr1CredentialHandlerModule.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,96 @@
import { requireNativeModule } from 'expo-modules-core';
import {
type EventTypeMap,
type PublicKeyCredential,
type AuthenticatorAttestationResponse,
type AuthenticatorAssertionResponse,
type AttestationOptions,
type AssertionOptions,
} from "@vaariance/shared-types";
import {
requireNativeModule,
NativeModulesProxy,
EventEmitter,
} from "expo-modules-core";

// It loads the native module object from the JSI or falls back to
// the bridge module (from NativeModulesProxy) if the remote debugger is on.
export default requireNativeModule('Cosmr1CredentialHandler');
const nativeModule = requireNativeModule("Cosmr1CredentialHandler");

const emitter = new EventEmitter(
nativeModule ?? NativeModulesProxy.Cosmr1CredentialHandler,
);

const moduleObjects = {
defaultConfiguraton: {
TIMEOUT: nativeModule.TIMEOUT,
ATTESTATION: nativeModule.ATTESTATION as AttestationConveyancePreference,
AUTHENTICATOR_ATTACHMENT:
nativeModule.AUTHENTICATOR_ATTACHMENT as AuthenticatorAttachment,
REQUIRE_RESIDENT_KEY: nativeModule.REQUIRE_RESIDENT_KEY as boolean,
RESIDENT_KEY: nativeModule.RESIDENT_KEY as ResidentKeyRequirement,
USER_VERIFICATION:
nativeModule.USER_VERIFICATION as UserVerificationRequirement,
PUB_KEY_CRED_PARAM:
nativeModule.PUB_KEY_CRED_PARAM as PublicKeyCredentialParameters,
},

events: {
onRegistrationStarted: (
callback: (event: EventTypeMap["onRegistrationStarted"]) => void,
) => emitter.addListener("onRegistrationStarted", callback),
onRegistrationFailed: (
callback: (event: EventTypeMap["onRegistrationFailed"]) => void,
) => emitter.addListener("onRegistrationFailed", callback),
onRegistrationComplete: (
callback: (event: EventTypeMap["onRegistrationComplete"]) => void,
) => emitter.addListener("onRegistrationComplete", callback),
onAuthenticationStarted: (
callback: (event: EventTypeMap["onAuthenticationStarted"]) => void,
) => emitter.addListener("onAuthenticationStarted", callback),
onAuthenticationFailed: (
callback: (event: EventTypeMap["onAuthenticationFailed"]) => void,
) => emitter.addListener("onAuthenticationFailed", callback),
onAuthenticationSuccess: (
callback: (event: EventTypeMap["onAuthenticationSuccess"]) => void,
) => emitter.addListener("onAuthenticationSuccess", callback),
},
};

const mainFunctions = {
async register(
args: AttestationOptions<string>,
): Promise<PublicKeyCredential<
string,
AuthenticatorAttestationResponse<string>
> | null> {
const credential = await nativeModule.register(
args.preferImmediatelyAvailableCred ?? false,
args.challenge,
args.rp,
args.user,
args.timeout,
args.attestation,
args.excludeCredentials,
args.authenticatorSelection,
);
return JSON.parse(credential);
},

async authenticate(
args: AssertionOptions<string>,
): Promise<PublicKeyCredential<
string,
AuthenticatorAssertionResponse<string>
> | null> {
const credential = await nativeModule.authenticate(
args.challenge,
args.timeout,
args.rpId,
args.userVerification,
args.allowCredentials,
);
return JSON.parse(credential);
},
};

export default Object.assign(moduleObjects, mainFunctions);
13 changes: 4 additions & 9 deletions packages/cred-native/src/Cosmr1CredentialHandlerModule.web.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,8 @@
import Cosmr1CredentialHandlerModule from "@vaariance/cred-web";
import { EventEmitter } from "expo-modules-core";

const emitter = new EventEmitter({} as any);

export default {
PI: Math.PI,
async setValueAsync(value: string): Promise<void> {
emitter.emit("onChange", { value });
},
hello() {
return "Hello world! 👋";
},
};
const webModule = Cosmr1CredentialHandlerModule(emitter);

export default webModule;
29 changes: 1 addition & 28 deletions packages/cred-native/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,5 @@
import {
NativeModulesProxy,
EventEmitter,
Subscription,
} from "expo-modules-core";

// Import the native module. On web, it will be resolved to Cosmr1CredentialHandler.web.ts
// and on native platforms to Cosmr1CredentialHandler.ts
//import { ChangeEventPayload } from "shared-types";

import Cosmr1CredentialHandlerModule from "./Cosmr1CredentialHandlerModule";

// Get the native constant value.
export const PI = Cosmr1CredentialHandlerModule.PI;

export function hello(): string {
return Cosmr1CredentialHandlerModule.hello();
}

export async function setValueAsync(value: string) {
return await Cosmr1CredentialHandlerModule.setValueAsync(value);
}

const emitter = new EventEmitter(
Cosmr1CredentialHandlerModule ?? NativeModulesProxy.Cosmr1CredentialHandler,
);

// export function addChangeListener(
// listener: (event: ChangeEventPayload) => void,
// ): Subscription {
// return emitter.addListener<ChangeEventPayload>("onChange", listener);
// }
export default Cosmr1CredentialHandlerModule;
193 changes: 192 additions & 1 deletion packages/cred-web/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,192 @@
console.log("Hello via Bun!");
import {
type AssertionOptions,
type AttestationOptions,
type CreateCredentialOptions,
type GetCredentialOptions,
type PublicKeyCredential,
type AuthenticatorAttestationResponse,
type AuthenticatorAssertionResponse,
type Cosmr1CredentialHandlerModuleEvents,
type Subscription,
type EventEmitter,
type EventTypeMap,
Constants,
} from "@vaariance/shared-types";

const EventEmitter = (): EventEmitter => {
const listeners = <
{
[K in keyof EventTypeMap]: Set<(event: EventTypeMap[K]) => void>;
}
>{};

return {
addListener<T extends Cosmr1CredentialHandlerModuleEvents>(
eventName: T,
listener: (event: EventTypeMap[T]) => void,
): Subscription {
if (!listeners[eventName]) {
listeners[eventName] = new Set<any>();
}
listeners[eventName].add(listener);

return {
remove: () => {
listeners[eventName].delete(listener);
},
};
},

removeAllListeners(eventName: Cosmr1CredentialHandlerModuleEvents): void {
delete listeners[eventName];
},

removeSubscription(subscription: Subscription): void {
subscription.remove();
},

emit<T extends Cosmr1CredentialHandlerModuleEvents>(
eventName: T,
params: EventTypeMap[T],
): void {
const callbacks = listeners[eventName];
if (callbacks) {
callbacks.forEach((callback) => callback(params));
}
},
};
};

const Cosmr1CredentialHandlerModule = (
eventEmitter: EventEmitter = EventEmitter(),
) => {
const moduleObject = {
// Constants
defaultConfiguraton: { ...Constants },

// Events
events: {
onRegistrationStarted: (
callback: (event: EventTypeMap["onRegistrationStarted"]) => void,
) => eventEmitter.addListener("onRegistrationStarted", callback),
onRegistrationFailed: (
callback: (event: EventTypeMap["onRegistrationFailed"]) => void,
) => eventEmitter.addListener("onRegistrationFailed", callback),
onRegistrationComplete: (
callback: (event: EventTypeMap["onRegistrationComplete"]) => void,
) => eventEmitter.addListener("onRegistrationComplete", callback),
onAuthenticationStarted: (
callback: (event: EventTypeMap["onAuthenticationStarted"]) => void,
) => eventEmitter.addListener("onAuthenticationStarted", callback),
onAuthenticationFailed: (
callback: (event: EventTypeMap["onAuthenticationFailed"]) => void,
) => eventEmitter.addListener("onAuthenticationFailed", callback),
onAuthenticationSuccess: (
callback: (event: EventTypeMap["onAuthenticationSuccess"]) => void,
) => eventEmitter.addListener("onAuthenticationSuccess", callback),
},
};

// Helper functions
const helpers = {
parseAssertionOptions: (
args: AssertionOptions<BufferSource>,
): GetCredentialOptions => {
return {
...args,
allowCredentials: args.allowCredentials?.items ?? [],
timeout: args.timeout ?? Constants.TIMEOUT,
userVerification: args.userVerification ?? Constants.USER_VERIFICATION,
};
},

parseAttestationOptions: (
args: AttestationOptions<BufferSource>,
): CreateCredentialOptions => {
return {
...args,
pubKeyCredParams: [Constants.PUB_KEY_CRED_PARAM],
timeout: args.timeout ?? Constants.TIMEOUT,
attestation: args.attestation ?? Constants.ATTESTATION,
excludeCredentials: args.excludeCredentials?.items ?? [],
authenticatorSelection: args.authenticatorSelection ?? {
authenticatorAttachment: Constants.AUTHENTICATOR_ATTACHMENT,
requireResidentKey: Constants.REQUIRE_RESIDENT_KEY,
residentKey: Constants.RESIDENT_KEY,
userVerification: Constants.USER_VERIFICATION,
},
};
},
_emitEvent: eventEmitter.emit.bind(eventEmitter),
};

const mainFunctions = {
async register(
this: typeof helpers,
args: AttestationOptions<BufferSource>,
) {
if (!navigator.credentials) {
throw new Error(
"Web Authentication API is not supported in this environment.",
);
}
try {
const createOptions = this.parseAttestationOptions(args);
this._emitEvent("onRegistrationStarted", createOptions);
const credential = await internalFunctions.register(createOptions);
this._emitEvent("onRegistrationComplete", credential);
return credential;
} catch (error: unknown) {
this._emitEvent("onRegistrationFailed", error);
throw error;
}
},

async authenticate(
this: typeof helpers,
args: AssertionOptions<BufferSource>,
) {
if (!navigator.credentials) {
throw new Error(
"Web Authentication API is not supported in this environment.",
);
}
try {
const getOptions = this.parseAssertionOptions(args);
this._emitEvent("onAuthenticationStarted", getOptions);
const credential = await internalFunctions.authenticate(getOptions);
this._emitEvent("onAuthenticationSuccess", credential);
return credential;
} catch (error: unknown) {
this._emitEvent("onAuthenticationFailed", error);
throw error;
}
},
};

const internalFunctions = {
register: async (
request: CreateCredentialOptions,
): Promise<PublicKeyCredential<
BufferSource,
AuthenticatorAttestationResponse<BufferSource>
> | null> =>
navigator.credentials.create({
publicKey: request,
}),

authenticate: async (
request: GetCredentialOptions,
): Promise<PublicKeyCredential<
BufferSource,
AuthenticatorAssertionResponse<BufferSource>
> | null> =>
navigator.credentials.get({
publicKey: request,
}),
};

return Object.assign(moduleObject, mainFunctions);
};

export default Cosmr1CredentialHandlerModule;
3 changes: 2 additions & 1 deletion packages/cred-web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
"type": "module",
"description": "Browser credential handler for passkey signatures in cosm-r1 lib",
"devDependencies": {
"@types/bun": "latest"
"@types/bun": "latest",
"@vaariance/shared-types": "*"
},
"peerDependencies": {
"typescript": "^5.0.0"
Expand Down
Loading

0 comments on commit 846080e

Please sign in to comment.