Skip to content

Commit 7f6e080

Browse files
authored
docs: add getting started guide (#624)
1 parent 46f3dd6 commit 7f6e080

2 files changed

Lines changed: 333 additions & 0 deletions

File tree

docs/getting-started.md

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
# Getting Started with Platform Development
2+
3+
This guide will help you get started building platforms in the metastate ecosystem. We'll cover the essential concepts and patterns you'll need to implement, using `@eCurrency-api` as a reference example.
4+
5+
## Overview
6+
7+
Platforms in the metastate ecosystem follow a standard architecture pattern:
8+
1. **Authentication** - Users authenticate using their W3ID (Web3 Identity) via the `w3ds://auth` protocol
9+
2. **Webhooks** - Platform data syncs from the global eVault system via webhooks
10+
3. **Mappings** - Data transformation between global ontology and local database schemas
11+
12+
This document focuses on authentication. For webhooks and mappings, see the other documentation files.
13+
14+
## Authentication
15+
16+
All platforms use a signature-based authentication system that leverages users' existing ename and keys attached to that. The authentication flow follows the `w3ds://auth` protocol.
17+
18+
### Authentication Flow
19+
20+
The authentication process involves these steps:
21+
22+
1. **Client requests auth offer** → Server returns `w3ds://auth` URL with session ID
23+
2. **User signs in via w3ds client** → User is redirected back with signature
24+
3. **Server verifies signature** → Uses `signature-validator` to verify the signature
25+
4. **Server finds/creates user** → Looks up user by eName, generates JWT token
26+
5. **Client uses Bearer token** → Includes token in `Authorization: Bearer <token>` header
27+
6. **Middleware validates token**`authMiddleware` extracts token and loads user into `req.user`
28+
7. **Protected routes** → Use `authGuard` to ensure user is authenticated
29+
30+
### Implementation Example (eCurrency-api)
31+
32+
#### 1. Offer Endpoint (`GET /api/auth/offer`)
33+
34+
This endpoint generates an authentication offer URL that the client can use to initiate the login flow.
35+
36+
```typescript
37+
getOffer = async (req: Request, res: Response) => {
38+
const baseUrl = "http://localhost:9888";
39+
const url = new URL("/api/auth", baseUrl).toString();
40+
const sessionId = uuidv4();
41+
const offer = `w3ds://auth?redirect=${url}&session=${sessionId}&platform='PLATFORM NAME HERE`;
42+
res.json({ offer, sessionId });
43+
};
44+
```
45+
46+
**Response:**
47+
```json
48+
{
49+
"offer": "w3ds://auth?redirect=http://localhost:9888/api/auth&session=abc123...&platform=ecurrency",
50+
"sessionId": "abc123..."
51+
}
52+
```
53+
54+
The client opens this URL in a w3ds-compatible client (like eID Wallet), which handles the user's signature and redirects back to your platform.
55+
56+
#### 2. Login Endpoint (`POST /api/auth`)
57+
58+
This endpoint receives the authentication result from the w3ds client and verifies the signature.
59+
60+
**Request body:**
61+
```json
62+
{
63+
"ename": "@user.w3id",
64+
"session": "abc123...",
65+
"w3id": "https://evault.example.com/users/123",
66+
"signature": "z..."
67+
}
68+
```
69+
70+
**Implementation:**
71+
```typescript
72+
login = async (req: Request, res: Response) => {
73+
const { ename, session, signature } = req.body;
74+
75+
// Verify signature using signature-validator
76+
const verificationResult = await verifySignature({
77+
eName: ename,
78+
signature: signature,
79+
payload: session,
80+
registryBaseUrl: process.env.PUBLIC_REGISTRY_URL,
81+
});
82+
83+
if (!verificationResult.valid) {
84+
return res.status(401).json({
85+
error: "Invalid signature",
86+
message: verificationResult.error
87+
});
88+
}
89+
90+
// Find user by eName (users must be created via webhook first)
91+
const user = await this.userService.findUser(ename);
92+
if (!user) {
93+
return res.status(404).json({
94+
error: "User not found",
95+
message: "User must be created via eVault webhook before authentication"
96+
});
97+
}
98+
99+
// Generate JWT token
100+
const token = signToken({ userId: user.id });
101+
102+
res.status(200).json({
103+
user: { /* user data */ },
104+
token,
105+
});
106+
};
107+
```
108+
109+
**Key points:**
110+
- The `session` string is what was signed by the user
111+
- Signature verification uses the `signature-validator` package, which:
112+
- Fetches the user's public key from their eVault
113+
- Verifies the signature using Web Crypto API
114+
- Supports multiple signature formats (multibase, base64, etc.)
115+
- Users must exist in your database before they can authenticate (created via webhooks)
116+
- The JWT token contains the `userId` and expires in 7 days
117+
118+
#### 3. JWT Token Generation
119+
120+
The JWT token is generated using a secret key stored in `JWT_SECRET` environment variable.
121+
122+
```typescript
123+
// src/utils/jwt.ts
124+
export const signToken = (payload: AuthTokenPayload): string => {
125+
return jwt.sign(payload, JWT_SECRET, { expiresIn: "7d" });
126+
};
127+
128+
export const verifyToken = (token: string): AuthTokenPayload => {
129+
const decoded = jwt.verify(token, JWT_SECRET) as JwtPayload & AuthTokenPayload;
130+
if (!decoded.userId || typeof decoded.userId !== 'string') {
131+
throw new Error("Invalid token: missing or invalid userId");
132+
}
133+
return { userId: decoded.userId };
134+
};
135+
```
136+
137+
**Important:** Always set `JWT_SECRET` as an environment variable and never commit it to version control.
138+
139+
#### 4. Auth Middleware
140+
141+
The auth middleware extracts the JWT token from the `Authorization` header and loads the user into `req.user`.
142+
143+
```typescript
144+
// src/middleware/auth.ts
145+
export const authMiddleware = async (req: Request, res: Response, next: NextFunction) => {
146+
const authHeader = req.headers.authorization;
147+
148+
if (!authHeader || !authHeader.startsWith('Bearer ')) {
149+
return next(); // Continue without user (for optional auth routes)
150+
}
151+
152+
const token = authHeader.substring(7);
153+
154+
try {
155+
const { userId } = verifyToken(token);
156+
const user = await userService.getUserById(userId);
157+
158+
if (user) {
159+
req.user = user;
160+
}
161+
} catch (error) {
162+
// Invalid token - continue without user
163+
}
164+
165+
next();
166+
};
167+
```
168+
169+
#### 5. Auth Guard
170+
171+
The auth guard ensures that a user is authenticated before proceeding.
172+
173+
```typescript
174+
export const authGuard = (req: Request, res: Response, next: NextFunction) => {
175+
if (!req.user) {
176+
return res.status(401).json({ error: "Unauthorized" });
177+
}
178+
next();
179+
};
180+
```
181+
182+
### Route Configuration
183+
184+
Routes are configured to use middleware appropriately:
185+
186+
```typescript
187+
// Public routes (no auth required)
188+
app.get("/api/auth/offer", authController.getOffer);
189+
app.post("/api/auth", authController.login);
190+
app.post("/api/webhook", webhookController.handleWebhook); // Webhooks don't require auth
191+
192+
// Protected routes (auth required)
193+
app.use(authMiddleware); // Apply auth middleware to all routes below
194+
195+
app.get("/api/users/me", authGuard, userController.currentUser);
196+
app.post("/api/currencies", authGuard, currencyController.createCurrency);
197+
// ... other protected routes
198+
```
199+
200+
**Route patterns:**
201+
- **Public routes**: Authentication endpoints, webhooks, and any public-facing APIs
202+
- **Protected routes**: All routes after `app.use(authMiddleware)` require authentication
203+
- **Optional auth routes**: Routes that work with or without authentication (rare)
204+
205+
### Environment Variables
206+
207+
Required environment variables for authentication:
208+
209+
```env
210+
# JWT secret for token signing/verification
211+
JWT_SECRET=your-secret-key-here
212+
213+
# Registry base URL for signature verification
214+
PUBLIC_REGISTRY_URL=https://registry.example.com
215+
```
216+
217+
### Next Steps
218+
219+
1. **[Webhook Controller](./webhook-controller.md)** - How to handle incoming webhooks from the eVault system
220+
2. **[Mapping Rules](../../infrastructure/web3-adapter/MAPPING_RULES.md)** - How to create mappings between global ontology and your local database schema
221+
222+
These components work together to create a seamless integration between your platform and the W3DS ecosystem.
223+
224+

docs/webhook-controller.md

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
# Webhook Controller Guide
2+
3+
The webhook controller receives awareness protocol packets from the eVault system and saves them to your local database.
4+
5+
## What the Webhook Receives
6+
7+
The webhook endpoint (`POST /api/webhook`) receives awareness protocol packets with the following structure:
8+
9+
```json
10+
{
11+
"id": "global-id-123",
12+
"schemaId": "uuid-of-schema",
13+
"w3id": "https://evault.example.com/users/123",
14+
"data": {
15+
"displayName": "John Doe",
16+
"username": "johndoe",
17+
// ... other fields according to the global schema
18+
}
19+
}
20+
```
21+
22+
The `schemaId` identifies which mapping to use for transforming the data, and `data` contains the entity information in the global ontology format.
23+
24+
## What to Do
25+
26+
1. **Find the mapping** using the `schemaId`:
27+
```typescript
28+
const mapping = Object.values(this.adapter.mapping).find(
29+
(m: any) => m.schemaId === schemaId
30+
);
31+
```
32+
33+
2. **Convert from global to local** using the adapter's `fromGlobal` method:
34+
```typescript
35+
const local = await this.adapter.fromGlobal({
36+
data: req.body.data,
37+
mapping,
38+
});
39+
```
40+
41+
This method uses your mapping configuration to transform the global ontology data into your local database schema format. See the [Mapping Rules documentation](../../infrastructure/web3-adapter/MAPPING_RULES.md) for details on creating mappings.
42+
43+
3. **Check if entity exists** using the global ID:
44+
```typescript
45+
let localId = await this.adapter.mappingDb.getLocalId(req.body.id);
46+
```
47+
48+
4. **Save or update** the entity in your database:
49+
- If `localId` exists, update the existing entity
50+
- If not, create a new entity and store the mapping:
51+
```typescript
52+
await this.adapter.mappingDb.storeMapping({
53+
localId: entity.id,
54+
globalId: req.body.id,
55+
});
56+
```
57+
58+
5. **Return success**:
59+
```typescript
60+
res.status(200).send();
61+
```
62+
63+
## Implementation Example
64+
65+
Here's a simplified example from `@eCurrency-api`:
66+
67+
```typescript
68+
handleWebhook = async (req: Request, res: Response) => {
69+
const globalId = req.body.id;
70+
const schemaId = req.body.schemaId;
71+
72+
try {
73+
// Find mapping
74+
const mapping = Object.values(this.adapter.mapping).find(
75+
(m: any) => m.schemaId === schemaId
76+
);
77+
78+
if (!mapping) {
79+
throw new Error("No mapping found");
80+
}
81+
82+
// Convert global to local
83+
const local = await this.adapter.fromGlobal({
84+
data: req.body.data,
85+
mapping,
86+
});
87+
88+
// Check if exists
89+
let localId = await this.adapter.mappingDb.getLocalId(globalId);
90+
91+
// Save or update based on entity type
92+
if (mapping.tableName === "users") {
93+
// Create or update user...
94+
} else if (mapping.tableName === "groups") {
95+
// Create or update group...
96+
}
97+
98+
res.status(200).send();
99+
} catch (e) {
100+
console.error("Webhook error:", e);
101+
res.status(500).send();
102+
}
103+
};
104+
```
105+
106+
## Related Documentation
107+
108+
- **[Getting Started](./getting-started.md)** - Authentication and platform setup
109+
- **[Mapping Rules](../../infrastructure/web3-adapter/MAPPING_RULES.md)** - How to create mappings between global and local schemas

0 commit comments

Comments
 (0)