diff --git a/next.config.js b/next.config.js
index fd90d7c..a77079a 100644
--- a/next.config.js
+++ b/next.config.js
@@ -2,7 +2,40 @@
const nextConfig = {
typescript: {
ignoreBuildErrors: true,
- }
+ },
+ // Configure headers for iframe support
+ async headers() {
+ return [
+ // Default security headers for main app
+ {
+ source: '/((?!iframe).*)',
+ headers: [
+ {
+ key: 'X-Frame-Options',
+ value: 'DENY',
+ },
+ {
+ key: 'Content-Security-Policy',
+ value: "frame-ancestors 'none';",
+ },
+ ],
+ },
+ // Iframe-friendly headers only for /iframe routes
+ {
+ source: '/iframe/:path*',
+ headers: [
+ {
+ key: 'X-Frame-Options',
+ value: 'ALLOWALL',
+ },
+ {
+ key: 'Content-Security-Policy',
+ value: "frame-ancestors *; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline';",
+ },
+ ],
+ },
+ ]
+ },
}
module.exports = nextConfig
diff --git a/src/app/iframe/layout.tsx b/src/app/iframe/layout.tsx
new file mode 100644
index 0000000..e73586d
--- /dev/null
+++ b/src/app/iframe/layout.tsx
@@ -0,0 +1,46 @@
+import '../globals.css'
+import type { Metadata } from 'next'
+import Provider from '@/components/W3UIProvider'
+import Toaster from '@/components/Toaster'
+import { Provider as MigrationsProvider } from '@/components/MigrationsProvider'
+import { IframeProvider } from '@/contexts/IframeContext'
+import IframeHeader from '@/components/IframeHeader'
+import IframeAuthenticator from '@/components/IframeAuthenticator'
+
+export const metadata: Metadata = {
+ title: 'Storacha Workspaces',
+ description: 'Manage your Storacha storage spaces',
+}
+
+export default function IframeLayout({
+ children,
+}: {
+ children: React.ReactNode
+}) {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {children}
+
+
+
+
+
+
+
+
+
+ )
+}
\ No newline at end of file
diff --git a/src/app/iframe/page.tsx b/src/app/iframe/page.tsx
new file mode 100644
index 0000000..58565dc
--- /dev/null
+++ b/src/app/iframe/page.tsx
@@ -0,0 +1,14 @@
+import { Suspense } from 'react'
+import IframeDashboard from '@/components/IframeDashboard'
+
+export default function IframePage() {
+ return (
+
+
+ )
+}
\ No newline at end of file
diff --git a/src/app/iframe/space/[did]/layout.tsx b/src/app/iframe/space/[did]/layout.tsx
new file mode 100644
index 0000000..7845d8d
--- /dev/null
+++ b/src/app/iframe/space/[did]/layout.tsx
@@ -0,0 +1,21 @@
+import { PropsWithChildren, ReactNode } from 'react'
+import IframeCompactSidebar from '@/components/IframeCompactSidebar'
+
+export const runtime = 'edge'
+
+interface LayoutProps extends PropsWithChildren {
+ params: {
+ did: string
+ }
+}
+
+export default function IframeSpaceLayout({ children }: LayoutProps): ReactNode {
+ return (
+
+ )
+}
\ No newline at end of file
diff --git a/src/app/iframe/space/[did]/page.tsx b/src/app/iframe/space/[did]/page.tsx
new file mode 100644
index 0000000..8da4d89
--- /dev/null
+++ b/src/app/iframe/space/[did]/page.tsx
@@ -0,0 +1,96 @@
+'use client'
+
+import { UploadsList } from '@/components/UploadsList'
+import { useW3, UnknownLink, UploadListSuccess, Authenticator } from '@w3ui/react'
+import useSWR from 'swr'
+import { useRouter, usePathname } from 'next/navigation'
+import { createUploadsListKey } from '@/cache'
+import { Breadcrumbs } from '@/components/Breadcrumbs'
+import { logAndCaptureError } from '@/sentry'
+import { AuthenticationEnsurer } from '@/components/Authenticator'
+import { SpaceEnsurer } from '@/components/SpaceEnsurer'
+import { MaybePlanGate } from '@/components/PlanGate'
+import IframeAuthenticator from '@/components/IframeAuthenticator'
+
+const pageSize = 15
+
+interface PageProps {
+ params: {
+ did: string
+ },
+ searchParams: {
+ cursor: string
+ pre: string
+ }
+}
+
+export default function IframeSpacePage({ params, searchParams }: PageProps): JSX.Element {
+ return (
+
+
+
+
+
+
+
+
+
+ )
+}
+
+function SpacePageContent({ params, searchParams }: PageProps): JSX.Element {
+ const [{ client, spaces }] = useW3()
+ const spaceDID = decodeURIComponent(params.did)
+ const space = spaces.find(s => s.did() === spaceDID)
+
+ const key = space ? createUploadsListKey(space.did(), searchParams.cursor, searchParams.pre === 'true') : ''
+ const { data: uploads, isLoading, isValidating, mutate } = useSWR(key, {
+ fetcher: async () => {
+ if (!client || !space) return
+
+ if (client.currentSpace()?.did() !== space.did()) {
+ await client.setCurrentSpace(space.did())
+ }
+
+ return await client.capability.upload.list({
+ cursor: searchParams.cursor,
+ pre: searchParams.pre === 'true',
+ size: pageSize
+ })
+ },
+ onError: logAndCaptureError,
+ keepPreviousData: true
+ })
+
+ const router = useRouter()
+ const pathname = usePathname()
+
+ if (!space) return
+
+ const handleSelect = (root: UnknownLink) => router.push(`${pathname}/root/${root}`)
+ const handleNext = uploads?.after && (uploads.results.length === pageSize)
+ ? () => router.push(`${pathname}?cursor=${uploads.after}`)
+ : undefined
+ const handlePrev = searchParams.cursor && uploads?.before
+ ? () => router.push(`${pathname}?cursor=${uploads.before ?? ''}&pre=true`)
+ : undefined
+ const handleRefresh = () => mutate()
+
+ return (
+
+ )
+}
\ No newline at end of file
diff --git a/src/components/IframeAuthenticator.tsx b/src/components/IframeAuthenticator.tsx
new file mode 100644
index 0000000..858d11f
--- /dev/null
+++ b/src/components/IframeAuthenticator.tsx
@@ -0,0 +1,219 @@
+'use client'
+
+import { useIframe } from '@/contexts/IframeContext'
+import { useW3, Authenticator } from '@w3ui/react'
+import { useEffect, useState, ReactNode } from 'react'
+import { Logo } from '@/brand'
+
+interface IframeAuthenticatorProps {
+ children: ReactNode
+}
+
+export default function IframeAuthenticator({ children }: IframeAuthenticatorProps) {
+ const { isIframe, isClient, parentUser, sendMessageToParent } = useIframe()
+ const [{ accounts }] = useW3()
+ const [authState, setAuthState] = useState<'pending' | 'authenticating' | 'authenticated' | 'failed'>('pending')
+ const [error, setError] = useState(null)
+
+ const isAuthenticated = accounts.length > 0
+
+ // Programmatic DMAIL authentication
+ useEffect(() => {
+ const authenticateWithDmail = async () => {
+ if (!parentUser?.email || authState !== 'pending' || isAuthenticated) return
+
+ try {
+ setAuthState('authenticating')
+ sendMessageToParent({
+ type: 'AUTH_STATUS',
+ status: 'authenticating',
+ email: parentUser.email
+ })
+
+ // Step 1: Validate user with DMAIL API (simulated)
+ console.log('🔐 Starting DMAIL OAuth authentication for:', parentUser.email)
+ const dmailValidation = await validateDmailUser(parentUser)
+ if (!dmailValidation.valid) {
+ throw new Error(`User ${parentUser.email} not found in DMAIL system`)
+ }
+
+ // Step 2: Simulate programmatic OAuth that bypasses email verification
+ await simulateProgrammaticAuth(parentUser.email)
+
+ setAuthState('authenticated')
+ sendMessageToParent({
+ type: 'AUTH_STATUS',
+ status: 'authenticated',
+ email: parentUser.email
+ })
+
+ } catch (err) {
+ console.error('DMAIL authentication failed:', err)
+ setError(err instanceof Error ? err.message : 'Authentication failed')
+ setAuthState('failed')
+ sendMessageToParent({
+ type: 'AUTH_STATUS',
+ status: 'failed',
+ email: parentUser.email,
+ error: err instanceof Error ? err.message : 'Authentication failed'
+ })
+ }
+ }
+
+ if (parentUser?.email && !isAuthenticated) {
+ authenticateWithDmail()
+ }
+ }, [parentUser, authState, isAuthenticated, sendMessageToParent])
+
+ // Monitor authentication state and notify parent
+ useEffect(() => {
+ if (isAuthenticated && parentUser) {
+ console.log('✅ User authenticated successfully!')
+ sendMessageToParent({
+ type: 'AUTH_STATUS',
+ status: 'authenticated',
+ email: parentUser.email
+ })
+ }
+ }, [isAuthenticated, parentUser, sendMessageToParent])
+
+ // Notify parent when user credentials are received
+ useEffect(() => {
+ if (parentUser && !isAuthenticated) {
+ sendMessageToParent({
+ type: 'AUTH_RECEIVED',
+ user: parentUser
+ })
+ }
+ }, [parentUser, isAuthenticated, sendMessageToParent])
+
+ // Don't render until client-side
+ if (!isClient) {
+ return (
+
+ )
+ }
+
+ // If not in iframe, use regular authenticator
+ if (!isIframe) {
+ return {children}
+ }
+
+ // If no parent user provided, show waiting state
+ if (!parentUser) {
+ return (
+
+
+
+
+ Waiting for Authentication
+
+
+ Waiting for user credentials from email provider...
+
+
+
+ )
+ }
+
+ // Show authentication progress
+ if (authState === 'authenticating') {
+ return (
+
+
+
+ Authenticating with DMAIL
+
+
+ Validating {parentUser.email}...
+
+
+ This should take just a moment
+
+
+ )
+ }
+
+ // Show authentication failure
+ if (authState === 'failed') {
+ return (
+
+
+
+
+ ✗
+
+
Authentication Failed
+
+
{error}
+
{
+ setAuthState('pending')
+ setError(null)
+ }}
+ className="w-full bg-red-600 text-white py-2 px-4 rounded-md hover:bg-red-700 transition-colors"
+ >
+ Retry Authentication
+
+
+
+ )
+ }
+
+ // If user is authenticated OR we completed our simulation, show the console
+ if (isAuthenticated || authState === 'authenticated') {
+ return {children}
+ }
+
+ // Default: show preparation state
+ return (
+
+
+
+
+ Preparing Workspace
+
+
+ Setting up your Storacha workspace...
+
+
+
+ )
+}
+
+/**
+ * Validate user exists in DMAIL system
+ */
+async function validateDmailUser(user: { email: string, id: string, name?: string }): Promise<{ valid: boolean, plan?: string }> {
+ // Simulate API call to DMAIL
+ await new Promise(resolve => setTimeout(resolve, 1500))
+
+ // Simulate validation logic
+ if (user.email.endsWith('@dmail.ai')) {
+ console.log('✅ User validated with DMAIL API:', user.email)
+ return { valid: true, plan: 'pro' }
+ }
+
+ throw new Error('User not found in DMAIL system')
+}
+
+/**
+ * Simulate programmatic authentication that would happen after DMAIL OAuth validation
+ */
+async function simulateProgrammaticAuth(email: string) {
+ console.log('🔐 Simulating DMAIL OAuth backend flow for:', email)
+
+ // Simulate the OAuth flow timing
+ await new Promise(resolve => setTimeout(resolve, 2000))
+
+ console.log('✅ Programmatic authentication completed for:', email)
+ console.log('📝 In production, this would:')
+ console.log(' 1. Validate user with DMAIL API')
+ console.log(' 2. Issue Access.confirm delegation on backend')
+ console.log(' 3. Use w3up client to claim delegated access')
+ console.log(' 4. User would be authenticated with Storacha')
+}
\ No newline at end of file
diff --git a/src/components/IframeDashboard.tsx b/src/components/IframeDashboard.tsx
new file mode 100644
index 0000000..966e00c
--- /dev/null
+++ b/src/components/IframeDashboard.tsx
@@ -0,0 +1,69 @@
+'use client'
+
+import { useW3, Space } from '@w3ui/react'
+import { DidIcon } from '@/components/DidIcon'
+import Link from 'next/link'
+import { H1, H2 } from '@/components/Text'
+import { ReactNode } from 'react'
+import { AuthenticationEnsurer } from '@/components/Authenticator'
+import { SpaceEnsurer } from '@/components/SpaceEnsurer'
+import { MaybePlanGate } from '@/components/PlanGate'
+import IframeAuthenticator from '@/components/IframeAuthenticator'
+
+export default function IframeDashboard(): ReactNode {
+ return (
+
+
+
+
+
+
+
+
+
+ )
+}
+
+function SpacePage(): ReactNode {
+ const [{ spaces }] = useW3()
+
+ if (spaces.length === 0) {
+ return (
+
+
Welcome to Storacha Workspaces
+
+ No spaces found. Create your first space to get started.
+
+
+ )
+ }
+
+ return (
+
+
Your Workspaces
+
Select a Space to Manage
+
+ {spaces.map(s => )}
+
+
+ )
+}
+
+function Item({ space }: { space: Space }) {
+ return (
+
+
+
+
+ {space.name || 'Untitled'}
+
+
+ {space.did()}
+
+
+
+ )
+}
\ No newline at end of file
diff --git a/src/contexts/IframeContext.tsx b/src/contexts/IframeContext.tsx
new file mode 100644
index 0000000..2ef13ad
--- /dev/null
+++ b/src/contexts/IframeContext.tsx
@@ -0,0 +1,101 @@
+'use client'
+
+import { createContext, useContext, useEffect, useState, ReactNode } from 'react'
+
+interface IframeContextType {
+ isIframe: boolean
+ isClient: boolean
+ parentOrigin: string | null
+ parentUser: { email: string; id: string } | null
+ sendMessageToParent: (data: any) => void
+ requestParentNavigation: (url: string) => void
+}
+
+const IframeContext = createContext(undefined)
+
+export function IframeProvider({ children }: { children: ReactNode }) {
+ const [isIframe, setIsIframe] = useState(false)
+ const [parentOrigin, setParentOrigin] = useState(null)
+ const [isClient, setIsClient] = useState(false)
+ const [parentUser, setParentUser] = useState<{ email: string; id: string } | null>(null)
+
+ useEffect(() => {
+ // Mark as client-side rendered
+ setIsClient(true)
+
+ // Detect if running in iframe
+ const inIframe = window.self !== window.top
+ setIsIframe(inIframe)
+
+ if (inIframe) {
+ // Listen for messages from parent
+ const handleMessage = (event: MessageEvent) => {
+ // Validate origin - in production, whitelist specific domains
+ if (event.origin !== parentOrigin && parentOrigin === null) {
+ setParentOrigin(event.origin)
+ }
+
+ // Handle parent messages
+ if (event.data.type === 'IFRAME_INIT') {
+ sendMessageToParent({
+ type: 'CONSOLE_READY',
+ url: window.location.href
+ })
+ }
+
+ if (event.data.type === 'USER_AUTH') {
+ console.log('🔐 Received user authentication from parent:', event.data.user)
+ setParentUser(event.data.user)
+ sendMessageToParent({
+ type: 'AUTH_RECEIVED',
+ user: event.data.user
+ })
+ }
+ }
+
+ window.addEventListener('message', handleMessage)
+
+ // Notify parent that iframe is loaded
+ window.parent?.postMessage({
+ type: 'CONSOLE_LOADED',
+ url: window.location.href
+ }, '*')
+
+ return () => window.removeEventListener('message', handleMessage)
+ }
+ }, [parentOrigin])
+
+ const sendMessageToParent = (data: any) => {
+ if (isIframe && window.parent) {
+ window.parent.postMessage(data, parentOrigin || '*')
+ }
+ }
+
+ const requestParentNavigation = (url: string) => {
+ sendMessageToParent({
+ type: 'REQUEST_NAVIGATION',
+ url
+ })
+ }
+
+ return (
+
+ {children}
+
+ )
+}
+
+export function useIframe() {
+ const context = useContext(IframeContext)
+ if (context === undefined) {
+ throw new Error('useIframe must be used within an IframeProvider')
+ }
+ return context
+}
\ No newline at end of file