feat: add EncryptID auth + consistent rApp header bar

- Add @encryptid/sdk (vendored) for WebAuthn passkey authentication
- Add zustand auth store matching rMaps/rWork pattern
- Create AuthButton component (sign in / register / sign out)
- Create HeaderBar client component: AppSwitcher + logo + nav + auth + cart
- Remove custom SpaceSwitcher (not part of standard rApp header)
- Fix hero: "Noticed" and "rMerch" now use same gradient color

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-02-24 22:33:04 -08:00
parent c31e440edf
commit 25482f9085
63 changed files with 10809 additions and 81 deletions

View File

@ -4,6 +4,7 @@ RUN apk add --no-cache libc6-compat
WORKDIR /app
COPY package.json pnpm-lock.yaml* ./
COPY vendor/ ./vendor/
RUN corepack enable pnpm && pnpm i --frozen-lockfile || npm install
# Stage 2: Builder

View File

@ -1,12 +1,10 @@
import type { Metadata } from "next";
import { GeistSans } from "geist/font";
import { cookies } from "next/headers";
import Link from "next/link";
import "./globals.css";
import type { SpaceConfig } from "@/lib/spaces";
import { themeToCSS } from "@/lib/spaces";
import { AppSwitcher } from "@/components/AppSwitcher";
import { SpaceSwitcher } from "@/components/SpaceSwitcher";
import { HeaderBar } from "@/components/HeaderBar";
const API_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000/api";
@ -80,77 +78,7 @@ export default async function RootLayout({
</head>
<body className={GeistSans.className}>
<div className="min-h-screen flex flex-col">
{/* ── Sticky Nav ──────────────────────────────────── */}
<header className="border-b sticky top-0 z-50 bg-background/90 backdrop-blur-sm">
<div className="max-w-6xl mx-auto px-4 py-2.5 flex items-center justify-between gap-3">
{/* Left: App switcher + Spaces + Logo */}
<div className="flex items-center gap-3">
<AppSwitcher current="swag" />
<SpaceSwitcher />
<div className="w-px h-5 bg-white/10 hidden sm:block" />
<Link
href="/"
className="flex items-center gap-2 font-bold text-lg"
>
{logoUrl ? (
<img src={logoUrl} alt="" className="h-7 w-7 rounded" />
) : (
<div className="h-7 w-7 bg-gradient-to-br from-cyan-300 to-amber-300 rounded-lg flex items-center justify-center text-slate-900 text-[10px] font-black leading-none">
rSw
</div>
)}
<span className="hidden sm:inline">
<span className="text-primary">r</span>
{name === "rSwag" ? "Swag" : name}
</span>
</Link>
</div>
{/* Center: Nav links */}
<nav className="flex items-center gap-1 sm:gap-2">
<Link
href="/design"
className="text-sm text-slate-300 hover:text-white transition-colors px-2.5 py-1.5 rounded-md hover:bg-white/[0.06] hidden sm:inline-flex"
>
Design
</Link>
<Link
href="/upload"
className="text-sm text-slate-300 hover:text-white transition-colors px-2.5 py-1.5 rounded-md hover:bg-white/[0.06]"
>
Upload
</Link>
<Link
href="/products"
className="text-sm px-4 py-1.5 bg-primary text-primary-foreground rounded-lg font-medium hover:bg-primary/90 transition-colors"
>
Shop
</Link>
</nav>
{/* Right: Cart */}
<div className="flex items-center gap-2">
<Link
href="/cart"
className="text-slate-300 hover:text-white transition-colors p-2 rounded-md hover:bg-white/[0.06]"
>
<svg
className="h-5 w-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M2.25 3h1.386c.51 0 .955.343 1.087.835l.383 1.437M7.5 14.25a3 3 0 00-3 3h15.75m-12.75-3h11.218c1.121-2.3 2.1-4.684 2.924-7.138a60.114 60.114 0 00-16.536-1.84M7.5 14.25L5.106 5.272M6 20.25a.75.75 0 11-1.5 0 .75.75 0 011.5 0zm12.75 0a.75.75 0 11-1.5 0 .75.75 0 011.5 0z"
/>
</svg>
</Link>
</div>
</div>
</header>
<HeaderBar name={name} logoUrl={logoUrl ?? null} />
{/* ── Main Content ────────────────────────────────── */}
<main className="flex-1">{children}</main>

View File

@ -81,13 +81,13 @@ export default async function HomePage() {
) : (
<>
Get Your Community{" "}
<span className="text-transparent bg-clip-text bg-gradient-to-r from-primary to-cyan-300">
<span className="text-transparent bg-clip-text bg-gradient-to-r from-primary to-secondary">
Noticed
</span>
<br />
with{" "}
<span className="text-white">(you)</span>
<span className="text-transparent bg-clip-text bg-gradient-to-r from-secondary to-amber-300">
<span className="text-transparent bg-clip-text bg-gradient-to-r from-primary to-secondary">
rMerch
</span>
</>

View File

@ -0,0 +1,103 @@
'use client';
import { useState } from 'react';
import { useAuthStore } from '@/stores/auth';
export function AuthButton() {
const { isAuthenticated, username, did, loading, login, register, logout } = useAuthStore();
const [showRegister, setShowRegister] = useState(false);
const [regUsername, setRegUsername] = useState('');
const [error, setError] = useState('');
if (isAuthenticated) {
return (
<div className="flex items-center gap-3">
<div className="text-sm">
<span className="text-white/60">Signed in as </span>
<span className="text-primary font-medium">{username || did?.slice(0, 16) + '...'}</span>
</div>
<button
onClick={logout}
className="text-xs text-white/40 hover:text-white/60 transition-colors"
>
Sign out
</button>
</div>
);
}
if (showRegister) {
return (
<div className="flex items-center gap-2">
<input
type="text"
value={regUsername}
onChange={(e) => setRegUsername(e.target.value)}
placeholder="Choose a username"
className="text-sm py-1 px-2 w-36 rounded-md bg-white/10 border border-white/10 text-white placeholder:text-white/30 focus:outline-none focus:ring-1 focus:ring-primary"
maxLength={20}
onKeyDown={(e) => {
if (e.key === 'Enter' && regUsername.trim()) {
setError('');
register(regUsername.trim()).catch((err: Error) => {
setError(err.message || 'Registration failed');
});
}
}}
/>
<button
onClick={async () => {
if (!regUsername.trim()) return;
setError('');
try {
await register(regUsername.trim());
} catch (e: unknown) {
setError(e instanceof Error ? e.message : 'Registration failed');
}
}}
disabled={loading || !regUsername.trim()}
className="text-sm py-1 px-3 bg-primary text-primary-foreground rounded-md font-medium hover:bg-primary/90 transition-colors disabled:opacity-50"
>
{loading ? '...' : 'Register'}
</button>
<button
onClick={() => setShowRegister(false)}
className="text-xs text-white/40 hover:text-white/60"
>
Cancel
</button>
{error && <span className="text-xs text-red-400">{error}</span>}
</div>
);
}
return (
<div className="flex items-center gap-2">
<button
onClick={async () => {
setError('');
try {
await login();
} catch (e: unknown) {
if (e instanceof DOMException && (e.name === 'NotAllowedError' || e.name === 'SecurityError' || e.name === 'AbortError')) {
setShowRegister(true);
} else {
setError(e instanceof Error ? e.message : 'Sign in failed');
}
}
}}
disabled={loading}
className="text-sm text-white/60 hover:text-primary transition-colors flex items-center gap-1.5"
>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} className="w-4 h-4">
<circle cx={12} cy={10} r={3} />
<path d="M12 13v8" />
<path d="M9 18h6" />
<circle cx={12} cy={10} r={7} />
</svg>
{loading ? 'Signing in...' : 'Sign in with Passkey'}
</button>
{error && <span className="text-xs text-red-400">{error}</span>}
</div>
);
}

View File

@ -0,0 +1,87 @@
'use client';
import Link from 'next/link';
import { AppSwitcher } from '@/components/AppSwitcher';
import { AuthButton } from '@/components/AuthButton';
interface HeaderBarProps {
name: string;
logoUrl: string | null;
}
export function HeaderBar({ name, logoUrl }: HeaderBarProps) {
return (
<header className="border-b sticky top-0 z-50 bg-background/90 backdrop-blur-sm">
<div className="max-w-6xl mx-auto px-4 py-2.5 flex items-center justify-between gap-3">
{/* Left: App switcher + Logo */}
<div className="flex items-center gap-3">
<AppSwitcher current="swag" />
<div className="w-px h-5 bg-white/10 hidden sm:block" />
<Link
href="/"
className="flex items-center gap-2 font-bold text-lg"
>
{logoUrl ? (
<img src={logoUrl} alt="" className="h-7 w-7 rounded" />
) : (
<div className="h-7 w-7 bg-gradient-to-br from-cyan-300 to-amber-300 rounded-lg flex items-center justify-center text-slate-900 text-[10px] font-black leading-none">
rSw
</div>
)}
<span className="hidden sm:inline">
<span className="text-primary">r</span>
{name === 'rSwag' ? 'Swag' : name}
</span>
</Link>
</div>
{/* Center: Nav links */}
<nav className="flex items-center gap-1 sm:gap-2">
<Link
href="/design"
className="text-sm text-slate-300 hover:text-white transition-colors px-2.5 py-1.5 rounded-md hover:bg-white/[0.06] hidden sm:inline-flex"
>
Design
</Link>
<Link
href="/upload"
className="text-sm text-slate-300 hover:text-white transition-colors px-2.5 py-1.5 rounded-md hover:bg-white/[0.06]"
>
Upload
</Link>
<Link
href="/products"
className="text-sm px-4 py-1.5 bg-primary text-primary-foreground rounded-lg font-medium hover:bg-primary/90 transition-colors"
>
Shop
</Link>
</nav>
{/* Right: Auth + Cart */}
<div className="flex items-center gap-4">
<div className="hidden sm:block">
<AuthButton />
</div>
<Link
href="/cart"
className="text-slate-300 hover:text-white transition-colors p-2 rounded-md hover:bg-white/[0.06]"
>
<svg
className="h-5 w-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M2.25 3h1.386c.51 0 .955.343 1.087.835l.383 1.437M7.5 14.25a3 3 0 00-3 3h15.75m-12.75-3h11.218c1.121-2.3 2.1-4.684 2.924-7.138a60.114 60.114 0 00-16.536-1.84M7.5 14.25L5.106 5.272M6 20.25a.75.75 0 11-1.5 0 .75.75 0 011.5 0zm12.75 0a.75.75 0 11-1.5 0 .75.75 0 011.5 0z"
/>
</svg>
</Link>
</div>
</div>
</header>
);
}

3322
frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -9,10 +9,7 @@
"lint": "next lint"
},
"dependencies": {
"geist": "^1.3.0",
"next": "^15.1.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"@encryptid/sdk": "file:./vendor/@encryptid/sdk",
"@radix-ui/react-dialog": "^1.1.4",
"@radix-ui/react-dropdown-menu": "^2.1.4",
"@radix-ui/react-label": "^2.1.1",
@ -24,9 +21,14 @@
"@radix-ui/react-toast": "^1.2.4",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"geist": "^1.3.0",
"lucide-react": "^0.469.0",
"next": "^15.1.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"tailwind-merge": "^2.6.0",
"tailwindcss-animate": "^1.0.7"
"tailwindcss-animate": "^1.0.7",
"zustand": "^5.0.11"
},
"devDependencies": {
"@types/node": "^22.10.0",

93
frontend/stores/auth.ts Normal file
View File

@ -0,0 +1,93 @@
/**
* EncryptID Auth Store for rSwag
*
* Optional authentication via WebAuthn passkeys.
* Zustand with localStorage persistence, delegates WebAuthn ceremony to @encryptid/sdk.
*/
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import { EncryptIDClient } from '@encryptid/sdk/client';
const ENCRYPTID_SERVER = process.env.NEXT_PUBLIC_ENCRYPTID_SERVER_URL || 'https://auth.ridentity.online';
const client = new EncryptIDClient(ENCRYPTID_SERVER);
interface AuthState {
isAuthenticated: boolean;
token: string | null;
did: string | null;
username: string | null;
loading: boolean;
login: () => Promise<void>;
register: (username: string) => Promise<void>;
logout: () => void;
}
export const useAuthStore = create<AuthState>()(
persist(
(set) => ({
isAuthenticated: false,
token: null,
did: null,
username: null,
loading: false,
login: async () => {
set({ loading: true });
try {
const result = await client.authenticate();
document.cookie = `encryptid_token=${result.token};path=/;max-age=900;SameSite=Lax`;
set({
isAuthenticated: true,
token: result.token,
did: result.did,
username: result.username,
loading: false,
});
} catch (error) {
set({ loading: false });
throw error;
}
},
register: async (username: string) => {
set({ loading: true });
try {
const result = await client.register(username);
document.cookie = `encryptid_token=${result.token};path=/;max-age=900;SameSite=Lax`;
set({
isAuthenticated: true,
token: result.token,
did: result.did,
username,
loading: false,
});
} catch (error) {
set({ loading: false });
throw error;
}
},
logout: () => {
document.cookie = 'encryptid_token=;path=/;max-age=0;SameSite=Lax';
set({
isAuthenticated: false,
token: null,
did: null,
username: null,
loading: false,
});
},
}),
{
name: 'rswag-auth',
partialize: (state) => ({
isAuthenticated: state.isAuthenticated,
token: state.token,
did: state.did,
username: state.username,
}),
}
)
);

View File

@ -0,0 +1,86 @@
/**
* EncryptID Browser Bundle
*
* IIFE entry point that provides window.EncryptID for vanilla JS apps.
* Build: bun build ./src/browser.ts --outfile dist/encryptid.browser.js --target browser
*
* Usage:
* <script src="encryptid.browser.js"></script>
* <script>
* EncryptID.renderAuthButton('auth-container');
* // or
* const result = await EncryptID.authenticate();
* </script>
*/
import { EncryptIDClient } from './client/api-client.js';
import { detectCapabilities } from './client/webauthn.js';
import { AuthLevel } from './client/session.js';
import './ui/login-button.js';
import './ui/guardian-setup.js';
interface StoredUser {
did: string;
username: string;
token: string;
}
/**
* Authenticate with an existing passkey
*/
declare function authenticate(): Promise<StoredUser>;
/**
* Register a new passkey
*/
declare function register(username: string, displayName?: string): Promise<StoredUser>;
/**
* Log out clear stored auth state
*/
declare function logout(): void;
/**
* Check if user is currently authenticated
*/
declare function isAuthenticated(): boolean;
/**
* Get stored user info
*/
declare function getUser(): StoredUser | null;
/**
* Get the stored token
*/
declare function getToken(): string | null;
/**
* Require authentication redirects to home with login hint if not authenticated
*/
declare function requireAuth(redirectUrl?: string): boolean;
/**
* Set a recovery email for the authenticated user
*/
declare function setRecoveryEmail(email: string): Promise<void>;
/**
* Request account recovery via email
*/
declare function requestRecovery(email: string): Promise<void>;
/**
* Render an auth button into a container element
*/
declare function renderAuthButton(containerId: string): void;
/**
* Verify the stored token is still valid, refresh if needed
*/
declare function verifySession(): Promise<boolean>;
declare const EncryptID: {
client: EncryptIDClient;
authenticate: typeof authenticate;
register: typeof register;
logout: typeof logout;
isAuthenticated: typeof isAuthenticated;
getUser: typeof getUser;
getToken: typeof getToken;
requireAuth: typeof requireAuth;
setRecoveryEmail: typeof setRecoveryEmail;
requestRecovery: typeof requestRecovery;
renderAuthButton: typeof renderAuthButton;
verifySession: typeof verifySession;
detectCapabilities: typeof detectCapabilities;
AuthLevel: typeof AuthLevel;
VERSION: string;
};
export default EncryptID;

View File

@ -0,0 +1,69 @@
/**
* EncryptID API Client
*
* HTTP client for communicating with the EncryptID server.
* Handles registration, authentication, session management.
*/
import type { RegistrationStartResponse, RegistrationCompleteResponse, AuthStartResponse, AuthCompleteResponse, SessionVerifyResponse, EmailRecoverySetResponse, EmailRecoveryRequestResponse, EmailRecoveryVerifyResponse } from '../types/index.js';
export declare class EncryptIDClient {
private serverUrl;
constructor(serverUrl?: string);
/**
* Start registration get challenge and options from server
*/
registerStart(username: string, displayName?: string): Promise<RegistrationStartResponse>;
/**
* Complete registration send credential to server
*/
registerComplete(challenge: string, credential: PublicKeyCredential, userId: string, username: string): Promise<RegistrationCompleteResponse>;
/**
* Start authentication get challenge from server
*/
authStart(credentialId?: string): Promise<AuthStartResponse>;
/**
* Complete authentication send assertion to server
*/
authComplete(challenge: string, credential: PublicKeyCredential): Promise<AuthCompleteResponse>;
/**
* Verify a session token
*/
verifySession(token: string): Promise<SessionVerifyResponse>;
/**
* Refresh a session token
*/
refreshToken(token: string): Promise<{
token: string;
}>;
/**
* List user's credentials
*/
listCredentials(token: string): Promise<{
credentials: any[];
}>;
/**
* Set recovery email for the authenticated user
*/
setRecoveryEmail(token: string, email: string): Promise<EmailRecoverySetResponse>;
/**
* Request account recovery via email
*/
requestEmailRecovery(email: string): Promise<EmailRecoveryRequestResponse>;
/**
* Verify a recovery token and get a temporary session
*/
verifyRecoveryToken(recoveryToken: string): Promise<EmailRecoveryVerifyResponse>;
/**
* Full registration flow: server challenge WebAuthn create server verify
*/
register(username: string, displayName?: string, config?: {
rpId?: string;
}): Promise<RegistrationCompleteResponse>;
/**
* Full authentication flow: server challenge WebAuthn get server verify
*/
authenticate(credentialId?: string, config?: {
rpId?: string;
}): Promise<AuthCompleteResponse & {
prfOutput?: ArrayBuffer;
}>;
}

View File

@ -0,0 +1,13 @@
/**
* @encryptid/sdk/client Browser-safe client module
*/
export { registerPasskey, authenticatePasskey, startConditionalUI, abortConditionalUI, isConditionalMediationAvailable, detectCapabilities, bufferToBase64url, base64urlToBuffer, generateChallenge, } from './webauthn.js';
export type { EncryptIDCredential, AuthenticationResult, EncryptIDConfig, WebAuthnCapabilities } from './webauthn.js';
export { EncryptIDKeyManager, getKeyManager, resetKeyManager, encryptData, decryptData, decryptDataAsString, signData, verifySignature, wrapKeyForRecipient, unwrapSharedKey, } from './key-derivation.js';
export type { DerivedKeys, EncryptedData, SignedData } from './key-derivation.js';
export { SessionManager, getSessionManager, AuthLevel, OPERATION_PERMISSIONS, } from './session.js';
export type { EncryptIDClaims, SessionState, OperationPermission } from './session.js';
export { RecoveryManager, getRecoveryManager, GuardianType, getGuardianTypeInfo, } from './recovery.js';
export type { Guardian, RecoveryConfig, RecoveryRequest } from './recovery.js';
export { EncryptIDClient } from './api-client.js';
export { shareTokenAcrossModules, clearTokenAcrossModules, initTokenRelayListener, getStoredToken, requestTokenFromDomain, MODULE_DOMAINS, } from './token-relay.js';

View File

@ -0,0 +1,66 @@
import {
EncryptIDKeyManager,
OPERATION_PERMISSIONS,
RecoveryManager,
SessionManager,
decryptData,
decryptDataAsString,
encryptData,
getGuardianTypeInfo,
getKeyManager,
getRecoveryManager,
getSessionManager,
resetKeyManager,
signData,
unwrapSharedKey,
verifySignature,
wrapKeyForRecipient
} from "../index-24r9wkfe.js";
import {
EncryptIDClient
} from "../index-7egxprg9.js";
import {
abortConditionalUI,
authenticatePasskey,
base64urlToBuffer,
bufferToBase64url,
detectCapabilities,
generateChallenge,
isConditionalMediationAvailable,
registerPasskey,
startConditionalUI
} from "../index-2cp5044h.js";
import {
AuthLevel,
GuardianType
} from "../index-5c1t4ftn.js";
export {
wrapKeyForRecipient,
verifySignature,
unwrapSharedKey,
startConditionalUI,
signData,
resetKeyManager,
registerPasskey,
isConditionalMediationAvailable,
getSessionManager,
getRecoveryManager,
getKeyManager,
getGuardianTypeInfo,
generateChallenge,
encryptData,
detectCapabilities,
decryptDataAsString,
decryptData,
bufferToBase64url,
base64urlToBuffer,
authenticatePasskey,
abortConditionalUI,
SessionManager,
RecoveryManager,
OPERATION_PERMISSIONS,
GuardianType,
EncryptIDKeyManager,
EncryptIDClient,
AuthLevel
};

View File

@ -0,0 +1,51 @@
/**
* EncryptID Key Derivation Module
*
* Derives application-specific cryptographic keys from WebAuthn PRF output
* or passphrase fallback. Layer 2 of the EncryptID architecture.
*/
import type { DerivedKeys, EncryptedData, SignedData } from '../types/index.js';
export type { DerivedKeys, EncryptedData, SignedData };
export declare class EncryptIDKeyManager {
private masterKey;
private derivedKeys;
private fromPRF;
initFromPRF(prfOutput: ArrayBuffer): Promise<void>;
initFromPassphrase(passphrase: string, salt: Uint8Array): Promise<void>;
static generateSalt(): Uint8Array;
isInitialized(): boolean;
getKeys(): Promise<DerivedKeys>;
private deriveEncryptionKey;
private deriveSigningKeyPair;
/**
* Derive deterministic secp256k1 keys from the master key via HKDF.
* This gives every EncryptID identity an Ethereum-compatible wallet address,
* enabling them to act as Gnosis Safe owners for multi-sig approvals.
*/
private deriveEthereumKeys;
private deriveDIDSeed;
private generateDID;
clear(): void;
}
export declare function encryptData(key: CryptoKey, data: ArrayBuffer | Uint8Array | string): Promise<EncryptedData>;
export declare function decryptData(key: CryptoKey, encrypted: EncryptedData): Promise<ArrayBuffer>;
export declare function decryptDataAsString(key: CryptoKey, encrypted: EncryptedData): Promise<string>;
export declare function signData(keyPair: CryptoKeyPair, data: ArrayBuffer | Uint8Array | string): Promise<SignedData>;
export declare function verifySignature(signed: SignedData): Promise<boolean>;
export declare function wrapKeyForRecipient(keyToWrap: CryptoKey, recipientPublicKey: CryptoKey): Promise<ArrayBuffer>;
export declare function unwrapSharedKey(wrappedKey: ArrayBuffer, privateKey: CryptoKey): Promise<CryptoKey>;
/**
* Sign an Ethereum-compatible message hash with a secp256k1 private key.
* Returns { r, s, v } components for Safe transaction signing.
*
* @param hash - 32-byte message hash (e.g. keccak256 of the message)
* @param privateKey - 32-byte secp256k1 private key
*/
export declare function signEthHash(hash: Uint8Array, privateKey: Uint8Array): {
r: string;
s: string;
v: number;
signature: Uint8Array;
};
export declare function getKeyManager(): EncryptIDKeyManager;
export declare function resetKeyManager(): void;

View File

@ -0,0 +1,37 @@
/**
* EncryptID Social Recovery Module
*
* Guardian-based account recovery with NO SEED PHRASES.
*/
import type { Guardian, RecoveryConfig, RecoveryRequest } from '../types/index.js';
import { GuardianType } from '../types/index.js';
export { GuardianType };
export type { Guardian, RecoveryConfig, RecoveryRequest };
export declare class RecoveryManager {
private config;
private activeRequest;
constructor();
initializeRecovery(threshold?: number): Promise<RecoveryConfig>;
addGuardian(guardian: Omit<Guardian, 'id' | 'addedAt'>): Promise<Guardian>;
removeGuardian(guardianId: string): Promise<void>;
setThreshold(threshold: number): Promise<void>;
setDelay(delaySeconds: number): Promise<void>;
getConfig(): RecoveryConfig | null;
isConfigured(): boolean;
verifyGuardian(guardianId: string): Promise<boolean>;
initiateRecovery(newCredentialId: string): Promise<RecoveryRequest>;
approveRecovery(guardianId: string, signature: string): Promise<RecoveryRequest>;
cancelRecovery(): Promise<void>;
completeRecovery(): Promise<void>;
getActiveRequest(): RecoveryRequest | null;
private hashGuardianList;
private saveConfig;
private loadConfig;
}
export declare function getRecoveryManager(): RecoveryManager;
export declare function getGuardianTypeInfo(type: GuardianType): {
name: string;
description: string;
icon: string;
setupInstructions: string;
};

View File

@ -0,0 +1,35 @@
/**
* EncryptID Session Management
*
* Handles session tokens, cross-app SSO, and authentication levels.
*/
import type { AuthenticationResult, EncryptIDClaims, SessionState, OperationPermission } from '../types/index.js';
import { AuthLevel } from '../types/index.js';
export { AuthLevel };
export type { EncryptIDClaims, SessionState, OperationPermission };
export declare const OPERATION_PERMISSIONS: Record<string, OperationPermission>;
export declare class SessionManager {
private session;
private refreshTimer;
constructor();
createSession(authResult: AuthenticationResult, did: string, capabilities: EncryptIDClaims['eid']['capabilities'], walletAddress?: string, username?: string): Promise<SessionState>;
getSession(): SessionState | null;
getDID(): string | null;
getAccessToken(): string | null;
getAuthLevel(): AuthLevel;
canPerform(operation: string): {
allowed: boolean;
reason?: string;
};
requiresFreshAuth(operation: string): boolean;
upgradeAuthLevel(level?: AuthLevel): void;
clearSession(): void;
isValid(): boolean;
private createUnsignedToken;
private createRefreshToken;
private persistSession;
private restoreSession;
private scheduleRefresh;
private refreshTokens;
}
export declare function getSessionManager(): SessionManager;

View File

@ -0,0 +1,58 @@
/**
* EncryptID Token Relay Cross-Domain Authentication
*
* Since .online is a public suffix, cookies can't be shared across
* r*.online domains. This module uses postMessage via hidden iframes
* to relay the JWT token to sibling modules after authentication.
*
* Usage (on the authenticating domain, e.g., rspace.online):
*
* import { shareTokenAcrossModules, MODULE_DOMAINS } from '@encryptid/sdk/client/token-relay';
* await shareTokenAcrossModules(jwt, MODULE_DOMAINS);
*
* Usage (on each module, add a relay page at /auth/relay):
*
* import { initTokenRelayListener } from '@encryptid/sdk/client/token-relay';
* initTokenRelayListener(); // Listens for postMessage, stores token
*/
/** All r*.online module domains for token relay */
export declare const MODULE_DOMAINS: readonly ["rvote.online", "rnotes.online", "rmaps.online", "rcal.online", "rfunds.online", "rtube.online", "rfiles.online", "rmail.online", "rtrips.online", "rnetwork.online", "rwallet.online", "rstack.online", "rspace.online"];
interface RelayResult {
domain: string;
success: boolean;
error?: string;
}
/**
* Share an EncryptID JWT token across all r*.online module domains.
* Creates hidden iframes pointing to each module's /auth/relay page,
* then sends the token via postMessage.
*
* @param token - The JWT token to share
* @param domains - Array of domains to relay to (defaults to MODULE_DOMAINS)
* @param timeout - Timeout per domain in ms (default 5000)
* @returns Results for each domain
*/
export declare function shareTokenAcrossModules(token: string, domains?: readonly string[], timeout?: number): Promise<RelayResult[]>;
/**
* Clear the token from all module domains.
* Call this on sign-out.
*/
export declare function clearTokenAcrossModules(domains?: readonly string[], timeout?: number): Promise<void>;
/**
* Initialize the token relay listener on a module's /auth/relay page.
* Listens for postMessage from sibling r*.online domains and stores
* the token in localStorage.
*
* This should be called on a minimal page served at /auth/relay on each module.
*/
export declare function initTokenRelayListener(): void;
/**
* Get the locally stored EncryptID token, if any.
*/
export declare function getStoredToken(): string | null;
/**
* Request the token from a sibling domain if we don't have it locally.
* Creates a hidden iframe to the specified domain and asks for the token.
*/
export declare function requestTokenFromDomain(domain: string, timeout?: number): Promise<string | null>;
export {};

View File

@ -0,0 +1,21 @@
/**
* EncryptID WebAuthn Module
*
* Handles passkey registration, authentication, and PRF extension
* for key derivation. This is the foundation layer of EncryptID.
*/
import type { EncryptIDCredential, AuthenticationResult, EncryptIDConfig, WebAuthnCapabilities } from '../types/index.js';
export type { EncryptIDCredential, AuthenticationResult, EncryptIDConfig, WebAuthnCapabilities };
/**
* Abort any pending conditional UI request
*/
export declare function abortConditionalUI(): void;
export declare function bufferToBase64url(buffer: ArrayBuffer): string;
export declare function base64urlToBuffer(base64url: string): ArrayBuffer;
export declare function generateChallenge(): ArrayBuffer;
export declare function generatePRFSalt(purpose: string): Promise<ArrayBuffer>;
export declare function registerPasskey(username: string, displayName: string, config?: Partial<EncryptIDConfig>): Promise<EncryptIDCredential>;
export declare function authenticatePasskey(credentialId?: string, config?: Partial<EncryptIDConfig>): Promise<AuthenticationResult>;
export declare function isConditionalMediationAvailable(): Promise<boolean>;
export declare function startConditionalUI(config?: Partial<EncryptIDConfig>): Promise<AuthenticationResult | null>;
export declare function detectCapabilities(): Promise<WebAuthnCapabilities>;

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,529 @@
import {
bufferToBase64url
} from "./index-2cp5044h.js";
import {
AuthLevel
} from "./index-5c1t4ftn.js";
// src/client/key-derivation.ts
class EncryptIDKeyManager {
masterKey = null;
derivedKeys = null;
fromPRF = false;
async initFromPRF(prfOutput) {
this.masterKey = await crypto.subtle.importKey("raw", prfOutput, { name: "HKDF" }, false, ["deriveKey", "deriveBits"]);
this.fromPRF = true;
this.derivedKeys = null;
}
async initFromPassphrase(passphrase, salt) {
const encoder = new TextEncoder;
const passphraseKey = await crypto.subtle.importKey("raw", encoder.encode(passphrase), { name: "PBKDF2" }, false, ["deriveBits"]);
const masterKeyMaterial = await crypto.subtle.deriveBits({ name: "PBKDF2", salt, iterations: 600000, hash: "SHA-256" }, passphraseKey, 256);
this.masterKey = await crypto.subtle.importKey("raw", masterKeyMaterial, { name: "HKDF" }, false, ["deriveKey", "deriveBits"]);
this.fromPRF = false;
this.derivedKeys = null;
}
static generateSalt() {
return crypto.getRandomValues(new Uint8Array(32));
}
isInitialized() {
return this.masterKey !== null;
}
async getKeys() {
if (!this.masterKey)
throw new Error("Key manager not initialized");
if (this.derivedKeys)
return this.derivedKeys;
const [encryptionKey, signingKeyPair, didSeed] = await Promise.all([
this.deriveEncryptionKey(),
this.deriveSigningKeyPair(),
this.deriveDIDSeed()
]);
const did = await this.generateDID(didSeed);
this.derivedKeys = { encryptionKey, signingKeyPair, didSeed, did, fromPRF: this.fromPRF };
return this.derivedKeys;
}
async deriveEncryptionKey() {
const encoder = new TextEncoder;
return crypto.subtle.deriveKey({ name: "HKDF", hash: "SHA-256", salt: encoder.encode("encryptid-encryption-key-v1"), info: encoder.encode("AES-256-GCM") }, this.masterKey, { name: "AES-GCM", length: 256 }, false, ["encrypt", "decrypt", "wrapKey", "unwrapKey"]);
}
async deriveSigningKeyPair() {
return crypto.subtle.generateKey({ name: "ECDSA", namedCurve: "P-256" }, false, ["sign", "verify"]);
}
async deriveDIDSeed() {
const encoder = new TextEncoder;
const seed = await crypto.subtle.deriveBits({ name: "HKDF", hash: "SHA-256", salt: encoder.encode("encryptid-did-key-v1"), info: encoder.encode("Ed25519-seed") }, this.masterKey, 256);
return new Uint8Array(seed);
}
async generateDID(seed) {
const publicKeyHash = await crypto.subtle.digest("SHA-256", seed);
const publicKeyBytes = new Uint8Array(publicKeyHash).slice(0, 32);
const multicodecPrefix = new Uint8Array([237, 1]);
const multicodecKey = new Uint8Array(34);
multicodecKey.set(multicodecPrefix);
multicodecKey.set(publicKeyBytes, 2);
const base58Encoded = bufferToBase64url(multicodecKey.buffer).replace(/-/g, "").replace(/_/g, "");
return `did:key:z${base58Encoded}`;
}
clear() {
this.masterKey = null;
this.derivedKeys = null;
this.fromPRF = false;
}
}
async function encryptData(key, data) {
let plaintext;
if (typeof data === "string")
plaintext = new TextEncoder().encode(data).buffer;
else if (data instanceof Uint8Array)
plaintext = data.buffer;
else
plaintext = data;
const iv = crypto.getRandomValues(new Uint8Array(12));
const ciphertext = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, plaintext);
return { ciphertext, iv };
}
async function decryptData(key, encrypted) {
return crypto.subtle.decrypt({ name: "AES-GCM", iv: encrypted.iv }, key, encrypted.ciphertext);
}
async function decryptDataAsString(key, encrypted) {
return new TextDecoder().decode(await decryptData(key, encrypted));
}
async function signData(keyPair, data) {
let dataBuffer;
if (typeof data === "string")
dataBuffer = new TextEncoder().encode(data).buffer;
else if (data instanceof Uint8Array)
dataBuffer = data.buffer;
else
dataBuffer = data;
const signature = await crypto.subtle.sign({ name: "ECDSA", hash: "SHA-256" }, keyPair.privateKey, dataBuffer);
const publicKey = await crypto.subtle.exportKey("raw", keyPair.publicKey);
return { data: dataBuffer, signature, publicKey };
}
async function verifySignature(signed) {
const publicKey = await crypto.subtle.importKey("raw", signed.publicKey, { name: "ECDSA", namedCurve: "P-256" }, false, ["verify"]);
return crypto.subtle.verify({ name: "ECDSA", hash: "SHA-256" }, publicKey, signed.signature, signed.data);
}
async function wrapKeyForRecipient(keyToWrap, recipientPublicKey) {
return crypto.subtle.wrapKey("raw", keyToWrap, recipientPublicKey, { name: "RSA-OAEP" });
}
async function unwrapSharedKey(wrappedKey, privateKey) {
return crypto.subtle.unwrapKey("raw", wrappedKey, privateKey, { name: "RSA-OAEP" }, { name: "AES-GCM", length: 256 }, false, ["encrypt", "decrypt"]);
}
var keyManagerInstance = null;
function getKeyManager() {
if (!keyManagerInstance)
keyManagerInstance = new EncryptIDKeyManager;
return keyManagerInstance;
}
function resetKeyManager() {
if (keyManagerInstance) {
keyManagerInstance.clear();
keyManagerInstance = null;
}
}
// src/client/session.ts
var OPERATION_PERMISSIONS = {
"rspace:view-public": { minAuthLevel: 1 /* BASIC */ },
"rspace:view-private": { minAuthLevel: 2 /* STANDARD */ },
"rspace:edit-board": { minAuthLevel: 2 /* STANDARD */ },
"rspace:create-board": { minAuthLevel: 2 /* STANDARD */ },
"rspace:delete-board": { minAuthLevel: 3 /* ELEVATED */, maxAgeSeconds: 300 },
"rspace:encrypt-board": { minAuthLevel: 3 /* ELEVATED */, requiresCapability: "encrypt" },
"rwallet:view-balance": { minAuthLevel: 1 /* BASIC */ },
"rwallet:view-history": { minAuthLevel: 2 /* STANDARD */ },
"rwallet:send-small": { minAuthLevel: 2 /* STANDARD */, requiresCapability: "wallet" },
"rwallet:send-large": { minAuthLevel: 3 /* ELEVATED */, requiresCapability: "wallet", maxAgeSeconds: 60 },
"rwallet:add-guardian": { minAuthLevel: 4 /* CRITICAL */, maxAgeSeconds: 60 },
"rwallet:remove-guardian": { minAuthLevel: 4 /* CRITICAL */, maxAgeSeconds: 60 },
"rvote:view-proposals": { minAuthLevel: 1 /* BASIC */ },
"rvote:cast-vote": { minAuthLevel: 3 /* ELEVATED */, requiresCapability: "sign", maxAgeSeconds: 300 },
"rvote:delegate": { minAuthLevel: 3 /* ELEVATED */, requiresCapability: "wallet" },
"rfiles:list-files": { minAuthLevel: 2 /* STANDARD */ },
"rfiles:download-own": { minAuthLevel: 2 /* STANDARD */, requiresCapability: "encrypt" },
"rfiles:upload": { minAuthLevel: 2 /* STANDARD */, requiresCapability: "encrypt" },
"rfiles:share": { minAuthLevel: 3 /* ELEVATED */, requiresCapability: "encrypt" },
"rfiles:delete": { minAuthLevel: 3 /* ELEVATED */, maxAgeSeconds: 300 },
"rfiles:export-keys": { minAuthLevel: 4 /* CRITICAL */, maxAgeSeconds: 60 },
"rmaps:view-public": { minAuthLevel: 1 /* BASIC */ },
"rmaps:add-location": { minAuthLevel: 2 /* STANDARD */ },
"rmaps:edit-location": { minAuthLevel: 2 /* STANDARD */, requiresCapability: "sign" },
"account:view-profile": { minAuthLevel: 2 /* STANDARD */ },
"account:edit-profile": { minAuthLevel: 3 /* ELEVATED */ },
"account:export-data": { minAuthLevel: 4 /* CRITICAL */, maxAgeSeconds: 60 },
"account:delete": { minAuthLevel: 4 /* CRITICAL */, maxAgeSeconds: 60 },
"rspace:create-space": { minAuthLevel: 2 /* STANDARD */ },
"rspace:configure-space": { minAuthLevel: 3 /* ELEVATED */, maxAgeSeconds: 300 },
"rspace:delete-space": { minAuthLevel: 4 /* CRITICAL */, maxAgeSeconds: 60 },
"rspace:invite-member": { minAuthLevel: 2 /* STANDARD */ },
"rspace:remove-member": { minAuthLevel: 3 /* ELEVATED */, maxAgeSeconds: 300 },
"rspace:change-visibility": { minAuthLevel: 3 /* ELEVATED */, maxAgeSeconds: 300 },
"rfunds:create-space": { minAuthLevel: 2 /* STANDARD */ },
"rfunds:edit-flows": { minAuthLevel: 2 /* STANDARD */ },
"rfunds:share-space": { minAuthLevel: 2 /* STANDARD */ }
};
var SESSION_STORAGE_KEY = "encryptid_session";
var TOKEN_REFRESH_THRESHOLD = 5 * 60 * 1000;
class SessionManager {
session = null;
refreshTimer = null;
constructor() {
this.restoreSession();
}
async createSession(authResult, did, capabilities) {
const now = Math.floor(Date.now() / 1000);
const claims = {
iss: "https://encryptid.jeffemmett.com",
sub: did,
aud: ["rspace.online", "rwallet.online", "rvote.online", "rfiles.online", "rmaps.online"],
iat: now,
exp: now + 15 * 60,
jti: bufferToBase64url(crypto.getRandomValues(new Uint8Array(16)).buffer),
eid: {
credentialId: authResult.credentialId,
authLevel: 3 /* ELEVATED */,
authTime: now,
capabilities,
recoveryConfigured: false
}
};
const accessToken = this.createUnsignedToken(claims);
const refreshToken = this.createRefreshToken(did);
this.session = { accessToken, refreshToken, claims, lastAuthTime: Date.now() };
this.persistSession();
this.scheduleRefresh();
return this.session;
}
getSession() {
return this.session;
}
getDID() {
return this.session?.claims.sub ?? null;
}
getAccessToken() {
return this.session?.accessToken ?? null;
}
getAuthLevel() {
if (!this.session)
return 1 /* BASIC */;
const now = Math.floor(Date.now() / 1000);
if (now >= this.session.claims.exp)
return 1 /* BASIC */;
const authAge = now - this.session.claims.eid.authTime;
if (authAge < 60)
return 3 /* ELEVATED */;
if (authAge < 15 * 60)
return 2 /* STANDARD */;
return 1 /* BASIC */;
}
canPerform(operation) {
const permission = OPERATION_PERMISSIONS[operation];
if (!permission)
return { allowed: false, reason: "Unknown operation" };
if (!this.session)
return { allowed: false, reason: "Not authenticated" };
const currentLevel = this.getAuthLevel();
if (currentLevel < permission.minAuthLevel) {
return { allowed: false, reason: `Requires ${AuthLevel[permission.minAuthLevel]} auth level (current: ${AuthLevel[currentLevel]})` };
}
if (permission.requiresCapability) {
if (!this.session.claims.eid.capabilities[permission.requiresCapability]) {
return { allowed: false, reason: `Requires ${permission.requiresCapability} capability` };
}
}
if (permission.maxAgeSeconds) {
const authAge = Math.floor(Date.now() / 1000) - this.session.claims.eid.authTime;
if (authAge > permission.maxAgeSeconds) {
return { allowed: false, reason: `Authentication too old (${authAge}s > ${permission.maxAgeSeconds}s)` };
}
}
return { allowed: true };
}
requiresFreshAuth(operation) {
const permission = OPERATION_PERMISSIONS[operation];
if (!permission)
return true;
if (permission.minAuthLevel >= 4 /* CRITICAL */)
return true;
if (permission.maxAgeSeconds && permission.maxAgeSeconds <= 60)
return true;
return false;
}
upgradeAuthLevel(level = 3 /* ELEVATED */) {
if (!this.session)
return;
this.session.claims.eid.authLevel = level;
this.session.claims.eid.authTime = Math.floor(Date.now() / 1000);
this.session.lastAuthTime = Date.now();
this.persistSession();
}
clearSession() {
this.session = null;
if (this.refreshTimer) {
clearTimeout(this.refreshTimer);
this.refreshTimer = null;
}
try {
localStorage.removeItem(SESSION_STORAGE_KEY);
} catch {}
}
isValid() {
if (!this.session)
return false;
return Math.floor(Date.now() / 1000) < this.session.claims.exp;
}
createUnsignedToken(claims) {
const header = { alg: "none", typ: "JWT" };
return `${btoa(JSON.stringify(header))}.${btoa(JSON.stringify(claims))}.`;
}
createRefreshToken(did) {
return btoa(JSON.stringify({
sub: did,
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + 7 * 24 * 60 * 60,
jti: bufferToBase64url(crypto.getRandomValues(new Uint8Array(16)).buffer)
}));
}
persistSession() {
if (!this.session)
return;
try {
localStorage.setItem(SESSION_STORAGE_KEY, JSON.stringify(this.session));
} catch {}
}
restoreSession() {
try {
const stored = localStorage.getItem(SESSION_STORAGE_KEY);
if (stored) {
const session = JSON.parse(stored);
if (Math.floor(Date.now() / 1000) < session.claims.exp) {
this.session = session;
this.scheduleRefresh();
} else {
localStorage.removeItem(SESSION_STORAGE_KEY);
}
}
} catch {}
}
scheduleRefresh() {
if (!this.session)
return;
if (this.refreshTimer)
clearTimeout(this.refreshTimer);
const expiresAt = this.session.claims.exp * 1000;
const refreshAt = expiresAt - TOKEN_REFRESH_THRESHOLD;
const delay = Math.max(refreshAt - Date.now(), 0);
this.refreshTimer = setTimeout(() => this.refreshTokens(), delay);
}
async refreshTokens() {
if (!this.session)
return;
const now = Math.floor(Date.now() / 1000);
this.session.claims.eid.authLevel = Math.min(this.session.claims.eid.authLevel, 2 /* STANDARD */);
this.session.claims.iat = now;
this.session.claims.exp = now + 15 * 60;
this.session.claims.jti = bufferToBase64url(crypto.getRandomValues(new Uint8Array(16)).buffer);
this.session.accessToken = this.createUnsignedToken(this.session.claims);
this.persistSession();
this.scheduleRefresh();
}
}
var sessionManagerInstance = null;
function getSessionManager() {
if (!sessionManagerInstance)
sessionManagerInstance = new SessionManager;
return sessionManagerInstance;
}
// src/client/recovery.ts
class RecoveryManager {
config = null;
activeRequest = null;
constructor() {
this.loadConfig();
}
async initializeRecovery(threshold = 3) {
this.config = {
threshold,
delaySeconds: 48 * 60 * 60,
guardians: [],
guardianListHash: "",
updatedAt: Date.now()
};
await this.saveConfig();
return this.config;
}
async addGuardian(guardian) {
if (!this.config)
throw new Error("Recovery not initialized");
if (this.config.guardians.length >= 7)
throw new Error("Maximum of 7 guardians allowed");
const newGuardian = {
...guardian,
id: bufferToBase64url(crypto.getRandomValues(new Uint8Array(16)).buffer),
addedAt: Date.now()
};
this.config.guardians.push(newGuardian);
this.config.guardianListHash = await this.hashGuardianList();
this.config.updatedAt = Date.now();
await this.saveConfig();
return newGuardian;
}
async removeGuardian(guardianId) {
if (!this.config)
throw new Error("Recovery not initialized");
const index = this.config.guardians.findIndex((g) => g.id === guardianId);
if (index === -1)
throw new Error("Guardian not found");
const remainingWeight = this.config.guardians.filter((g) => g.id !== guardianId).reduce((sum, g) => sum + g.weight, 0);
if (remainingWeight < this.config.threshold)
throw new Error("Cannot remove guardian: would make recovery impossible");
this.config.guardians.splice(index, 1);
this.config.guardianListHash = await this.hashGuardianList();
this.config.updatedAt = Date.now();
await this.saveConfig();
}
async setThreshold(threshold) {
if (!this.config)
throw new Error("Recovery not initialized");
const totalWeight = this.config.guardians.reduce((sum, g) => sum + g.weight, 0);
if (threshold > totalWeight)
throw new Error("Threshold cannot exceed total guardian weight");
if (threshold < 1)
throw new Error("Threshold must be at least 1");
this.config.threshold = threshold;
this.config.updatedAt = Date.now();
await this.saveConfig();
}
async setDelay(delaySeconds) {
if (!this.config)
throw new Error("Recovery not initialized");
if (delaySeconds < 3600 || delaySeconds > 7 * 24 * 3600)
throw new Error("Delay must be between 1 hour and 7 days");
this.config.delaySeconds = delaySeconds;
this.config.updatedAt = Date.now();
await this.saveConfig();
}
getConfig() {
return this.config;
}
isConfigured() {
if (!this.config)
return false;
return this.config.guardians.reduce((sum, g) => sum + g.weight, 0) >= this.config.threshold;
}
async verifyGuardian(guardianId) {
if (!this.config)
throw new Error("Recovery not initialized");
const guardian = this.config.guardians.find((g) => g.id === guardianId);
if (!guardian)
throw new Error("Guardian not found");
guardian.lastVerified = Date.now();
await this.saveConfig();
return true;
}
async initiateRecovery(newCredentialId) {
if (!this.config)
throw new Error("Recovery not configured");
if (this.activeRequest?.status === "pending")
throw new Error("Recovery already in progress");
const now = Date.now();
this.activeRequest = {
id: bufferToBase64url(crypto.getRandomValues(new Uint8Array(16)).buffer),
accountDID: "",
newCredentialId,
initiatedAt: now,
completesAt: now + this.config.delaySeconds * 1000,
status: "pending",
approvals: [],
approvalWeight: 0
};
return this.activeRequest;
}
async approveRecovery(guardianId, signature) {
if (!this.activeRequest || this.activeRequest.status !== "pending")
throw new Error("No pending recovery request");
if (!this.config)
throw new Error("Recovery not configured");
const guardian = this.config.guardians.find((g) => g.id === guardianId);
if (!guardian)
throw new Error("Guardian not found");
if (this.activeRequest.approvals.some((a) => a.guardianId === guardianId))
throw new Error("Guardian already approved");
this.activeRequest.approvals.push({ guardianId, approvedAt: Date.now(), signature });
this.activeRequest.approvalWeight += guardian.weight;
if (this.activeRequest.approvalWeight >= this.config.threshold) {
this.activeRequest.status = "approved";
}
return this.activeRequest;
}
async cancelRecovery() {
if (!this.activeRequest || this.activeRequest.status !== "pending")
throw new Error("No pending recovery request to cancel");
this.activeRequest.status = "cancelled";
this.activeRequest = null;
}
async completeRecovery() {
if (!this.activeRequest)
throw new Error("No recovery request");
if (this.activeRequest.status !== "approved")
throw new Error("Recovery not approved");
if (Date.now() < this.activeRequest.completesAt) {
const remaining = this.activeRequest.completesAt - Date.now();
throw new Error(`Time-lock not expired. ${Math.ceil(remaining / 1000 / 60)} minutes remaining.`);
}
this.activeRequest.status = "completed";
this.activeRequest = null;
}
getActiveRequest() {
return this.activeRequest;
}
async hashGuardianList() {
if (!this.config)
return "";
const sortedIds = this.config.guardians.map((g) => g.id).sort().join(",");
const hash = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(sortedIds));
return bufferToBase64url(hash);
}
async saveConfig() {
if (!this.config)
return;
try {
localStorage.setItem("encryptid_recovery", JSON.stringify(this.config));
} catch {}
}
loadConfig() {
try {
const stored = localStorage.getItem("encryptid_recovery");
if (stored)
this.config = JSON.parse(stored);
} catch {}
}
}
var recoveryManagerInstance = null;
function getRecoveryManager() {
if (!recoveryManagerInstance)
recoveryManagerInstance = new RecoveryManager;
return recoveryManagerInstance;
}
function getGuardianTypeInfo(type) {
switch (type) {
case "secondary_passkey" /* SECONDARY_PASSKEY */:
return { name: "Backup Passkey", description: "Another device you own (phone, YubiKey, etc.)", icon: "key", setupInstructions: "Register a passkey on a second device you control." };
case "trusted_contact" /* TRUSTED_CONTACT */:
return { name: "Trusted Contact", description: "A friend or family member with their own EncryptID", icon: "user", setupInstructions: "Ask a trusted person to create an EncryptID account." };
case "hardware_key" /* HARDWARE_KEY */:
return { name: "Hardware Security Key", description: "A YubiKey or similar device stored offline", icon: "shield", setupInstructions: "Register a hardware security key and store it safely." };
case "institutional" /* INSTITUTIONAL */:
return { name: "Recovery Service", description: "A professional recovery service provider", icon: "building", setupInstructions: "Connect with a trusted recovery service." };
case "time_delayed_self" /* TIME_DELAYED_SELF */:
return { name: "Time-Delayed Self", description: "Recover yourself after a waiting period", icon: "clock", setupInstructions: "Set up a recovery option that requires waiting before completing." };
default:
return { name: "Unknown", description: "Unknown guardian type", icon: "question", setupInstructions: "" };
}
}
export { EncryptIDKeyManager, encryptData, decryptData, decryptDataAsString, signData, verifySignature, wrapKeyForRecipient, unwrapSharedKey, getKeyManager, resetKeyManager, OPERATION_PERMISSIONS, SessionManager, getSessionManager, RecoveryManager, getRecoveryManager, getGuardianTypeInfo };

View File

@ -0,0 +1,194 @@
// src/client/webauthn.ts
var DEFAULT_CONFIG = {
rpId: "jeffemmett.com",
rpName: "EncryptID",
origin: typeof window !== "undefined" ? window.location.origin : "",
userVerification: "required",
timeout: 60000
};
var conditionalUIAbortController = null;
function abortConditionalUI() {
if (conditionalUIAbortController) {
conditionalUIAbortController.abort();
conditionalUIAbortController = null;
}
}
function bufferToBase64url(buffer) {
const bytes = new Uint8Array(buffer);
let binary = "";
for (let i = 0;i < bytes.byteLength; i++) {
binary += String.fromCharCode(bytes[i]);
}
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
}
function base64urlToBuffer(base64url) {
const base64 = base64url.replace(/-/g, "+").replace(/_/g, "/");
const padding = "=".repeat((4 - base64.length % 4) % 4);
const binary = atob(base64 + padding);
const bytes = new Uint8Array(binary.length);
for (let i = 0;i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i);
}
return bytes.buffer;
}
function generateChallenge() {
return crypto.getRandomValues(new Uint8Array(32)).buffer;
}
async function generatePRFSalt(purpose) {
const encoder = new TextEncoder;
const data = encoder.encode(`encryptid-prf-salt-${purpose}-v1`);
return crypto.subtle.digest("SHA-256", data);
}
async function registerPasskey(username, displayName, config = {}) {
abortConditionalUI();
const cfg = { ...DEFAULT_CONFIG, ...config };
if (!window.PublicKeyCredential) {
throw new Error("WebAuthn is not supported in this browser");
}
const platformAvailable = await PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable();
const userId = crypto.getRandomValues(new Uint8Array(32));
const challenge = generateChallenge();
const prfSalt = await generatePRFSalt("master-key");
const createOptions = {
publicKey: {
challenge: new Uint8Array(challenge),
rp: { id: cfg.rpId, name: cfg.rpName },
user: { id: userId, name: username, displayName },
pubKeyCredParams: [
{ alg: -7, type: "public-key" },
{ alg: -257, type: "public-key" }
],
authenticatorSelection: {
residentKey: "required",
requireResidentKey: true,
userVerification: cfg.userVerification,
authenticatorAttachment: platformAvailable ? "platform" : undefined
},
attestation: "none",
timeout: cfg.timeout,
extensions: {
prf: { eval: { first: new Uint8Array(prfSalt) } },
credProps: true
}
}
};
const credential = await navigator.credentials.create(createOptions);
if (!credential)
throw new Error("Failed to create credential");
const response = credential.response;
const prfSupported = credential.getClientExtensionResults()?.prf?.enabled === true;
const publicKey = response.getPublicKey();
if (!publicKey)
throw new Error("Failed to get public key from credential");
return {
credentialId: bufferToBase64url(credential.rawId),
publicKey,
userId: bufferToBase64url(userId.buffer),
username,
createdAt: Date.now(),
prfSupported,
transports: response.getTransports?.()
};
}
async function authenticatePasskey(credentialId, config = {}) {
abortConditionalUI();
const cfg = { ...DEFAULT_CONFIG, ...config };
if (!window.PublicKeyCredential) {
throw new Error("WebAuthn is not supported in this browser");
}
const challenge = generateChallenge();
const prfSalt = await generatePRFSalt("master-key");
const allowCredentials = credentialId ? [{ type: "public-key", id: new Uint8Array(base64urlToBuffer(credentialId)) }] : undefined;
const getOptions = {
publicKey: {
challenge: new Uint8Array(challenge),
rpId: cfg.rpId,
allowCredentials,
userVerification: cfg.userVerification,
timeout: cfg.timeout,
extensions: {
prf: { eval: { first: new Uint8Array(prfSalt) } }
}
}
};
const credential = await navigator.credentials.get(getOptions);
if (!credential)
throw new Error("Authentication failed");
const response = credential.response;
const prfResults = credential.getClientExtensionResults()?.prf?.results;
return {
credentialId: bufferToBase64url(credential.rawId),
userId: response.userHandle ? bufferToBase64url(response.userHandle) : "",
prfOutput: prfResults?.first,
signature: response.signature,
authenticatorData: response.authenticatorData
};
}
async function isConditionalMediationAvailable() {
if (!window.PublicKeyCredential)
return false;
if (typeof PublicKeyCredential.isConditionalMediationAvailable === "function") {
return PublicKeyCredential.isConditionalMediationAvailable();
}
return false;
}
async function startConditionalUI(config = {}) {
const available = await isConditionalMediationAvailable();
if (!available)
return null;
abortConditionalUI();
conditionalUIAbortController = new AbortController;
const cfg = { ...DEFAULT_CONFIG, ...config };
const challenge = generateChallenge();
const prfSalt = await generatePRFSalt("master-key");
try {
const credential = await navigator.credentials.get({
publicKey: {
challenge: new Uint8Array(challenge),
rpId: cfg.rpId,
userVerification: cfg.userVerification,
timeout: cfg.timeout,
extensions: {
prf: { eval: { first: new Uint8Array(prfSalt) } }
}
},
mediation: "conditional",
signal: conditionalUIAbortController.signal
});
conditionalUIAbortController = null;
if (!credential)
return null;
const response = credential.response;
const prfResults = credential.getClientExtensionResults()?.prf?.results;
return {
credentialId: bufferToBase64url(credential.rawId),
userId: response.userHandle ? bufferToBase64url(response.userHandle) : "",
prfOutput: prfResults?.first,
signature: response.signature,
authenticatorData: response.authenticatorData
};
} catch {
return null;
}
}
async function detectCapabilities() {
const capabilities = {
webauthn: false,
platformAuthenticator: false,
conditionalUI: false,
prfExtension: false
};
if (!window.PublicKeyCredential)
return capabilities;
capabilities.webauthn = true;
try {
capabilities.platformAuthenticator = await PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable();
} catch {
capabilities.platformAuthenticator = false;
}
capabilities.conditionalUI = await isConditionalMediationAvailable();
capabilities.prfExtension = true;
return capabilities;
}
export { abortConditionalUI, bufferToBase64url, base64urlToBuffer, generateChallenge, registerPasskey, authenticatePasskey, isConditionalMediationAvailable, startConditionalUI, detectCapabilities };

View File

@ -0,0 +1,38 @@
import {
verifyEncryptIDToken
} from "./index-stg63j73.js";
// src/server/ws-auth.ts
async function authenticateWSUpgrade(request, options = {}) {
const url = new URL(request.url);
const queryToken = url.searchParams.get("token");
if (queryToken) {
try {
return await verifyEncryptIDToken(queryToken, options);
} catch {
return null;
}
}
const protocols = request.headers.get("Sec-WebSocket-Protocol") || "";
const tokenProtocol = protocols.split(",").map((p) => p.trim()).find((p) => p.startsWith("encryptid."));
if (tokenProtocol) {
const token = tokenProtocol.slice("encryptid.".length);
try {
return await verifyEncryptIDToken(token, options);
} catch {
return null;
}
}
const cookie = request.headers.get("Cookie") || "";
const match = cookie.match(/encryptid_token=([^;]+)/);
if (match) {
try {
return await verifyEncryptIDToken(match[1], options);
} catch {
return null;
}
}
return null;
}
export { authenticateWSUpgrade };

View File

@ -0,0 +1,25 @@
// src/types/index.ts
var AuthLevel;
((AuthLevel2) => {
AuthLevel2[AuthLevel2["BASIC"] = 1] = "BASIC";
AuthLevel2[AuthLevel2["STANDARD"] = 2] = "STANDARD";
AuthLevel2[AuthLevel2["ELEVATED"] = 3] = "ELEVATED";
AuthLevel2[AuthLevel2["CRITICAL"] = 4] = "CRITICAL";
})(AuthLevel ||= {});
var GuardianType;
((GuardianType2) => {
GuardianType2["SECONDARY_PASSKEY"] = "secondary_passkey";
GuardianType2["TRUSTED_CONTACT"] = "trusted_contact";
GuardianType2["HARDWARE_KEY"] = "hardware_key";
GuardianType2["INSTITUTIONAL"] = "institutional";
GuardianType2["TIME_DELAYED_SELF"] = "time_delayed_self";
})(GuardianType ||= {});
var SpaceVisibility;
((SpaceVisibility2) => {
SpaceVisibility2["PUBLIC"] = "public";
SpaceVisibility2["PUBLIC_READ"] = "public_read";
SpaceVisibility2["AUTHENTICATED"] = "authenticated";
SpaceVisibility2["MEMBERS_ONLY"] = "members_only";
})(SpaceVisibility ||= {});
export { AuthLevel, GuardianType, SpaceVisibility };

View File

@ -0,0 +1,170 @@
import {
bufferToBase64url
} from "./index-2cp5044h.js";
// src/client/api-client.ts
var DEFAULT_SERVER_URL = "https://encryptid.jeffemmett.com";
class EncryptIDClient {
serverUrl;
constructor(serverUrl = DEFAULT_SERVER_URL) {
this.serverUrl = serverUrl.replace(/\/$/, "");
}
async registerStart(username, displayName) {
const res = await fetch(`${this.serverUrl}/api/register/start`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ username, displayName: displayName || username })
});
if (!res.ok) {
const err = await res.json().catch(() => ({ error: "Registration start failed" }));
throw new Error(err.error || `HTTP ${res.status}`);
}
return res.json();
}
async registerComplete(challenge, credential, userId, username) {
const response = credential.response;
const publicKey = response.getPublicKey();
const res = await fetch(`${this.serverUrl}/api/register/complete`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
challenge,
userId,
username,
credential: {
credentialId: bufferToBase64url(credential.rawId),
publicKey: publicKey ? bufferToBase64url(publicKey) : "",
transports: response.getTransports?.() || []
}
})
});
if (!res.ok) {
const err = await res.json().catch(() => ({ error: "Registration complete failed" }));
throw new Error(err.error || `HTTP ${res.status}`);
}
return res.json();
}
async authStart(credentialId) {
const res = await fetch(`${this.serverUrl}/api/auth/start`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(credentialId ? { credentialId } : {})
});
if (!res.ok) {
const err = await res.json().catch(() => ({ error: "Auth start failed" }));
throw new Error(err.error || `HTTP ${res.status}`);
}
return res.json();
}
async authComplete(challenge, credential) {
const response = credential.response;
const prfResults = credential.getClientExtensionResults()?.prf?.results;
const res = await fetch(`${this.serverUrl}/api/auth/complete`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
challenge,
credential: {
credentialId: bufferToBase64url(credential.rawId),
signature: bufferToBase64url(response.signature),
authenticatorData: bufferToBase64url(response.authenticatorData),
prfOutput: prfResults?.first ? bufferToBase64url(prfResults.first) : null
}
})
});
if (!res.ok) {
const err = await res.json().catch(() => ({ error: "Auth complete failed" }));
throw new Error(err.error || `HTTP ${res.status}`);
}
return res.json();
}
async verifySession(token) {
const res = await fetch(`${this.serverUrl}/api/session/verify`, {
headers: { Authorization: `Bearer ${token}` }
});
return res.json();
}
async refreshToken(token) {
const res = await fetch(`${this.serverUrl}/api/session/refresh`, {
method: "POST",
headers: { Authorization: `Bearer ${token}` }
});
if (!res.ok) {
const err = await res.json().catch(() => ({ error: "Token refresh failed" }));
throw new Error(err.error || `HTTP ${res.status}`);
}
return res.json();
}
async listCredentials(token) {
const res = await fetch(`${this.serverUrl}/api/user/credentials`, {
headers: { Authorization: `Bearer ${token}` }
});
if (!res.ok) {
throw new Error("Failed to list credentials");
}
return res.json();
}
async register(username, displayName, config) {
const { options, userId } = await this.registerStart(username, displayName);
const createOptions = {
publicKey: {
...options,
challenge: base64urlToUint8Array(options.challenge),
user: {
...options.user,
id: base64urlToUint8Array(options.user.id)
},
pubKeyCredParams: options.pubKeyCredParams,
extensions: {
credProps: true,
prf: { eval: { first: new Uint8Array(32) } }
}
}
};
const credential = await navigator.credentials.create(createOptions);
if (!credential)
throw new Error("Failed to create credential");
return this.registerComplete(options.challenge, credential, userId, username);
}
async authenticate(credentialId, config) {
const { options } = await this.authStart(credentialId);
const getOptions = {
publicKey: {
challenge: base64urlToUint8Array(options.challenge),
rpId: options.rpId,
userVerification: options.userVerification,
timeout: options.timeout,
allowCredentials: options.allowCredentials?.map((c) => ({
type: c.type,
id: base64urlToUint8Array(c.id),
transports: c.transports
})),
extensions: {
prf: { eval: { first: new Uint8Array(32) } }
}
}
};
const credential = await navigator.credentials.get(getOptions);
if (!credential)
throw new Error("Authentication failed");
const result = await this.authComplete(options.challenge, credential);
const prfResults = credential.getClientExtensionResults()?.prf?.results;
return {
...result,
prfOutput: prfResults?.first
};
}
}
function base64urlToUint8Array(base64url) {
const base64 = base64url.replace(/-/g, "+").replace(/_/g, "/");
const padding = "=".repeat((4 - base64.length % 4) % 4);
const binary = atob(base64 + padding);
const bytes = new Uint8Array(binary.length);
for (let i = 0;i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i);
}
return bytes;
}
export { EncryptIDClient };

View File

@ -0,0 +1,72 @@
import {
verifyEncryptIDToken
} from "./index-stg63j73.js";
// src/server/space-auth.ts
async function evaluateSpaceAccess(spaceSlug, token, method, options) {
const config = await options.getSpaceConfig(spaceSlug);
if (!config) {
return { allowed: false, claims: null, reason: "Space not found", isOwner: false, readOnly: false };
}
let claims = null;
if (token) {
try {
claims = await verifyEncryptIDToken(token, options);
} catch {}
}
const isRead = method === "GET" || method === "HEAD" || method === "OPTIONS";
const isOwner = !!(claims && config.ownerDID && claims.sub === config.ownerDID);
switch (config.visibility) {
case "public" /* PUBLIC */:
return { allowed: true, claims, isOwner, readOnly: false };
case "public_read" /* PUBLIC_READ */:
if (isRead) {
return { allowed: true, claims, isOwner, readOnly: !claims };
}
if (!claims) {
return {
allowed: false,
claims: null,
reason: "Authentication required to modify this space",
isOwner: false,
readOnly: true
};
}
return { allowed: true, claims, isOwner, readOnly: false };
case "authenticated" /* AUTHENTICATED */:
if (!claims) {
return { allowed: false, claims: null, reason: "Authentication required", isOwner: false, readOnly: false };
}
return { allowed: true, claims, isOwner, readOnly: false };
case "members_only" /* MEMBERS_ONLY */:
if (!claims) {
return { allowed: false, claims: null, reason: "Authentication required", isOwner: false, readOnly: false };
}
return { allowed: true, claims, isOwner, readOnly: false };
default:
return { allowed: false, claims: null, reason: "Unknown visibility setting", isOwner: false, readOnly: false };
}
}
function extractToken(headers) {
if (typeof headers.get === "function") {
const auth = headers.get("Authorization") || headers.get("authorization");
if (auth?.startsWith("Bearer "))
return auth.slice(7);
const cookie = headers.get("Cookie") || headers.get("cookie") || "";
const match = cookie.match(/encryptid_token=([^;]+)/);
if (match)
return match[1];
}
if (typeof headers.authorization === "string") {
if (headers.authorization.startsWith("Bearer "))
return headers.authorization.slice(7);
}
if (typeof headers.cookie === "string") {
const match = headers.cookie.match(/encryptid_token=([^;]+)/);
if (match)
return match[1];
}
return null;
}
export { evaluateSpaceAccess, extractToken };

View File

@ -0,0 +1,118 @@
// src/server/jwt-verify.ts
var ENCRYPTID_SERVER = "https://encryptid.jeffemmett.com";
async function verifyEncryptIDToken(token, options = {}) {
const { secret, serverUrl = ENCRYPTID_SERVER, audience, clockTolerance = 30 } = options;
if (secret) {
return verifyLocally(token, secret, audience, clockTolerance);
}
return verifyRemotely(token, serverUrl);
}
async function verifyLocally(token, secret, audience, clockTolerance = 30) {
const parts = token.split(".");
if (parts.length !== 3) {
throw new Error("Invalid JWT format");
}
const [headerB64, payloadB64, signatureB64] = parts;
const encoder = new TextEncoder;
const key = await crypto.subtle.importKey("raw", encoder.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["verify"]);
const data = encoder.encode(`${headerB64}.${payloadB64}`);
const signature = base64urlDecode(signatureB64);
const valid = await crypto.subtle.verify("HMAC", key, signature, data);
if (!valid) {
throw new Error("Invalid JWT signature");
}
const payload = JSON.parse(new TextDecoder().decode(base64urlDecode(payloadB64)));
const now = Math.floor(Date.now() / 1000);
if (payload.exp && now > payload.exp + clockTolerance) {
throw new Error("Token expired");
}
if (audience && payload.aud) {
const auds = Array.isArray(payload.aud) ? payload.aud : [payload.aud];
if (!auds.some((a) => a.includes(audience))) {
throw new Error(`Token audience mismatch: expected ${audience}`);
}
}
return payload;
}
async function verifyRemotely(token, serverUrl) {
const res = await fetch(`${serverUrl}/api/session/verify`, {
headers: { Authorization: `Bearer ${token}` }
});
const data = await res.json();
if (!data.valid) {
throw new Error(data.error || "Invalid token");
}
const parts = token.split(".");
if (parts.length >= 2) {
try {
const payload = JSON.parse(new TextDecoder().decode(base64urlDecode(parts[1])));
return payload;
} catch {}
}
return {
iss: serverUrl,
sub: data.userId,
aud: [],
iat: 0,
exp: data.exp || 0,
jti: "",
username: data.username,
did: data.did,
eid: {
authLevel: 2,
authTime: 0,
capabilities: { encrypt: true, sign: true, wallet: false },
recoveryConfigured: false
}
};
}
function getAuthLevel(claims) {
if (!claims.eid)
return 1;
const authAge = Math.floor(Date.now() / 1000) - claims.eid.authTime;
if (authAge < 60)
return 3;
if (authAge < 15 * 60)
return 2;
return 1;
}
function checkPermission(claims, permission) {
const currentLevel = getAuthLevel(claims);
if (currentLevel < permission.minAuthLevel) {
return {
allowed: false,
reason: `Requires auth level ${permission.minAuthLevel} (current: ${currentLevel})`
};
}
if (permission.requiresCapability) {
const has = claims.eid?.capabilities?.[permission.requiresCapability];
if (!has) {
return {
allowed: false,
reason: `Requires ${permission.requiresCapability} capability`
};
}
}
if (permission.maxAgeSeconds) {
const authAge = Math.floor(Date.now() / 1000) - (claims.eid?.authTime || 0);
if (authAge > permission.maxAgeSeconds) {
return {
allowed: false,
reason: `Authentication too old (${authAge}s > ${permission.maxAgeSeconds}s)`
};
}
}
return { allowed: true };
}
function base64urlDecode(str) {
const base64 = str.replace(/-/g, "+").replace(/_/g, "/");
const padding = "=".repeat((4 - base64.length % 4) % 4);
const binary = atob(base64 + padding);
const bytes = new Uint8Array(binary.length);
for (let i = 0;i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i);
}
return bytes.buffer;
}
export { verifyEncryptIDToken, getAuthLevel, checkPermission };

View File

@ -0,0 +1,15 @@
/**
* @encryptid/sdk Self-Sovereign Identity SDK
*
* WebAuthn passkey authentication with derived keys, social recovery,
* and cross-app SSO for the r-ecosystem.
*/
export type { EncryptIDCredential, AuthenticationResult, EncryptIDConfig, WebAuthnCapabilities, DerivedKeys, EncryptedData, SignedData, EncryptIDClaims, SessionState, OperationPermission, Guardian, RecoveryConfig, RecoveryRequest, RegistrationStartResponse, RegistrationCompleteResponse, AuthStartResponse, AuthCompleteResponse, SessionVerifyResponse, EmailRecoverySetResponse, EmailRecoveryRequestResponse, EmailRecoveryVerifyResponse, } from './types/index.js';
export { AuthLevel, GuardianType } from './types/index.js';
export { EncryptIDClient } from './client/api-client.js';
export { registerPasskey, authenticatePasskey, startConditionalUI, detectCapabilities, bufferToBase64url, base64urlToBuffer, } from './client/webauthn.js';
export { EncryptIDKeyManager, getKeyManager, signEthHash } from './client/key-derivation.js';
export { SessionManager, getSessionManager, OPERATION_PERMISSIONS } from './client/session.js';
export { RecoveryManager, getRecoveryManager, getGuardianTypeInfo } from './client/recovery.js';
export declare const VERSION = "0.1.0";
export declare const SPEC_VERSION = "2026-02";

3618
frontend/vendor/@encryptid/sdk/index.js vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,129 @@
{
"name": "@encryptid/sdk",
"version": "0.1.0",
"description": "Unified identity SDK for the r-ecosystem — WebAuthn passkeys, key derivation, session management, and social recovery",
"type": "module",
"main": "./index.js",
"types": "./index.d.ts",
"exports": {
".": {
"import": "./index.js",
"types": "./index.d.ts"
},
"./client": {
"import": "./client/index.js",
"types": "./client/index.d.ts"
},
"./server": {
"import": "./server/index.js",
"types": "./server/index.d.ts"
},
"./server/nextjs": {
"import": "./server/middleware/nextjs.js",
"types": "./server/middleware/nextjs.d.ts"
},
"./server/hono": {
"import": "./server/middleware/hono.js",
"types": "./server/middleware/hono.d.ts"
},
"./server/express": {
"import": "./server/middleware/express.js",
"types": "./server/middleware/express.d.ts"
},
"./server/space-auth": {
"import": "./server/space-auth.js",
"types": "./server/space-auth.d.ts"
},
"./server/ws-auth": {
"import": "./server/ws-auth.js",
"types": "./server/ws-auth.d.ts"
},
"./ui": {
"import": "./ui/index.js",
"types": "./ui/index.d.ts"
},
"./ui/react": {
"import": "./ui/react/index.js",
"types": "./ui/react/index.d.ts"
},
"./types": {
"import": "./types/index.js",
"types": "./types/index.d.ts"
},
"./types/roles": {
"import": "./types/roles.js",
"types": "./types/roles.d.ts"
},
"./types/module-permissions": {
"import": "./types/module-permissions.js",
"types": "./types/module-permissions.d.ts"
},
"./types/modules": {
"import": "./types/modules/index.js",
"types": "./types/modules/index.d.ts"
},
"./server/role-resolver": {
"import": "./server/role-resolver.js",
"types": "./server/role-resolver.d.ts"
},
"./types/membership-events": {
"import": "./types/membership-events.js",
"types": "./types/membership-events.d.ts"
},
"./client/token-relay": {
"import": "./client/token-relay.js",
"types": "./client/token-relay.d.ts"
},
"./browser": {
"import": "./encryptid.browser.js",
"default": "./encryptid.browser.js"
}
},
"files": [
"dist",
"src/python",
"src/browser.ts"
],
"scripts": {
"build": "bun build ./src/index.ts --outdir ./dist --target browser && tsc --emitDeclarationOnly",
"build:browser": "bun build ./src/browser.ts --outfile ./encryptid.browser.js --target browser --minify",
"build:node": "bun build ./src/server/index.ts --outdir ./server --target node",
"build:all": "bun run build && bun run build:browser",
"typecheck": "tsc --noEmit",
"clean": "rm -rf dist"
},
"dependencies": {
"@noble/curves": "^2.0.1",
"@noble/hashes": "^2.0.1",
"hono": "^4.11.0"
},
"peerDependencies": {
"next": ">=14.0.0",
"react": ">=18.0.0"
},
"peerDependenciesMeta": {
"react": {
"optional": true
},
"next": {
"optional": true
}
},
"devDependencies": {
"@types/react": "^19.0.0",
"typescript": "^5.7.0"
},
"license": "MIT",
"repository": {
"type": "git",
"url": "https://gitea.jeffemmett.com/jeffemmett/encryptid-sdk"
},
"keywords": [
"webauthn",
"passkey",
"identity",
"encryption",
"self-sovereign",
"social-recovery"
]
}

View File

@ -0,0 +1,10 @@
/**
* @encryptid/sdk/server Server-side module
*/
export { verifyEncryptIDToken, getAuthLevel, checkPermission, } from './jwt-verify.js';
export type { VerifyOptions } from './jwt-verify.js';
export { evaluateSpaceAccess, extractToken, SpaceVisibility, } from './space-auth.js';
export type { SpaceAuthConfig, SpaceAuthResult, SpaceAuthOptions } from './space-auth.js';
export { authenticateWSUpgrade } from './ws-auth.js';
export { resolveSpaceRole, resolveSpaceRoleRemote, createRemoteMembershipLookup, invalidateRoleCache } from './role-resolver.js';
export type { RoleResolverOptions, RemoteResolverOptions } from './role-resolver.js';

View File

@ -0,0 +1,24 @@
import {
evaluateSpaceAccess,
extractToken
} from "../index-j6kh1974.js";
import {
SpaceVisibility
} from "../index-5c1t4ftn.js";
import {
authenticateWSUpgrade
} from "../index-2yszamrn.js";
import {
checkPermission,
getAuthLevel,
verifyEncryptIDToken
} from "../index-stg63j73.js";
export {
verifyEncryptIDToken,
getAuthLevel,
extractToken,
evaluateSpaceAccess,
checkPermission,
authenticateWSUpgrade,
SpaceVisibility
};

View File

@ -0,0 +1,35 @@
/**
* EncryptID JWT Verification
*
* Server-side utilities for verifying EncryptID JWT tokens.
* Can verify locally with shared secret or by calling the EncryptID server.
*/
import type { EncryptIDClaims, OperationPermission } from '../types/index.js';
export interface VerifyOptions {
/** JWT secret for local verification (HS256) */
secret?: string;
/** EncryptID server URL for remote verification */
serverUrl?: string;
/** Expected audience (your app's origin) */
audience?: string;
/** Clock tolerance in seconds for expiration check */
clockTolerance?: number;
}
/**
* Verify an EncryptID JWT token
*
* If `secret` is provided, verifies locally using HMAC-SHA256.
* Otherwise, calls the EncryptID server's /api/session/verify endpoint.
*/
export declare function verifyEncryptIDToken(token: string, options?: VerifyOptions): Promise<EncryptIDClaims>;
/**
* Extract the auth level from claims
*/
export declare function getAuthLevel(claims: EncryptIDClaims): number;
/**
* Check if claims satisfy an operation permission
*/
export declare function checkPermission(claims: EncryptIDClaims, permission: OperationPermission): {
allowed: boolean;
reason?: string;
};

View File

@ -0,0 +1,74 @@
/**
* EncryptID Express Middleware
*
* Authentication middleware for Express and compatible frameworks.
*/
import { type VerifyOptions } from '../jwt-verify.js';
import { type SpaceAuthOptions } from '../space-auth.js';
import type { EncryptIDClaims, SpaceAuthResult } from '../../types/index.js';
declare global {
namespace Express {
interface Request {
encryptid?: EncryptIDClaims;
spaceAuth?: SpaceAuthResult;
}
}
}
interface ExpressRequest {
headers: Record<string, string | string[] | undefined>;
method: string;
encryptid?: EncryptIDClaims;
spaceAuth?: SpaceAuthResult;
}
interface ExpressResponse {
status(code: number): ExpressResponse;
json(body: unknown): void;
}
type NextFunction = () => void;
/**
* Express middleware that verifies EncryptID JWT tokens
*
* Usage:
* ```ts
* import express from 'express';
* import { encryptIDAuth } from '@encryptid/sdk/server/express';
*
* const app = express();
*
* app.use('/api', encryptIDAuth());
*
* app.get('/api/profile', (req, res) => {
* res.json({ did: req.encryptid.did });
* });
* ```
*/
export declare function encryptIDAuth(options?: VerifyOptions): (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => Promise<void>;
/**
* Optional auth sets session if token present, continues either way
*/
export declare function encryptIDOptional(options?: VerifyOptions): (req: ExpressRequest, _res: ExpressResponse, next: NextFunction) => Promise<void>;
export interface EncryptIDSpaceAuthConfig extends SpaceAuthOptions {
/** Function to extract space slug from request (default: req.params.slug) */
getSlug?: (req: ExpressRequest & {
params?: Record<string, string>;
}) => string;
}
/**
* Express middleware for space-aware auth.
*
* Usage:
* ```ts
* app.use('/api/spaces/:slug', encryptIDSpaceAuth({
* getSpaceConfig: async (slug) => db.getSpace(slug),
* }));
*
* app.get('/api/spaces/:slug', (req, res) => {
* const { readOnly, isOwner } = req.spaceAuth;
* res.json({ canEdit: !readOnly, isOwner });
* });
* ```
*/
export declare function encryptIDSpaceAuth(config: EncryptIDSpaceAuthConfig): (req: ExpressRequest & {
params?: Record<string, string>;
}, res: ExpressResponse, next: NextFunction) => Promise<void>;
export {};

View File

@ -0,0 +1,59 @@
import {
evaluateSpaceAccess,
extractToken
} from "../../index-j6kh1974.js";
import"../../index-5c1t4ftn.js";
import {
verifyEncryptIDToken
} from "../../index-stg63j73.js";
// src/server/middleware/express.ts
function encryptIDAuth(options = {}) {
return async (req, res, next) => {
const authHeader = req.headers.authorization;
if (!authHeader || typeof authHeader !== "string" || !authHeader.startsWith("Bearer ")) {
res.status(401).json({ error: "Missing EncryptID token" });
return;
}
const token = authHeader.slice(7);
try {
req.encryptid = await verifyEncryptIDToken(token, options);
next();
} catch (err) {
res.status(401).json({ error: err.message || "Invalid token" });
}
};
}
function encryptIDOptional(options = {}) {
return async (req, _res, next) => {
const authHeader = req.headers.authorization;
if (authHeader && typeof authHeader === "string" && authHeader.startsWith("Bearer ")) {
try {
req.encryptid = await verifyEncryptIDToken(authHeader.slice(7), options);
} catch {}
}
next();
};
}
function encryptIDSpaceAuth(config) {
const { getSlug, ...options } = config;
return async (req, res, next) => {
const slug = getSlug ? getSlug(req) : req.params?.slug || "";
const token = extractToken(req.headers);
const result = await evaluateSpaceAccess(slug, token, req.method, options);
if (!result.allowed) {
res.status(result.claims ? 403 : 401).json({ error: result.reason });
return;
}
if (result.claims) {
req.encryptid = result.claims;
}
req.spaceAuth = result;
next();
};
}
export {
encryptIDSpaceAuth,
encryptIDOptional,
encryptIDAuth
};

View File

@ -0,0 +1,100 @@
/**
* EncryptID Hono Middleware
*
* Authentication middleware for Hono web framework.
*/
import type { MiddlewareHandler } from 'hono';
import { type VerifyOptions } from '../jwt-verify.js';
import { type SpaceAuthOptions } from '../space-auth.js';
import type { EncryptIDClaims, SpaceAuthResult } from '../../types/index.js';
import type { ResolvedRole } from '../../types/roles.js';
declare module 'hono' {
interface ContextVariableMap {
encryptid: EncryptIDClaims;
spaceAuth: SpaceAuthResult;
spaceRole: ResolvedRole;
}
}
/**
* Hono middleware that verifies EncryptID JWT tokens
*
* Usage:
* ```ts
* import { Hono } from 'hono';
* import { encryptIDAuth } from '@encryptid/sdk/server/hono';
*
* const app = new Hono();
*
* // Protect all /api routes
* app.use('/api/*', encryptIDAuth());
*
* app.get('/api/profile', (c) => {
* const session = c.get('encryptid');
* return c.json({ did: session.did, sub: session.sub });
* });
* ```
*/
export declare function encryptIDAuth(options?: VerifyOptions): MiddlewareHandler;
/**
* Optional auth sets session if token present, continues either way
*/
export declare function encryptIDOptional(options?: VerifyOptions): MiddlewareHandler;
export interface EncryptIDSpaceAuthConfig extends SpaceAuthOptions {
/** Route param name for the space slug (default: 'slug') */
slugParam?: string;
/** Query param fallback for the space slug (default: 'space') */
slugQuery?: string;
}
/**
* Hono middleware for space-aware auth.
*
* Reads the space slug from route params or query, evaluates access
* based on visibility, and sets `c.var.spaceAuth` with the result.
*
* Usage:
* ```ts
* app.use('/api/communities/:slug/*', encryptIDSpaceAuth({
* getSpaceConfig: async (slug) => db.getCommunity(slug),
* }));
*
* app.get('/api/communities/:slug', (c) => {
* const auth = c.get('spaceAuth');
* if (auth.readOnly) { // public_read, unauthenticated
* return c.json({ ...community, canEdit: false });
* }
* });
* ```
*/
export declare function encryptIDSpaceAuth(config: EncryptIDSpaceAuthConfig): MiddlewareHandler;
export interface EncryptIDSpaceRoleConfig extends EncryptIDSpaceAuthConfig {
/** Look up membership for a DID in a space. You provide the DB query. */
getMembership: (userDID: string, spaceSlug: string) => Promise<import('../../types/roles.js').SpaceMembership | null>;
/** Resolve visibility for a space slug (if not in SpaceAuthConfig). Defaults to using getSpaceConfig. */
getVisibility?: (spaceSlug: string) => Promise<import('../../types/index.js').SpaceVisibility>;
}
/**
* Combined space auth + role resolution middleware for Hono.
*
* Sets `c.var.spaceAuth`, `c.var.spaceRole`, and optionally `c.var.encryptid`.
*
* Usage:
* ```ts
* import { encryptIDSpaceRoleAuth } from '@encryptid/sdk/server/hono';
* import { hasCapability } from '@encryptid/sdk';
* import { RVOTE_PERMISSIONS } from '@encryptid/sdk/types/modules';
*
* app.use('/api/spaces/:slug/*', encryptIDSpaceRoleAuth({
* getSpaceConfig: async (slug) => db.getSpace(slug),
* getMembership: async (did, slug) => db.getMembership(did, slug),
* }));
*
* app.post('/api/spaces/:slug/proposals', (c) => {
* const { role } = c.get('spaceRole');
* if (!hasCapability(role, 'create_proposal', RVOTE_PERMISSIONS)) {
* return c.json({ error: 'Insufficient permissions' }, 403);
* }
* // ...
* });
* ```
*/
export declare function encryptIDSpaceRoleAuth(config: EncryptIDSpaceRoleConfig): MiddlewareHandler;

View File

@ -0,0 +1,59 @@
import {
evaluateSpaceAccess,
extractToken
} from "../../index-j6kh1974.js";
import"../../index-5c1t4ftn.js";
import {
verifyEncryptIDToken
} from "../../index-stg63j73.js";
// src/server/middleware/hono.ts
function encryptIDAuth(options = {}) {
return async (c, next) => {
const authHeader = c.req.header("Authorization");
if (!authHeader?.startsWith("Bearer ")) {
return c.json({ error: "Missing EncryptID token" }, 401);
}
const token = authHeader.slice(7);
try {
const claims = await verifyEncryptIDToken(token, options);
c.set("encryptid", claims);
await next();
} catch (err) {
return c.json({ error: err.message || "Invalid token" }, 401);
}
};
}
function encryptIDOptional(options = {}) {
return async (c, next) => {
const authHeader = c.req.header("Authorization");
if (authHeader?.startsWith("Bearer ")) {
try {
const claims = await verifyEncryptIDToken(authHeader.slice(7), options);
c.set("encryptid", claims);
} catch {}
}
await next();
};
}
function encryptIDSpaceAuth(config) {
const { slugParam = "slug", slugQuery = "space", ...options } = config;
return async (c, next) => {
const spaceSlug = c.req.param(slugParam) || c.req.query(slugQuery) || "";
const token = extractToken(c.req.raw.headers);
const result = await evaluateSpaceAccess(spaceSlug, token, c.req.method, options);
if (!result.allowed) {
return c.json({ error: result.reason }, result.claims ? 403 : 401);
}
if (result.claims) {
c.set("encryptid", result.claims);
}
c.set("spaceAuth", result);
await next();
};
}
export {
encryptIDSpaceAuth,
encryptIDOptional,
encryptIDAuth
};

View File

@ -0,0 +1,136 @@
/**
* EncryptID Next.js Middleware
*
* Helpers for protecting Next.js App Router routes and API endpoints.
*/
import { type VerifyOptions } from '../jwt-verify.js';
import { type SpaceAuthOptions } from '../space-auth.js';
import type { EncryptIDClaims, SpaceAuthResult, SpaceVisibility } from '../../types/index.js';
import type { ResolvedRole, SpaceMembership } from '../../types/roles.js';
export interface EncryptIDNextConfig extends VerifyOptions {
/** Paths that don't require authentication */
publicPaths?: string[];
/** Redirect URL for unauthenticated requests (null = return 401) */
loginUrl?: string | null;
}
/**
* Get EncryptID session from a Next.js request
*
* Usage in API routes:
* ```ts
* import { getEncryptIDSession } from '@encryptid/sdk/server/nextjs';
*
* export async function GET(req: Request) {
* const session = await getEncryptIDSession(req);
* if (!session) return new Response('Unauthorized', { status: 401 });
* // session.sub, session.did, session.eid.authLevel, etc.
* }
* ```
*/
export declare function getEncryptIDSession(request: Request, options?: VerifyOptions): Promise<EncryptIDClaims | null>;
/**
* Protect a Next.js API route handler
*
* Usage:
* ```ts
* import { withEncryptID } from '@encryptid/sdk/server/nextjs';
*
* export const GET = withEncryptID(async (req, session) => {
* return Response.json({ user: session.sub, did: session.did });
* });
* ```
*/
export declare function withEncryptID(handler: (request: Request, session: EncryptIDClaims) => Promise<Response>, options?: VerifyOptions): (request: Request) => Promise<Response>;
/**
* Create Next.js middleware for EncryptID
*
* Usage in middleware.ts:
* ```ts
* import { createEncryptIDMiddleware } from '@encryptid/sdk/server/nextjs';
*
* const encryptIDMiddleware = createEncryptIDMiddleware({
* publicPaths: ['/auth/signin', '/api/auth'],
* loginUrl: '/auth/signin',
* });
*
* export function middleware(request: NextRequest) {
* return encryptIDMiddleware(request);
* }
* ```
*/
export declare function createEncryptIDMiddleware(config?: EncryptIDNextConfig): (request: Request) => Promise<Response | null>;
/**
* Check space access in a Next.js API route or server component.
*
* Usage:
* ```ts
* const result = await checkSpaceAccess(request, spaceSlug, {
* getSpaceConfig: async (slug) => {
* const space = await prisma.space.findUnique({ where: { slug } });
* if (!space) return null;
* return { spaceSlug: slug, visibility: space.visibility, app: 'rvote' };
* },
* });
* if (!result.allowed) return new Response(result.reason, { status: 401 });
* ```
*/
export declare function checkSpaceAccess(request: Request, spaceSlug: string, options: SpaceAuthOptions): Promise<SpaceAuthResult>;
/**
* HOC that wraps a Next.js API route handler with space auth.
*
* Usage:
* ```ts
* export const POST = withSpaceAuth(
* async (req, spaceAuth, slug) => {
* return Response.json({ owner: spaceAuth.isOwner });
* },
* (req) => new URL(req.url).pathname.split('/')[2],
* { getSpaceConfig: async (slug) => { ... } },
* );
* ```
*/
export declare function withSpaceAuth(handler: (request: Request, spaceAuth: SpaceAuthResult, spaceSlug: string) => Promise<Response>, getSlug: (request: Request) => string, options: SpaceAuthOptions): (request: Request) => Promise<Response>;
export interface SpaceRoleOptions extends SpaceAuthOptions {
/** Look up membership for a DID in a space. You provide the DB query. */
getMembership: (userDID: string, spaceSlug: string) => Promise<SpaceMembership | null>;
/** Resolve visibility for a space slug. If not provided, uses getSpaceConfig. */
getVisibility?: (spaceSlug: string) => Promise<SpaceVisibility>;
}
export interface SpaceRoleResult {
spaceAuth: SpaceAuthResult;
resolvedRole: ResolvedRole;
}
/**
* Check space access AND resolve role in one call.
*
* Usage:
* ```ts
* const result = await checkSpaceRole(request, slug, {
* getSpaceConfig: async (slug) => prisma.space.findUnique({ where: { slug } }),
* getMembership: async (did, slug) => prisma.spaceMember.findUnique({
* where: { userDID_spaceSlug: { userDID: did, spaceSlug: slug } }
* }),
* });
* if (!result.spaceAuth.allowed) return deny();
* if (hasCapability(result.resolvedRole.role, 'create_proposal', RVOTE_PERMISSIONS)) { ... }
* ```
*/
export declare function checkSpaceRole(request: Request, spaceSlug: string, options: SpaceRoleOptions): Promise<SpaceRoleResult>;
/**
* HOC that wraps a Next.js API route handler with space auth + role resolution.
*
* Usage:
* ```ts
* export const POST = withSpaceRole(
* async (req, spaceAuth, role, slug) => {
* if (!hasCapability(role.role, 'create_proposal', RVOTE_PERMISSIONS)) {
* return Response.json({ error: 'Forbidden' }, { status: 403 });
* }
* return Response.json({ created: true });
* },
* (req) => new URL(req.url).pathname.split('/')[3], // extract slug
* { getSpaceConfig: ..., getMembership: ... },
* );
* ```
*/
export declare function withSpaceRole(handler: (request: Request, spaceAuth: SpaceAuthResult, resolvedRole: ResolvedRole, spaceSlug: string) => Promise<Response>, getSlug: (request: Request) => string, options: SpaceRoleOptions): (request: Request) => Promise<Response>;

View File

@ -0,0 +1,83 @@
import {
evaluateSpaceAccess,
extractToken
} from "../../index-j6kh1974.js";
import"../../index-5c1t4ftn.js";
import {
verifyEncryptIDToken
} from "../../index-stg63j73.js";
// src/server/middleware/nextjs.ts
async function getEncryptIDSession(request, options = {}) {
const authHeader = request.headers.get("Authorization");
if (authHeader?.startsWith("Bearer ")) {
try {
return await verifyEncryptIDToken(authHeader.slice(7), options);
} catch {
return null;
}
}
const cookieHeader = request.headers.get("Cookie") || "";
const tokenMatch = cookieHeader.match(/encryptid_token=([^;]+)/);
if (tokenMatch) {
try {
return await verifyEncryptIDToken(tokenMatch[1], options);
} catch {
return null;
}
}
return null;
}
function withEncryptID(handler, options = {}) {
return async (request) => {
const session = await getEncryptIDSession(request, options);
if (!session) {
return new Response(JSON.stringify({ error: "Unauthorized" }), {
status: 401,
headers: { "Content-Type": "application/json" }
});
}
return handler(request, session);
};
}
function createEncryptIDMiddleware(config = {}) {
const { publicPaths = [], loginUrl = null, ...verifyOptions } = config;
return async (request) => {
const url = new URL(request.url);
if (publicPaths.some((p) => url.pathname.startsWith(p))) {
return null;
}
const session = await getEncryptIDSession(request, verifyOptions);
if (!session) {
if (loginUrl) {
return Response.redirect(new URL(loginUrl, request.url));
}
return new Response("Unauthorized", { status: 401 });
}
return null;
};
}
async function checkSpaceAccess(request, spaceSlug, options) {
const token = extractToken(request.headers);
return evaluateSpaceAccess(spaceSlug, token, request.method, options);
}
function withSpaceAuth(handler, getSlug, options) {
return async (request) => {
const slug = getSlug(request);
const result = await checkSpaceAccess(request, slug, options);
if (!result.allowed) {
return new Response(JSON.stringify({ error: result.reason }), {
status: result.claims ? 403 : 401,
headers: { "Content-Type": "application/json" }
});
}
return handler(request, result, slug);
};
}
export {
withSpaceAuth,
withEncryptID,
getEncryptIDSession,
createEncryptIDMiddleware,
checkSpaceAccess
};

View File

@ -0,0 +1,94 @@
/**
* EncryptID Space Role Resolver
*
* Resolves a user's effective SpaceRole given their space access result
* and a membership lookup function. This is the bridge between
* evaluateSpaceAccess() (layer 1: "can they enter?") and
* hasCapability() (layer 3: "what can they do?").
*/
import type { ResolvedRole, SpaceMembership } from '../types/roles.js';
import type { SpaceAuthResult } from '../types/index.js';
import { SpaceVisibility } from '../types/index.js';
export interface RoleResolverOptions {
/**
* Look up membership for a DID in a space.
* You provide the DB query works with Prisma, Automerge, raw SQL, etc.
* Return null if no membership found.
*/
getMembership: (userDID: string, spaceSlug: string) => Promise<SpaceMembership | null>;
/** The space's visibility setting */
visibility: SpaceVisibility;
}
export interface RemoteResolverOptions {
/** EncryptID server URL (e.g., https://encryptid.jeffemmett.com) */
serverUrl: string;
/** The space's visibility setting */
visibility: SpaceVisibility;
/** Cache TTL in milliseconds (default: 5 minutes) */
cacheTtlMs?: number;
}
/**
* Invalidate the role cache for a specific user/space or the entire cache.
*/
export declare function invalidateRoleCache(userDID?: string, spaceSlug?: string): void;
/**
* Create a getMembership function that calls the EncryptID server.
* This is a convenience wrapper for modules that don't have local membership data.
*
* @example
* ```ts
* const role = await resolveSpaceRole(spaceAuth, slug, {
* visibility: 'public',
* getMembership: createRemoteMembershipLookup('https://encryptid.jeffemmett.com'),
* });
* ```
*/
export declare function createRemoteMembershipLookup(serverUrl: string): (userDID: string, spaceSlug: string) => Promise<SpaceMembership | null>;
/**
* Resolve a user's effective SpaceRole in a space.
*
* Decision flow:
* 1. Owner ADMIN (always)
* 2. Has explicit membership membership.role
* 3. No membership, apply defaults based on visibility:
* - PUBLIC: anonymous & authenticated PARTICIPANT
* - PUBLIC_READ: anonymous VIEWER, authenticated PARTICIPANT
* - AUTHENTICATED: VIEWER (must have membership for more)
* - MEMBERS_ONLY: should not reach here (denied at space access layer)
*
* @param spaceAuth - Result from evaluateSpaceAccess()
* @param spaceSlug - The space identifier
* @param options - Membership lookup and visibility config
*
* @example
* ```ts
* const spaceAuth = await evaluateSpaceAccess(slug, token, method, opts);
* if (!spaceAuth.allowed) return deny();
*
* const { role, source } = await resolveSpaceRole(spaceAuth, slug, {
* visibility: space.visibility,
* getMembership: (did, slug) => db.membership.findUnique({ where: { did_slug: { did, slug } } }),
* });
*
* if (hasCapability(role, 'create_proposal', RVOTE_PERMISSIONS)) { ... }
* ```
*/
export declare function resolveSpaceRole(spaceAuth: SpaceAuthResult, spaceSlug: string, options: RoleResolverOptions): Promise<ResolvedRole>;
/**
* Resolve a user's SpaceRole by querying the EncryptID server.
*
* This is the recommended function for modules that don't maintain
* their own membership table. It:
* 1. Checks the in-memory cache (5-min TTL)
* 2. If miss, queries EncryptID server for membership
* 3. Falls back to visibility-based defaults on network error
*
* @example
* ```ts
* const { role, source } = await resolveSpaceRoleRemote(spaceAuth, slug, {
* serverUrl: 'https://encryptid.jeffemmett.com',
* visibility: space.visibility,
* });
* ```
*/
export declare function resolveSpaceRoleRemote(spaceAuth: SpaceAuthResult, spaceSlug: string, options: RemoteResolverOptions): Promise<ResolvedRole>;

View File

@ -0,0 +1,35 @@
/**
* EncryptID Space Auth Guard
*
* Framework-agnostic space-aware authentication.
* Evaluates whether a request should be allowed based on:
* 1. Space visibility configuration
* 2. Request method (GET/HEAD/OPTIONS = read, others = write)
* 3. EncryptID session (if present)
*/
import { type VerifyOptions } from './jwt-verify.js';
import { SpaceVisibility } from '../types/index.js';
import type { SpaceAuthConfig, SpaceAuthResult } from '../types/index.js';
export { SpaceVisibility };
export type { SpaceAuthConfig, SpaceAuthResult };
export interface SpaceAuthOptions extends VerifyOptions {
/** Resolve a space slug to its auth config. You provide the DB/store query. */
getSpaceConfig: (spaceSlug: string) => Promise<SpaceAuthConfig | null>;
}
/**
* Core space auth evaluation framework-agnostic.
*
* Apps call this with the space slug, the extracted token (or null),
* the HTTP method, and a callback to look up the space's config.
*/
export declare function evaluateSpaceAccess(spaceSlug: string, token: string | null, method: string, options: SpaceAuthOptions): Promise<SpaceAuthResult>;
/**
* Extract EncryptID token from request headers or cookies.
* Works with both the standard Headers API (fetch/Hono/Next.js) and
* Express-style header objects.
*/
export declare function extractToken(headers: {
get?: (name: string) => string | null | undefined;
authorization?: string;
cookie?: string;
}): string | null;

View File

@ -0,0 +1,13 @@
import {
evaluateSpaceAccess,
extractToken
} from "../index-j6kh1974.js";
import {
SpaceVisibility
} from "../index-5c1t4ftn.js";
import"../index-stg63j73.js";
export {
extractToken,
evaluateSpaceAccess,
SpaceVisibility
};

View File

@ -0,0 +1,18 @@
/**
* EncryptID WebSocket Authentication
*
* Since WebSocket upgrade requests carry the initial HTTP headers,
* we verify the token during the upgrade handshake.
*
* Supported token locations (checked in order):
* 1. `token` query parameter: ws://host/ws?token=xxx
* 2. Sec-WebSocket-Protocol subprotocol: "encryptid.TOKEN_HERE"
* 3. Cookie: encryptid_token=xxx
*/
import { type VerifyOptions } from './jwt-verify.js';
import type { EncryptIDClaims } from '../types/index.js';
/**
* Authenticate a WebSocket upgrade request.
* Returns claims if a valid token is found, null otherwise.
*/
export declare function authenticateWSUpgrade(request: Request, options?: VerifyOptions): Promise<EncryptIDClaims | null>;

View File

@ -0,0 +1,7 @@
import {
authenticateWSUpgrade
} from "../index-2yszamrn.js";
import"../index-stg63j73.js";
export {
authenticateWSUpgrade
};

View File

@ -0,0 +1,246 @@
/**
* EncryptID SDK Shared Types
*/
export interface EncryptIDCredential {
credentialId: string;
publicKey: ArrayBuffer;
userId: string;
username: string;
createdAt: number;
prfSupported: boolean;
transports?: AuthenticatorTransport[];
}
export interface AuthenticationResult {
credentialId: string;
userId: string;
prfOutput?: ArrayBuffer;
signature: ArrayBuffer;
authenticatorData: ArrayBuffer;
}
export interface EncryptIDConfig {
rpId: string;
rpName: string;
origin: string;
userVerification: UserVerificationRequirement;
timeout: number;
}
export interface WebAuthnCapabilities {
webauthn: boolean;
platformAuthenticator: boolean;
conditionalUI: boolean;
prfExtension: boolean;
}
export interface DerivedKeys {
encryptionKey: CryptoKey;
signingKeyPair: CryptoKeyPair;
didSeed: Uint8Array;
did: string;
fromPRF: boolean;
/** Ethereum-compatible secp256k1 wallet derived from the same master key */
ethereum?: {
address: string;
publicKey: Uint8Array;
privateKey: Uint8Array;
};
}
export interface EncryptedData {
ciphertext: ArrayBuffer;
iv: Uint8Array;
tag?: ArrayBuffer;
}
export interface SignedData {
data: ArrayBuffer;
signature: ArrayBuffer;
publicKey: ArrayBuffer;
}
export declare enum AuthLevel {
BASIC = 1,
STANDARD = 2,
ELEVATED = 3,
CRITICAL = 4
}
export interface EncryptIDClaims {
iss: string;
sub: string;
aud: string[];
iat: number;
exp: number;
jti: string;
username: string;
did?: string;
eid: {
walletAddress?: string;
credentialId?: string;
authLevel: AuthLevel;
authTime: number;
capabilities: {
encrypt: boolean;
sign: boolean;
wallet: boolean;
};
recoveryConfigured: boolean;
};
}
export interface SessionState {
accessToken: string;
refreshToken: string;
claims: EncryptIDClaims;
lastAuthTime: number;
}
export interface OperationPermission {
minAuthLevel: AuthLevel;
requiresCapability?: 'encrypt' | 'sign' | 'wallet';
maxAgeSeconds?: number;
}
export declare enum GuardianType {
SECONDARY_PASSKEY = "secondary_passkey",
TRUSTED_CONTACT = "trusted_contact",
HARDWARE_KEY = "hardware_key",
INSTITUTIONAL = "institutional",
TIME_DELAYED_SELF = "time_delayed_self"
}
export interface Guardian {
id: string;
type: GuardianType;
name: string;
weight: number;
credentialId?: string;
contactDID?: string;
contactEmail?: string;
serviceUrl?: string;
delaySeconds?: number;
addedAt: number;
lastVerified?: number;
}
export interface RecoveryConfig {
threshold: number;
delaySeconds: number;
guardians: Guardian[];
guardianListHash: string;
updatedAt: number;
}
export interface RecoveryRequest {
id: string;
accountDID: string;
newCredentialId: string;
initiatedAt: number;
completesAt: number;
status: 'pending' | 'approved' | 'cancelled' | 'completed';
approvals: {
guardianId: string;
approvedAt: number;
signature: string;
}[];
approvalWeight: number;
}
export interface RegistrationStartResponse {
options: {
challenge: string;
rp: {
id: string;
name: string;
};
user: {
id: string;
name: string;
displayName: string;
};
pubKeyCredParams: {
alg: number;
type: string;
}[];
authenticatorSelection: Record<string, unknown>;
timeout: number;
attestation: string;
extensions?: Record<string, unknown>;
};
userId: string;
}
export interface RegistrationCompleteResponse {
success: boolean;
userId: string;
token: string;
did: string;
}
export interface AuthStartResponse {
options: {
challenge: string;
rpId: string;
userVerification: string;
timeout: number;
allowCredentials?: {
type: string;
id: string;
transports?: string[];
}[];
};
}
export interface AuthCompleteResponse {
success: boolean;
userId: string;
username: string;
token: string;
did: string;
}
export interface SessionVerifyResponse {
valid: boolean;
userId?: string;
username?: string;
did?: string;
exp?: number;
error?: string;
}
export interface EmailRecoverySetResponse {
success: boolean;
email: string;
}
export interface EmailRecoveryRequestResponse {
success: boolean;
message: string;
}
export interface EmailRecoveryVerifyResponse {
success: boolean;
token: string;
userId: string;
username: string;
did: string;
message: string;
}
export declare enum SpaceVisibility {
/** Anyone can view and interact, no auth required */
PUBLIC = "public",
/** Anyone can view, auth required for write/interact */
PUBLIC_READ = "public_read",
/** Auth required for any access */
AUTHENTICATED = "authenticated",
/** Only space members can access (app must check membership separately) */
MEMBERS_ONLY = "members_only"
}
export type AppName = 'rspace' | 'rvote' | 'rfiles' | 'rmaps' | 'rwallet' | 'rfunds' | 'rnotes' | 'rtrips' | 'rnetwork' | 'rcart' | 'rmail' | 'rcal' | 'rtube' | 'rstack' | 'canvas';
export { SpaceRole, SPACE_ROLE_LEVEL, roleAtLeast } from './roles.js';
export type { SpaceMembership, ResolvedRole } from './roles.js';
export { hasCapability, getCapabilities } from './module-permissions.js';
export type { ModulePermissionMap } from './module-permissions.js';
export type { SpaceMembershipEvent, SpaceMembershipEventType, AddMemberRequest, UpdateMemberRequest, MemberResponse, MemberListResponse, } from './membership-events.js';
export interface SpaceAuthConfig {
/** Space identifier (slug) */
spaceSlug: string;
/** Who can see/interact with this space */
visibility: SpaceVisibility;
/** DID of the space creator/owner */
ownerDID?: string;
/** App this space belongs to */
app: AppName;
}
export interface SpaceAuthResult {
/** Whether access is allowed */
allowed: boolean;
/** The authenticated user's claims (null if unauthenticated public access) */
claims: EncryptIDClaims | null;
/** Why access was denied */
reason?: string;
/** Whether the user is the space owner */
isOwner: boolean;
/** Whether this is read-only access (public_read with no auth) */
readOnly: boolean;
}

View File

@ -0,0 +1,10 @@
import {
AuthLevel,
GuardianType,
SpaceVisibility
} from "../index-5c1t4ftn.js";
export {
SpaceVisibility,
GuardianType,
AuthLevel
};

View File

@ -0,0 +1,49 @@
/**
* EncryptID SDK Space Membership Event Types
*
* Events emitted when membership changes in a space.
* Used by the membership sync system to keep all r*.online
* modules consistent when a user's role changes.
*/
import type { SpaceRole } from './roles.js';
export type SpaceMembershipEventType = 'member.joined' | 'member.left' | 'member.role_changed';
/**
* Emitted by EncryptID server when space membership changes.
* Modules can subscribe to these events to invalidate role caches.
*/
export interface SpaceMembershipEvent {
type: SpaceMembershipEventType;
/** Space identifier */
spaceSlug: string;
/** DID of the user whose membership changed */
userDID: string;
/** New role (undefined for member.left) */
role?: SpaceRole;
/** Previous role (undefined for member.joined) */
previousRole?: SpaceRole;
/** DID of user who initiated the change */
changedBy?: string;
/** Unix timestamp (ms) */
timestamp: number;
}
export interface AddMemberRequest {
/** DID of user to add */
userDID: string;
/** Role to grant */
role: SpaceRole;
}
export interface UpdateMemberRequest {
/** New role */
role: SpaceRole;
}
export interface MemberResponse {
userDID: string;
spaceSlug: string;
role: SpaceRole;
joinedAt: number;
grantedBy?: string;
}
export interface MemberListResponse {
members: MemberResponse[];
total: number;
}

View File

@ -0,0 +1,53 @@
/**
* EncryptID SDK Module Permission Maps
*
* Each r*.online module declares a static mapping from capabilities
* to the minimum SpaceRole required. This is the central abstraction
* for permission inheritance across the ecosystem.
*/
import { SpaceRole } from './roles.js';
import type { AppName } from './index.js';
/**
* A module's permission declaration.
* Maps each capability string to the minimum SpaceRole required.
*
* @template TCapability - Union of capability string literals for this module
*/
export interface ModulePermissionMap<TCapability extends string = string> {
/** Module identifier (matches AppName) */
module: AppName;
/** Human-readable module name */
displayName: string;
/**
* For each capability, the minimum SpaceRole required.
* If a capability is not listed, it requires ADMIN by default.
*/
capabilities: Record<TCapability, SpaceRole>;
}
/**
* Check if a user's SpaceRole satisfies a module capability requirement.
*
* @param userRole - The user's resolved SpaceRole in the space
* @param capability - The capability to check
* @param permMap - The module's permission map
* @returns true if the user's role meets or exceeds the minimum for this capability
*
* @example
* ```ts
* import { hasCapability } from '@encryptid/sdk';
* import { RVOTE_PERMISSIONS } from '@encryptid/sdk/types/modules';
*
* if (hasCapability(userRole, 'create_proposal', RVOTE_PERMISSIONS)) {
* // user can create proposals
* }
* ```
*/
export declare function hasCapability<T extends string>(userRole: SpaceRole, capability: T, permMap: ModulePermissionMap<T>): boolean;
/**
* Get all capabilities a role has access to in a module.
*
* @param userRole - The user's resolved SpaceRole
* @param permMap - The module's permission map
* @returns Array of capability strings the user has access to
*/
export declare function getCapabilities<T extends string>(userRole: SpaceRole, permMap: ModulePermissionMap<T>): T[];

View File

@ -0,0 +1,15 @@
/**
* Module Permission Maps Barrel Export
*
* Import specific module permissions:
* import { RVOTE_PERMISSIONS } from '@encryptid/sdk/types/modules';
*
* Or import all:
* import * as modules from '@encryptid/sdk/types/modules';
*/
export { RSPACE_PERMISSIONS, type RSpaceCapability } from './rspace.js';
export { RVOTE_PERMISSIONS, type RVoteCapability } from './rvote.js';
export { RNOTES_PERMISSIONS, type RNotesCapability } from './rnotes.js';
export { RFUNDS_PERMISSIONS, type RFundsCapability } from './rfunds.js';
export { RMAPS_PERMISSIONS, type RMapsCapability } from './rmaps.js';
export { RTUBE_PERMISSIONS, type RTubeCapability } from './rtube.js';

View File

@ -0,0 +1,8 @@
/**
* rFunds Funding Flows & Treasury
*
* Permission capabilities for the rFunds funding/treasury module.
*/
import type { ModulePermissionMap } from '../module-permissions.js';
export type RFundsCapability = 'view_flows' | 'create_flow' | 'contribute_funds' | 'moderate_flows' | 'configure_treasury';
export declare const RFUNDS_PERMISSIONS: ModulePermissionMap<RFundsCapability>;

View File

@ -0,0 +1,8 @@
/**
* rMaps Spatial Intelligence
*
* Permission capabilities for the rMaps collaborative mapping module.
*/
import type { ModulePermissionMap } from '../module-permissions.js';
export type RMapsCapability = 'view_map' | 'add_markers' | 'share_location' | 'moderate_markers' | 'configure_map';
export declare const RMAPS_PERMISSIONS: ModulePermissionMap<RMapsCapability>;

View File

@ -0,0 +1,10 @@
/**
* rNotes Collaborative Notebooks
*
* Permission capabilities for the rNotes note-taking module.
* Note: rNotes also has per-notebook CollaboratorRole overrides.
* Space-level role sets the default; notebook-level can narrow or widen.
*/
import type { ModulePermissionMap } from '../module-permissions.js';
export type RNotesCapability = 'view_notebooks' | 'create_notebook' | 'edit_own_notes' | 'edit_any_notes' | 'manage_notebooks';
export declare const RNOTES_PERMISSIONS: ModulePermissionMap<RNotesCapability>;

View File

@ -0,0 +1,8 @@
/**
* rSpace Canvas/Collaboration Platform
*
* Permission capabilities for the rSpace collaborative canvas.
*/
import type { ModulePermissionMap } from '../module-permissions.js';
export type RSpaceCapability = 'view_canvas' | 'add_shapes' | 'edit_own_shapes' | 'edit_any_shape' | 'delete_any_shape' | 'configure_space';
export declare const RSPACE_PERMISSIONS: ModulePermissionMap<RSpaceCapability>;

View File

@ -0,0 +1,8 @@
/**
* rTube Video Hosting & Streaming
*
* Permission capabilities for the rTube video module.
*/
import type { ModulePermissionMap } from '../module-permissions.js';
export type RTubeCapability = 'view_videos' | 'upload_video' | 'start_stream' | 'moderate_videos' | 'configure_channel';
export declare const RTUBE_PERMISSIONS: ModulePermissionMap<RTubeCapability>;

View File

@ -0,0 +1,8 @@
/**
* rVote Decision Engine
*
* Permission capabilities for the rVote voting/governance module.
*/
import type { ModulePermissionMap } from '../module-permissions.js';
export type RVoteCapability = 'view_proposals' | 'create_proposal' | 'cast_vote' | 'moderate_proposals' | 'configure_voting';
export declare const RVOTE_PERMISSIONS: ModulePermissionMap<RVoteCapability>;

View File

@ -0,0 +1,57 @@
/**
* EncryptID SDK Space Role System
*
* Defines the unified role hierarchy for the r*.online ecosystem.
* Spaces have visibility (who can enter) and roles (what you can do).
*/
/**
* Ecosystem-wide role within a space.
* These are the ONLY roles stored at the space level.
* Modules interpret these into module-specific capabilities.
*/
export declare enum SpaceRole {
/** Can view public content in the space */
VIEWER = "viewer",
/** Can participate: create, edit own content */
PARTICIPANT = "participant",
/** Can moderate: edit/delete others' content, manage participants */
MODERATOR = "moderator",
/** Full control: configure space, manage roles, delete space */
ADMIN = "admin"
}
/**
* Ordered precedence (higher number = more powerful).
* Used by hasCapability() and role comparison helpers.
*/
export declare const SPACE_ROLE_LEVEL: Record<SpaceRole, number>;
/**
* Check if a role meets or exceeds a required minimum role.
*/
export declare function roleAtLeast(userRole: SpaceRole, requiredRole: SpaceRole): boolean;
/**
* Space membership record.
* Each app stores memberships in its own DB (Prisma, Automerge, etc.)
* but the shape is consistent across the ecosystem.
*/
export interface SpaceMembership {
/** User's DID (from EncryptID claims.sub) */
userDID: string;
/** Space identifier (slug) */
spaceSlug: string;
/** Role in this space */
role: SpaceRole;
/** When the membership was granted (epoch ms) */
joinedAt: number;
/** DID of user who granted this membership (null = self-join or owner) */
grantedBy?: string;
}
/**
* Result of resolving a user's effective role in a space.
* Includes the source for debugging and audit.
*/
export interface ResolvedRole {
/** The effective role */
role: SpaceRole;
/** How the role was determined */
source: 'membership' | 'owner' | 'default' | 'anonymous';
}

View File

@ -0,0 +1,12 @@
/**
* EncryptID Guardian Setup Web Component
*
* Custom element: <encryptid-guardian-setup>
* Events: guardian-added, guardian-removed, guardian-verified
*/
export declare class GuardianSetupElement extends HTMLElement {
private shadow;
constructor();
connectedCallback(): void;
private render;
}

View File

@ -0,0 +1,5 @@
/**
* @encryptid/sdk/ui Web Components
*/
export { EncryptIDLoginButton } from './login-button.js';
export { GuardianSetupElement } from './guardian-setup.js';

View File

@ -0,0 +1,263 @@
import {
getGuardianTypeInfo,
getKeyManager,
getRecoveryManager,
getSessionManager
} from "../index-24r9wkfe.js";
import {
authenticatePasskey,
detectCapabilities,
registerPasskey,
startConditionalUI
} from "../index-2cp5044h.js";
import {
AuthLevel
} from "../index-5c1t4ftn.js";
// src/ui/login-button.ts
var PASSKEY_ICON = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="passkey-icon"><circle cx="12" cy="10" r="3"/><path d="M12 13v8"/><path d="M9 18h6"/><circle cx="12" cy="10" r="7"/></svg>`;
var styles = `
:host { --eid-primary: #06b6d4; --eid-primary-hover: #0891b2; --eid-bg: #0f172a; --eid-bg-hover: #1e293b; --eid-text: #f1f5f9; --eid-text-secondary: #94a3b8; --eid-radius: 8px; display: inline-block; font-family: system-ui, -apple-system, sans-serif; }
.login-btn { display: flex; align-items: center; gap: 12px; padding: 12px 24px; background: var(--eid-primary); color: white; border: none; border-radius: var(--eid-radius); font-size: 1rem; font-weight: 500; cursor: pointer; transition: all 0.2s; box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.3); }
.login-btn:hover { background: var(--eid-primary-hover); transform: translateY(-1px); }
.login-btn:disabled { opacity: 0.6; cursor: not-allowed; transform: none; }
.login-btn.outline { background: transparent; border: 2px solid var(--eid-primary); color: var(--eid-primary); }
.login-btn.outline:hover { background: var(--eid-primary); color: white; }
.login-btn.small { padding: 8px 16px; font-size: 0.875rem; }
.login-btn.large { padding: 16px 32px; font-size: 1.125rem; }
.passkey-icon { width: 24px; height: 24px; }
.user-info { display: flex; align-items: center; gap: 12px; padding: 8px 16px; background: var(--eid-bg); border-radius: var(--eid-radius); color: var(--eid-text); cursor: pointer; }
.user-avatar { width: 36px; height: 36px; border-radius: 50%; background: var(--eid-primary); display: flex; align-items: center; justify-content: center; font-weight: 600; font-size: 0.875rem; }
.user-did { font-size: 0.75rem; color: var(--eid-text-secondary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 150px; }
.auth-level { font-size: 0.625rem; padding: 2px 6px; border-radius: 4px; }
.auth-level.elevated { background: rgba(34, 197, 94, 0.2); color: #22c55e; }
.auth-level.standard { background: rgba(234, 179, 8, 0.2); color: #eab308; }
.dropdown { position: absolute; top: 100%; right: 0; margin-top: 8px; background: var(--eid-bg); border-radius: var(--eid-radius); box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.3); min-width: 200px; z-index: 100; overflow: hidden; }
.dropdown-item { display: flex; align-items: center; gap: 12px; padding: 12px 16px; color: var(--eid-text); cursor: pointer; transition: background 0.2s; }
.dropdown-item:hover { background: var(--eid-bg-hover); }
.dropdown-item.danger { color: #ef4444; }
.dropdown-divider { height: 1px; background: #334155; margin: 4px 0; }
.loading-spinner { width: 20px; height: 20px; border: 2px solid transparent; border-top-color: currentColor; border-radius: 50%; animation: spin 0.8s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }
`;
class EncryptIDLoginButton extends HTMLElement {
shadow;
loading = false;
showDropdown = false;
capabilities = null;
static get observedAttributes() {
return ["size", "variant", "label", "show-user"];
}
constructor() {
super();
this.shadow = this.attachShadow({ mode: "open" });
}
async connectedCallback() {
this.capabilities = await detectCapabilities();
if (this.capabilities.conditionalUI)
this.startConditionalAuth();
this.render();
document.addEventListener("click", (e) => {
if (!this.contains(e.target)) {
this.showDropdown = false;
this.render();
}
});
}
attributeChangedCallback() {
this.render();
}
get size() {
return this.getAttribute("size") || "medium";
}
get variant() {
return this.getAttribute("variant") || "primary";
}
get label() {
return this.getAttribute("label") || "Sign in with Passkey";
}
get showUser() {
return this.hasAttribute("show-user");
}
render() {
const session = getSessionManager();
const isLoggedIn = session.isValid();
const did = session.getDID();
const authLevel = session.getAuthLevel();
this.shadow.innerHTML = `<style>${styles}</style><div class="login-container" style="position:relative">
${isLoggedIn && this.showUser ? this.renderUserInfo(did, authLevel) : this.renderLoginButton()}
${this.showDropdown ? this.renderDropdown() : ""}
</div>`;
this.attachEventListeners();
}
renderLoginButton() {
const sizeClass = this.size === "medium" ? "" : this.size;
const variantClass = this.variant === "primary" ? "" : this.variant;
return `<button class="login-btn ${sizeClass} ${variantClass}" ${this.loading ? "disabled" : ""}>
${this.loading ? '<div class="loading-spinner"></div>' : PASSKEY_ICON}
<span>${this.loading ? "Authenticating..." : this.label}</span></button>`;
}
renderUserInfo(did, authLevel) {
const shortDID = did.slice(0, 20) + "..." + did.slice(-8);
const initial = did.slice(8, 10).toUpperCase();
const levelName = AuthLevel[authLevel].toLowerCase();
return `<div class="user-info"><div class="user-avatar">${initial}</div>
<div><div class="user-did">${shortDID}</div><span class="auth-level ${levelName}">${levelName}</span></div></div>`;
}
renderDropdown() {
return `<div class="dropdown">
<div class="dropdown-item" data-action="profile">Profile</div>
<div class="dropdown-item" data-action="recovery">Recovery Settings</div>
<div class="dropdown-item" data-action="upgrade">Upgrade Auth Level</div>
<div class="dropdown-divider"></div>
<div class="dropdown-item danger" data-action="logout">Sign Out</div></div>`;
}
attachEventListeners() {
const session = getSessionManager();
if (session.isValid() && this.showUser) {
this.shadow.querySelector(".user-info")?.addEventListener("click", () => {
this.showDropdown = !this.showDropdown;
this.render();
});
this.shadow.querySelectorAll(".dropdown-item").forEach((item) => {
item.addEventListener("click", (e) => {
e.stopPropagation();
this.handleDropdownAction(item.dataset.action);
});
});
} else {
this.shadow.querySelector(".login-btn")?.addEventListener("click", () => this.handleLogin());
}
}
async handleLogin() {
if (this.loading)
return;
this.loading = true;
this.render();
try {
const result = await authenticatePasskey();
const keyManager = getKeyManager();
if (result.prfOutput)
await keyManager.initFromPRF(result.prfOutput);
const keys = await keyManager.getKeys();
await getSessionManager().createSession(result, keys.did, { encrypt: true, sign: true, wallet: false });
this.dispatchEvent(new CustomEvent("login-success", { detail: { did: keys.did, credentialId: result.credentialId, prfAvailable: !!result.prfOutput }, bubbles: true }));
} catch (error) {
if (error.name === "NotAllowedError" || error.message?.includes("No credential")) {
this.dispatchEvent(new CustomEvent("login-register-needed", { bubbles: true }));
} else {
this.dispatchEvent(new CustomEvent("login-error", { detail: { error: error.message }, bubbles: true }));
}
} finally {
this.loading = false;
this.render();
}
}
async handleDropdownAction(action) {
this.showDropdown = false;
if (action === "logout") {
getSessionManager().clearSession();
getKeyManager().clear();
this.dispatchEvent(new CustomEvent("logout", { bubbles: true }));
} else if (action === "upgrade") {
try {
await authenticatePasskey();
getSessionManager().upgradeAuthLevel(3 /* ELEVATED */);
this.dispatchEvent(new CustomEvent("auth-upgraded", { detail: { level: 3 /* ELEVATED */ }, bubbles: true }));
} catch {}
} else {
this.dispatchEvent(new CustomEvent("navigate", { detail: { path: `/${action}` }, bubbles: true }));
}
this.render();
}
async startConditionalAuth() {
try {
const result = await startConditionalUI();
if (result) {
const keyManager = getKeyManager();
if (result.prfOutput)
await keyManager.initFromPRF(result.prfOutput);
const keys = await keyManager.getKeys();
await getSessionManager().createSession(result, keys.did, { encrypt: true, sign: true, wallet: false });
this.dispatchEvent(new CustomEvent("login-success", { detail: { did: keys.did, credentialId: result.credentialId, viaConditionalUI: true }, bubbles: true }));
this.render();
}
} catch {}
}
async register(username, displayName) {
this.loading = true;
this.render();
try {
const credential = await registerPasskey(username, displayName);
this.dispatchEvent(new CustomEvent("register-success", { detail: { credentialId: credential.credentialId, prfSupported: credential.prfSupported }, bubbles: true }));
await this.handleLogin();
} catch (error) {
this.dispatchEvent(new CustomEvent("register-error", { detail: { error: error.message }, bubbles: true }));
} finally {
this.loading = false;
this.render();
}
}
}
customElements.define("encryptid-login", EncryptIDLoginButton);
// src/ui/guardian-setup.ts
class GuardianSetupElement extends HTMLElement {
shadow;
constructor() {
super();
this.shadow = this.attachShadow({ mode: "open" });
}
connectedCallback() {
const manager = getRecoveryManager();
const config = manager.getConfig();
if (!config) {
manager.initializeRecovery(3).then(() => this.render());
} else {
this.render();
}
}
render() {
const manager = getRecoveryManager();
const config = manager.getConfig();
const guardians = config?.guardians ?? [];
const threshold = config?.threshold ?? 3;
const totalWeight = guardians.reduce((sum, g) => sum + g.weight, 0);
const isConfigured = totalWeight >= threshold;
this.shadow.innerHTML = `
<style>
:host { display: block; font-family: system-ui, sans-serif; color: #f1f5f9; }
.setup { background: #0f172a; border-radius: 8px; padding: 24px; max-width: 600px; }
h2 { margin: 0 0 8px; font-size: 1.5rem; }
.subtitle { color: #94a3b8; font-size: 0.875rem; margin: 0 0 24px; }
.status { display: flex; align-items: center; gap: 16px; padding: 16px; background: #1e293b; border-radius: 8px; margin-bottom: 24px; }
.dot { width: 12px; height: 12px; border-radius: 50%; background: ${isConfigured ? "#22c55e" : "#eab308"}; }
.guardian { display: flex; align-items: center; gap: 16px; padding: 16px; background: #1e293b; border-radius: 8px; margin-bottom: 12px; border: 1px solid #475569; }
.icon { width: 48px; height: 48px; border-radius: 50%; background: #334155; display: flex; align-items: center; justify-content: center; font-size: 1.25rem; }
.info { flex: 1; }
.name { font-weight: 500; }
.type { font-size: 0.75rem; color: #94a3b8; }
</style>
<div class="setup">
<h2>Social Recovery</h2>
<p class="subtitle">Set up guardians to recover your account without seed phrases</p>
<div class="status">
<div class="dot"></div>
<div>
<div>${isConfigured ? "Recovery Configured" : "Setup Incomplete"}</div>
<div style="font-size:0.75rem;color:#94a3b8">${totalWeight}/${threshold} guardians</div>
</div>
</div>
${guardians.map((g) => {
const info = getGuardianTypeInfo(g.type);
return `<div class="guardian"><div class="icon">${info.icon === "key" ? "\uD83D\uDD11" : info.icon === "user" ? "\uD83D\uDC64" : info.icon === "shield" ? "\uD83D\uDEE1" : info.icon === "building" ? "\uD83C\uDFE2" : "⏰"}</div><div class="info"><div class="name">${g.name}</div><div class="type">${info.name}</div></div></div>`;
}).join("")}
</div>
`;
}
}
customElements.define("encryptid-guardian-setup", GuardianSetupElement);
export {
GuardianSetupElement,
EncryptIDLoginButton
};

View File

@ -0,0 +1,30 @@
/**
* EncryptID Login Button Web Component
*
* Custom element: <encryptid-login>
* Attributes: size (small|medium|large), variant (primary|outline), label, show-user
* Events: login-success, login-error, login-register-needed, logout, auth-upgraded
*/
export declare class EncryptIDLoginButton extends HTMLElement {
private shadow;
private loading;
private showDropdown;
private capabilities;
static get observedAttributes(): string[];
constructor();
connectedCallback(): Promise<void>;
attributeChangedCallback(): void;
private get size();
private get variant();
private get label();
private get showUser();
private render;
private renderLoginButton;
private renderUserInfo;
private renderDropdown;
private attachEventListeners;
private handleLogin;
private handleDropdownAction;
private startConditionalAuth;
register(username: string, displayName: string): Promise<void>;
}

View File

@ -0,0 +1,39 @@
/**
* EncryptID React Context Provider
*
* Wraps your app to provide EncryptID auth state to all components.
* Features: localStorage + cookie persistence, auto-refresh, session verification on mount.
*/
import React, { type ReactNode } from 'react';
import { EncryptIDClient } from '../../client/api-client.js';
import type { EncryptIDClaims } from '../../types/index.js';
interface EncryptIDContextValue {
/** Whether the user is authenticated */
isAuthenticated: boolean;
/** JWT token (null if not authenticated) */
token: string | null;
/** Decoded claims (null if not authenticated) */
claims: EncryptIDClaims | null;
/** User's DID (null if not authenticated) */
did: string | null;
/** Username from EncryptID */
username: string | null;
/** Whether auth state is being loaded */
loading: boolean;
/** Full registration + authentication flow */
register: (username: string, displayName?: string) => Promise<void>;
/** Full authentication flow */
login: (credentialId?: string) => Promise<void>;
/** Clear session */
logout: () => void;
/** The EncryptID API client */
client: EncryptIDClient;
}
interface EncryptIDProviderProps {
children: ReactNode;
/** EncryptID server URL (default: https://encryptid.jeffemmett.com) */
serverUrl?: string;
}
export declare function EncryptIDProvider({ children, serverUrl }: EncryptIDProviderProps): React.FunctionComponentElement<React.ProviderProps<EncryptIDContextValue | null>>;
export declare function useEncryptID(): EncryptIDContextValue;
export {};

View File

@ -0,0 +1,32 @@
/**
* EncryptID Login Button React Component
*
* Wraps the EncryptID client for easy React integration.
*/
import React from 'react';
interface LoginButtonProps {
/** Button label (default: "Sign in with Passkey") */
label?: string;
/** Button size */
size?: 'small' | 'medium' | 'large';
/** Visual variant */
variant?: 'primary' | 'outline';
/** Callback after successful login */
onSuccess?: (result: {
token: string;
did: string;
}) => void;
/** Callback on error */
onError?: (error: Error) => void;
/** Callback when registration is needed */
onRegisterNeeded?: () => void;
/** Additional CSS class */
className?: string;
}
export declare function LoginButton({ label, size, variant, onSuccess, onError, onRegisterNeeded, className, }: LoginButtonProps): React.DetailedReactHTMLElement<{
onClick: () => Promise<void>;
disabled: boolean;
style: React.CSSProperties;
className: string;
}, HTMLElement>;
export {};

View File

@ -0,0 +1,5 @@
/**
* @encryptid/sdk/ui/react React Components
*/
export { EncryptIDProvider, useEncryptID } from './EncryptIDProvider.js';
export { LoginButton } from './LoginButton.js';

View File

@ -0,0 +1,160 @@
import {
EncryptIDClient
} from "../../index-7egxprg9.js";
import"../../index-2cp5044h.js";
// src/ui/react/EncryptIDProvider.tsx
import React, { createContext, useContext, useEffect, useState, useCallback } from "react";
var EncryptIDContext = createContext(null);
var TOKEN_KEY = "encryptid_token";
function EncryptIDProvider({ children, serverUrl }) {
const [client] = useState(() => new EncryptIDClient(serverUrl));
const [token, setToken] = useState(null);
const [claims, setClaims] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const stored = localStorage.getItem(TOKEN_KEY);
if (stored) {
client.verifySession(stored).then((res) => {
if (res.valid) {
setToken(stored);
try {
const payload = JSON.parse(atob(stored.split(".")[1]));
setClaims(payload);
} catch {}
} else {
localStorage.removeItem(TOKEN_KEY);
}
}).catch(() => {
localStorage.removeItem(TOKEN_KEY);
}).finally(() => setLoading(false));
} else {
setLoading(false);
}
}, [client]);
const register = useCallback(async (username, displayName) => {
setLoading(true);
try {
const result = await client.register(username, displayName);
setToken(result.token);
localStorage.setItem(TOKEN_KEY, result.token);
try {
setClaims(JSON.parse(atob(result.token.split(".")[1])));
} catch {}
} finally {
setLoading(false);
}
}, [client]);
const login = useCallback(async (credentialId) => {
setLoading(true);
try {
const result = await client.authenticate(credentialId);
setToken(result.token);
localStorage.setItem(TOKEN_KEY, result.token);
try {
setClaims(JSON.parse(atob(result.token.split(".")[1])));
} catch {}
} finally {
setLoading(false);
}
}, [client]);
const logout = useCallback(() => {
setToken(null);
setClaims(null);
localStorage.removeItem(TOKEN_KEY);
localStorage.removeItem("encryptid_session");
}, []);
const value = {
isAuthenticated: !!token,
token,
claims,
did: claims?.did || claims?.sub || null,
username: claims?.username || null,
loading,
register,
login,
logout,
client
};
return React.createElement(EncryptIDContext.Provider, { value }, children);
}
function useEncryptID() {
const ctx = useContext(EncryptIDContext);
if (!ctx)
throw new Error("useEncryptID must be used within <EncryptIDProvider>");
return ctx;
}
// src/ui/react/LoginButton.tsx
import React2, { useState as useState2, useCallback as useCallback2 } from "react";
function LoginButton({
label = "Sign in with Passkey",
size = "medium",
variant = "primary",
onSuccess,
onError,
onRegisterNeeded,
className = ""
}) {
const { login, isAuthenticated, did, logout, loading: contextLoading } = useEncryptID();
const [loading, setLoading] = useState2(false);
const handleClick = useCallback2(async () => {
if (isAuthenticated) {
logout();
return;
}
setLoading(true);
try {
await login();
const token = localStorage.getItem("encryptid_token") || "";
const currentDid = did || "";
onSuccess?.({ token, did: currentDid });
} catch (error) {
if (error.name === "NotAllowedError") {
onRegisterNeeded?.();
} else {
onError?.(error);
}
} finally {
setLoading(false);
}
}, [login, logout, isAuthenticated, did, onSuccess, onError, onRegisterNeeded]);
const isLoading = loading || contextLoading;
const sizeStyles = {
small: { padding: "8px 16px", fontSize: "0.875rem" },
medium: { padding: "12px 24px", fontSize: "1rem" },
large: { padding: "16px 32px", fontSize: "1.125rem" }
};
const baseStyle = {
display: "inline-flex",
alignItems: "center",
gap: "12px",
border: "none",
borderRadius: "8px",
fontWeight: 500,
cursor: isLoading ? "not-allowed" : "pointer",
opacity: isLoading ? 0.6 : 1,
transition: "all 0.2s",
fontFamily: "system-ui, -apple-system, sans-serif",
...sizeStyles[size],
...variant === "primary" ? { background: "#06b6d4", color: "white" } : { background: "transparent", border: "2px solid #06b6d4", color: "#06b6d4" }
};
return React2.createElement("button", {
onClick: handleClick,
disabled: isLoading,
style: baseStyle,
className
}, isLoading ? React2.createElement("span", null, "Authenticating...") : React2.createElement(React2.Fragment, null, React2.createElement("svg", {
viewBox: "0 0 24 24",
fill: "none",
stroke: "currentColor",
strokeWidth: 2,
strokeLinecap: "round",
strokeLinejoin: "round",
style: { width: "24px", height: "24px" }
}, React2.createElement("circle", { cx: 12, cy: 10, r: 3 }), React2.createElement("path", { d: "M12 13v8" }), React2.createElement("path", { d: "M9 18h6" }), React2.createElement("circle", { cx: 12, cy: 10, r: 7 })), React2.createElement("span", null, isAuthenticated ? "Sign Out" : label)));
}
export {
useEncryptID,
LoginButton,
EncryptIDProvider
};

View File

@ -0,0 +1,4 @@
/**
* Re-export useEncryptID hook from Provider module
*/
export { useEncryptID } from './EncryptIDProvider.js';