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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions apps/wallet/web/components/auth/LinkedAccounts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@ type ProviderInfo = {
description: string
}

const PROVIDER_INFO: Record<AuthProvider, ProviderInfo> = {
type SupportedAuthProvider = Exclude<AuthProvider, 'email' | 'phone'>

const PROVIDER_INFO: Record<SupportedAuthProvider, ProviderInfo> = {
[AuthProvider.WALLET]: {
name: 'Wallet',
icon: Wallet,
Expand Down Expand Up @@ -85,13 +87,13 @@ const PROVIDER_INFO: Record<AuthProvider, ProviderInfo> = {
}

type LinkedProvider = {
provider: AuthProvider
provider: SupportedAuthProvider
providerId: string
handle?: string
linkedAt: number
}

const SUPPORTED_PROVIDERS: AuthProvider[] = [
const SUPPORTED_PROVIDERS: SupportedAuthProvider[] = [
AuthProvider.WALLET,
AuthProvider.GOOGLE,
AuthProvider.APPLE,
Expand All @@ -102,7 +104,7 @@ const SUPPORTED_PROVIDERS: AuthProvider[] = [
AuthProvider.PASSKEY,
]

function toAuthProvider(type: string): AuthProvider | null {
function toAuthProvider(type: string): SupportedAuthProvider | null {
switch (type) {
case 'wallet':
return AuthProvider.WALLET
Expand Down
78 changes: 52 additions & 26 deletions packages/auth/src/credentials/verifiable-credentials.ts
Original file line number Diff line number Diff line change
Expand Up @@ -309,19 +309,28 @@ export class VerifiableCredentialIssuer {
}

private getCredentialTypeForProvider(provider: AuthProvider): string {
const typeMap: Record<AuthProvider, string> = {
wallet: 'WalletOwnershipCredential',
farcaster: 'FarcasterAccountCredential',
google: 'GoogleAccountCredential',
apple: 'AppleAccountCredential',
twitter: 'TwitterAccountCredential',
github: 'GitHubAccountCredential',
discord: 'DiscordAccountCredential',
email: 'EmailAccountCredential',
phone: 'PhoneAccountCredential',
}

return typeMap[provider] ?? 'OAuth3IdentityCredential'
switch (provider) {
case 'wallet':
return 'WalletOwnershipCredential'
case 'passkey':
return 'PasskeyAccountCredential'
case 'farcaster':
return 'FarcasterAccountCredential'
case 'google':
return 'GoogleAccountCredential'
case 'apple':
return 'AppleAccountCredential'
case 'twitter':
return 'TwitterAccountCredential'
case 'github':
return 'GitHubAccountCredential'
case 'discord':
return 'DiscordAccountCredential'
case 'email':
return 'EmailAccountCredential'
case 'phone':
return 'PhoneAccountCredential'
}
}

private createJWS(hash: Hex, challenge: string, domain?: string): string {
Expand Down Expand Up @@ -594,6 +603,35 @@ export class VerifiableCredentialVerifier {
}
}

export function getOnChainProviderId(provider: AuthProvider): number {
switch (provider) {
case 'wallet':
return 0
case 'farcaster':
return 1
case 'google':
return 2
case 'apple':
return 3
case 'twitter':
return 4
case 'github':
return 5
case 'discord':
return 6
case 'email':
return 7
case 'phone':
return 8
case 'passkey':
throw new Error('Passkey credentials are not yet supported on-chain')
default: {
const _exhaustive: never = provider
return _exhaustive
}
}
}

export function createCredentialHash(credential: VerifiableCredential): Hex {
const essential = {
type: credential.type,
Expand All @@ -613,20 +651,8 @@ export function credentialToOnChainAttestation(
issuedAt: number
expiresAt: number
} {
const providerMap: Record<AuthProvider, number> = {
wallet: 0,
farcaster: 1,
google: 2,
apple: 3,
twitter: 4,
github: 5,
discord: 6,
email: 7,
phone: 8,
}

return {
provider: providerMap[credential.credentialSubject.provider],
provider: getOnChainProviderId(credential.credentialSubject.provider),
providerId: keccak256(toBytes(credential.credentialSubject.providerId)),
credentialHash: createCredentialHash(credential),
issuedAt: Math.floor(new Date(credential.issuanceDate).getTime() / 1000),
Expand Down
51 changes: 43 additions & 8 deletions packages/auth/src/sdk/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ function generateUUID(): string {
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`
}

function arrayBufferToBase64url(buffer: ArrayBuffer): string {
function arrayBufferToBase64url(buffer: ArrayBuffer | SharedArrayBuffer): string {
const bytes = new Uint8Array(buffer)
let binary = ''
for (const byte of bytes) {
Expand All @@ -56,6 +56,28 @@ function isPasskeyRequestOptions(
return 'challenge' in options && !('user' in options)
}

function isAuthenticatorAssertionResponse(
response: AuthenticatorResponse,
): response is AuthenticatorAssertionResponse {
return 'authenticatorData' in response && 'signature' in response
}

function safeArrayBufferToBase64url(
value:
| ArrayBuffer
| SharedArrayBuffer
| ArrayBufferView
| null
| undefined,
): string | undefined {
if (!value) return undefined
const buffer =
value instanceof ArrayBuffer || value instanceof SharedArrayBuffer
? value
: value.buffer.slice(value.byteOffset, value.byteOffset + value.byteLength)
return arrayBufferToBase64url(buffer)
}

// OAuth callback data schema
const OAuthCallbackSchema = z.object({
code: z.string().optional(),
Expand Down Expand Up @@ -622,21 +644,34 @@ export class OAuth3Client {
if (!('attestationObject' in response)) {
throw new Error('Invalid passkey registration response')
}
const registrationResponse = response as AuthenticatorAttestationResponse & {
attestationObject: ArrayBuffer | SharedArrayBuffer
clientDataJSON: ArrayBuffer
}
responsePayload = {
clientDataJSON,
attestationObject: arrayBufferToBase64url(response.attestationObject),
attestationObject: safeArrayBufferToBase64url(
registrationResponse.attestationObject,
),
}
} else {
if (!('authenticatorData' in response) || !('signature' in response)) {
if (!isAuthenticatorAssertionResponse(response)) {
throw new Error('Invalid passkey authentication response')
}
const assertionResponse = response as AuthenticatorAssertionResponse & {
authenticatorData: ArrayBuffer | SharedArrayBuffer
signature: ArrayBuffer | SharedArrayBuffer
userHandle?: ArrayBuffer | SharedArrayBuffer | null
}
responsePayload = {
clientDataJSON,
authenticatorData: arrayBufferToBase64url(response.authenticatorData),
signature: arrayBufferToBase64url(response.signature),
userHandle: response.userHandle
? arrayBufferToBase64url(response.userHandle)
: undefined,
authenticatorData: safeArrayBufferToBase64url(
assertionResponse.authenticatorData,
),
signature: safeArrayBufferToBase64url(assertionResponse.signature),
userHandle: safeArrayBufferToBase64url(
assertionResponse.userHandle,
),
}
}

Expand Down
4 changes: 3 additions & 1 deletion packages/auth/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@
* threshold MPC signing, and W3C Verifiable Credentials.
*/

import type { JsonRecord, TEEAttestation } from '@jejunetwork/types'
import type { TEEAttestation } from '@jejunetwork/types'
import type { Address, Hex } from 'viem'

export type { JsonRecord } from '@jejunetwork/types'

export const AuthProvider = {
WALLET: 'wallet',
PASSKEY: 'passkey',
Expand Down
62 changes: 56 additions & 6 deletions packages/cli/src/commands/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,11 @@ async function authenticateWithDWS(
export const loginCommand = new Command('login')
.description('Authenticate with Jeju Network using your wallet')
.option('-n, --network <network>', 'Network to authenticate with', 'testnet')
.option(
'--address <address>',
'Wallet address to authenticate (required for --external mode)',
)
.option('--signature <signature>', 'Wallet signature from --external flow')
.option(
'-k, --private-key <key>',
'Private key (or use DEPLOYER_PRIVATE_KEY env)',
Expand Down Expand Up @@ -256,6 +261,15 @@ export const loginCommand = new Command('login')
}

if (options.external) {
if (!options.address) {
logger.error('External auth requires --address.')
logger.info(
'Example: jeju login --external --address 0xYourAddress --network localnet',
)
return
}

const address = options.address as Address
// External signing - output message for user to sign elsewhere
const nonce = bytesToHex(randomBytes(32))
const timestamp = Date.now()
Comment on lines 274 to 275

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Reuse the signed challenge for external login

The external flow rebuilds message with a fresh random nonce/timestamp before checking options.signature, so the second invocation cannot verify the signature from the first invocation's printed message. In the two-step flow (--external --address first, then --external --address --signature), verifyMessage will always fail because the signed payload changed between runs.

Useful? React with 👍 / 👎.

Expand All @@ -266,14 +280,50 @@ export const loginCommand = new Command('login')
timestamp,
)

logger.info('Sign the following message with your wallet:\n')
console.log('---')
console.log(message)
console.log('---\n')
if (!options.signature) {
logger.info('Sign the following message with your wallet:\n')
console.log('---')
console.log(message)
console.log('---\n')

logger.info('Then run:')
logger.info(
`jeju login --network ${network} --address ${address} --signature <your-signature>`,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Include --external in the follow-up login command

The suggested command omits --external, so users following this prompt run the non-external branch where --signature/--address are ignored and private-key auth is attempted instead. This makes the documented external workflow fail (or authenticate as a different local key if one is configured) even when the user provides a valid external signature.

Useful? React with 👍 / 👎.

)
return
}

// Complete external login with provided signature
const signature = options.signature
const isValid = await verifyMessage({ address, message, signature })
if (!isValid) {
logger.error('Signature verification failed')
return
}

const authResult = await authenticateWithDWS(
address,
signature,
message,
network,
)

const credentials: Credentials = {
version: 1,
network,
address,
keyType: 'external',
authToken: authResult.token,
createdAt: Date.now(),
expiresAt: authResult.expiresAt,
}

logger.info('Then run:')
saveCredentials(credentials)
logger.success(`Logged in as ${address}`)
logger.info(`Network: ${network}`)
logger.info(`Expires at: ${new Date(authResult.expiresAt).toLocaleDateString()}`)
logger.info(
`jeju login --network ${network} --signature <your-signature> --address <your-address>`,
'Use `jeju login` again if your token expires or you change wallets.',
)
return
}
Expand Down
1 change: 1 addition & 0 deletions packages/monitoring/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"version": "0.1.0",
"dependencies": {
"@elysiajs/cors": "^1.4.0",
"@jejunetwork/auth": "workspace:*",
"@jejunetwork/cache": "workspace:*",
"@jejunetwork/config": "workspace:*",
"@jejunetwork/types": "workspace:*",
Expand Down
7 changes: 6 additions & 1 deletion packages/sqlit/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,12 @@ export class SQLitClient {
return rows.filter(isRecordRow)
}

if (isRecordRow(rows[0])) {
const firstRow = rows[0]
if (firstRow === undefined) {
return []
}

if (isRecordRow(firstRow)) {
return rows.filter(isRecordRow)
}

Expand Down
Loading