Skip to content

icon picker
Identity / Profiles

the profile as archive is an important metaphor. this is what made original social media so compelling because you were building a lasting archive.

Now it's become curated and a portfolio rather than a personal archive, but for your internet activity, you don't care. it's not so tied to you. it's a bit disconnected from who you are as a person and what you do and more connected to where you spend time and what your interests are? or maybe its a more pure look into what your personality is? what decisions do you make in your daily (internet) life?
So you should have a profile page that has your personal archive, maybe people can follow certain timeline activities and see the urls you took actions on?
pseudonymous? — you should be able to have meaningufl intimate encounters with people without them knowing everything about you like they do on social media? but you still need something that makes this personal and vulnerable somehow.. while avoiding the risk of doxxing / abuse that is the double-edged sword of platforms like Omegle.
you can personalize the cursor, avatar, character but hmm this doesn't really feel like you? maybe you use a profile picture that's not exactly you so its like a social media alt? but then people can put their actual faces.. unless we run it through some filter that obscures it but still keeps the identity?
will people use it if they have nothing to gain from it? but isn't that what everyone started using social media in the first place for? to share the random lunch photos and grainy family videos for the sake of these encounters with people beyond their immediately accessible world / community?

Overview

A lightweight, decentralized identity system that works seamlessly between the browser extension and core PlayHTML library. Like MetaMask for social web interactions, providing permissioned access to PlayHTML elements across the internet without requiring traditional login systems. This will also form the basis of the Internet Game.

Core Architecture

1. Identity Generation & Storage

We’ll host an easy interface for generating identities (auth.playhtml.fun), where people can generate new keys for themselves. Simple local storage remembers who they are and provides instructions for authenticating with their domain name.
They are given a public and a private key which they much keep secret. TBD how to make this more user-friendly. These can be imported into the extension

2. Extension Integration

When an identity is included in the extension, it is injected into any page that integrates with playhtml
Global Identity Injection:
interface PlayHTMLIdentity {
privateKey: string; // Ed25519 private key for signing
publicKey: string; // Public identity (like MetaMask address)
createdAt: number; // Identity creation timestamp
profileSettings: {} // optional things to store on here? maybe let playhtml sites modify freely? like cursor style, color etc.
}

// Extension injects identity into page context
interface PlayHTMLAuth {
identity?: PlayHTMLIdentity;
isAuthenticated: boolean;
sign: (message: string) => Promise<string>;
verify: (
message: string,
signature: string,
publicKey: string
) => Promise<boolean>;
}

// Content script automatically injects authenticated identity
function injectAuth() {
const identity = getStoredIdentity(); // From extension storage

window.playhtmlAuth = {
identity,
isAuthenticated: !!identity,
sign: async (message) => signMessage(message, identity.privateKey),
verify: async (message, signature, publicKey) =>
verifySignature(message, signature, publicKey),
};

// Dispatch event so PlayHTML can react to auth changes
window.dispatchEvent(
new CustomEvent("playhtmlAuthReady", { detail: window.playhtmlAuth })
);
}

3. Declaring permissions

in initializing playhtml we can declare the owner of the page
<!-- Basic owner-only permissions -->
<div
id="admin-panel"
can-play
playhtml-permissions="write:owner, delete:owner"
></div>

<!-- Role-based permissions -->
<div
id="community-board"
can-move
playhtml-permissions="write:contributors, delete:moderators"
></div>

<!-- Multiple actions with different roles -->
<div
id="guestbook"
can-play
playhtml-owner="spencer.place"
playhtml-permissions="read:everyone, write:visitors, moderate:moderators, delete:owner"
></div>

<!-- No restrictions = everyone can do everything -->
<div id="public-canvas" can-move can-spin playhtml-owner="spencer.place"></div>

React Component Props:
// React components use props instead of HTML attributes
function MyGuestbook() {
return (
<CanPlay
id="guestbook"
owner="spencer.place"
permissions={{
read: "everyone",
write: "visitors",
moderate: "moderators",
delete: "owner",
}}
>
{/* guestbook content */}
</CanPlay>
);
}

// Alternative object syntax for complex permissions
function ComplexElement() {
return (
<CanPlay
id="advanced-element"
owner="spencer.place"
permissions={[
{ action: "read", role: "everyone" },
{ action: "write", role: "contributors", condition: "frequentVisitor" },
{ action: "delete", role: "moderators" },
{ action: "admin", role: "owner" },
]}
>
{/* element content */}
</CanPlay>
);
}

4. JavaScript Configuration System

Global PlayHTML Configuration:

5. Automatic Signing & Server Validation

Client-Side Automatic Signing:
// Enhanced setData with automatic authentication
async function setData(
elementId: string,
newData: any,
options: { action?: string } = {}
) {
const action = options.action || "write"; // Default action

if (window.playhtmlAuth?.identity) {
try {
// Check permissions first
const hasPermission = await checkPermission(
elementId,
action,
window.playhtmlAuth.identity
);

if (!hasPermission) {
throw new Error("Permission denied");
}

// Automatically sign the data change
const signedChange = await createSignedAction(
action,
elementId,
newData,
window.playhtmlAuth.identity
);

// Apply to CRDT with temporary auth data
applyDataChange({
type: "crdt_update",
elementId,
data: {
...newData,
_temp_auth: signedChange,
},
});
} catch (error) {
console.error("Failed to perform action:", error);
showUserFeedback(`Unable to ${action}: ${error.message}`);
}
} else {
// No identity - check if action is allowed for "everyone"
const hasPermission = await checkPermission(elementId, action);

if (hasPermission) {
// Apply change without authentication
applyDataChange({
type: "crdt_update",
elementId,
data: newData,
});
} else {
showUserFeedback("Please connect your PlayHTML identity to interact");
}
}
}

async function createSignedAction(
action: string,
elementId: string,
data: any,
identity: PlayHTMLIdentity
): Promise<SignedAction> {
const payload = {
action,
elementId,
data,
timestamp: Date.now(),
nonce: crypto.randomUUID(),
};

const message = JSON.stringify(payload);
const signature = await signMessage(message, identity.privateKey);

return {
...payload,
signature,
publicKey: identity.publicKey,
};
}

Server-Side Session & Renewal Management:
// Enhanced PartyKit server with session renewal support
export default class SessionValidatedPlayHTML implements PartyKitServer {
private validSessions = new Map<string, ValidatedSession>();
private pendingChallenges = new Map<string, SessionChallenge>();
private usedNonces = new Set<string>();

// Session establishment with renewal support
async handleSessionEstablishment(request: Request): Response<Response> {
const { challenge, signature, publicKey } = await request.json();

// Validate challenge exists and signature is correct (ONLY crypto verification)
const storedChallenge = this.pendingChallenges.get(challenge.challenge);
if (!storedChallenge || storedChallenge.expiresAt < Date.now()) {
return new Response('Invalid or expired challenge', { status: 400 });
}

const isValidSignature = await verifySignature(
JSON.stringify(challenge),
signature,
publicKey
);

if (!isValidSignature) {
return new Response('Invalid signature', { status: 400 });
}

// Check if this is a renewal (user already has active session)
const existingSession = this.findExistingSession(publicKey);

if (existingSession) {
// Extend existing session instead of creating new one
existingSession.expiresAt = Date.now() + (24 * 60 * 60 * 1000);

console.log(`🔄 Renewed session for ${publicKey}`);

return new Response(JSON.stringify({
sessionId: existingSession.sessionId, // Keep same session ID
publicKey: existingSession.publicKey,
expiresAt: existingSession.expiresAt,
renewed: true
}));
} else {
// Create new session
const session: ValidatedSession = {
sessionId: crypto.randomUUID(),
publicKey,
Want to print your doc?
This is not the way.
Try clicking the ⋯ next to your doc name or using a keyboard shortcut (
CtrlP
) instead.