@volter/twin-clerk 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client/clerk-mirror.bundle.js +242 -175
- package/dist/src/clerk-events.js +7 -6
- package/dist/src/clerk-jwt.js +5 -5
- package/dist/src/clerk-mirror-ui.js +3 -3
- package/dist/src/clerk-server.js +5 -3
- package/dist/src/clerk-twin.js +2 -2
- package/package.json +3 -3
- package/src/clerk-events.ts +6 -5
- package/src/clerk-jwt.ts +5 -5
- package/src/clerk-mirror-ui.ts +3 -3
- package/src/clerk-server.ts +5 -3
- package/src/clerk-twin.ts +2 -2
package/dist/src/clerk-events.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { nodeBuiltin } from '@volter/world-core';
|
|
1
|
+
import { nodeBuiltin, worldNow, deliveryTraceHeaders } from '@volter/world-core';
|
|
2
|
+
import { appDestination, appFetch } from '@volter/world-core/app-route';
|
|
2
3
|
import { worldEgressRefusal } from '@volter/world-core/network-policy';
|
|
3
4
|
function nodeCrypto() {
|
|
4
5
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
@@ -41,7 +42,7 @@ export function verifyWebhook(payload, headers, secret, opts = {}) {
|
|
|
41
42
|
if (!id || !sigHeader || !Number.isFinite(ts))
|
|
42
43
|
throw new ClerkWebhookVerificationError('Missing required svix headers');
|
|
43
44
|
if (opts.tolerance !== undefined) {
|
|
44
|
-
const now = opts.now ?? Math.floor(Date.
|
|
45
|
+
const now = opts.now ?? Math.floor(Date.parse(worldNow()) / 1000);
|
|
45
46
|
if (Math.abs(now - ts) > opts.tolerance)
|
|
46
47
|
throw new ClerkWebhookVerificationError('Message timestamp too old');
|
|
47
48
|
}
|
|
@@ -100,10 +101,10 @@ function svixMessageId(url, timestamp, body) { let h = 0x811c9dc5; const s = `${
|
|
|
100
101
|
h = Math.imul(h, 0x01000193) >>> 0;
|
|
101
102
|
} return `msg_twin_${h.toString(16).padStart(8, '0')}`; }
|
|
102
103
|
const httpDelivery = async (url, body, headers) => {
|
|
103
|
-
if (worldEgressRefusal(url) !== null)
|
|
104
|
-
return; // the World's egress rule: refused like an unreachable endpoint
|
|
104
|
+
if (!appDestination(url) && worldEgressRefusal(url) !== null)
|
|
105
|
+
return; // the World's egress rule, the application's own hostnames reaching it inside the World: refused like an unreachable endpoint
|
|
105
106
|
try {
|
|
106
|
-
await
|
|
107
|
+
await appFetch(url, { method: 'POST', headers: { 'content-type': 'application/json', ...headers, ...deliveryTraceHeaders() }, body });
|
|
107
108
|
}
|
|
108
109
|
catch {
|
|
109
110
|
/* fire-and-forget, like the real vendor */
|
|
@@ -126,7 +127,7 @@ export async function emitClerkEvent(operation, data, opts = {}) {
|
|
|
126
127
|
if (!type || targets.length === 0)
|
|
127
128
|
return [];
|
|
128
129
|
const deliver = opts.deliver ?? httpDelivery;
|
|
129
|
-
const timestamp = Math.floor((opts.occurredAt ? Date.parse(opts.occurredAt) : Date.
|
|
130
|
+
const timestamp = Math.floor((opts.occurredAt ? Date.parse(opts.occurredAt) : Date.parse(worldNow())) / 1000) || 0;
|
|
130
131
|
const event = { type, object: 'event', timestamp, data };
|
|
131
132
|
const body = JSON.stringify(event);
|
|
132
133
|
const out = [];
|
package/dist/src/clerk-jwt.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
// top-level static import, so this module can be pulled into the browser bundle of
|
|
13
13
|
// the UI mirror without Bun's browser target choking on `node:crypto` — the signing
|
|
14
14
|
// helpers are server-only and never reached in the browser path.
|
|
15
|
-
import { applyTwinWrite, projectResources, nodeBuiltin } from '@volter/world-core';
|
|
15
|
+
import { applyTwinWrite, projectResources, nodeBuiltin, worldNow } from '@volter/world-core';
|
|
16
16
|
function nodeCrypto() {
|
|
17
17
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
18
18
|
return nodeBuiltin('node:crypto');
|
|
@@ -91,7 +91,7 @@ function base64urlJson(obj) {
|
|
|
91
91
|
export async function signJwt(claims, opts = {}) {
|
|
92
92
|
const { createSign } = nodeCrypto();
|
|
93
93
|
const { kid, privateKeyPem } = await ensureKeypair(opts.root);
|
|
94
|
-
const iat = opts.now ?? Math.floor(Date.
|
|
94
|
+
const iat = opts.now ?? Math.floor(Date.parse(worldNow()) / 1000);
|
|
95
95
|
const exp = iat + (opts.expiresInSeconds ?? 60);
|
|
96
96
|
const header = { alg: 'RS256', typ: 'JWT', kid };
|
|
97
97
|
const payload = { iat, exp, nbf: iat, ...claims };
|
|
@@ -111,7 +111,7 @@ export async function signJwt(claims, opts = {}) {
|
|
|
111
111
|
*/
|
|
112
112
|
export function signJwtHs256(claims, secret, opts = {}) {
|
|
113
113
|
const { createHmac } = nodeCrypto();
|
|
114
|
-
const iat = opts.now ?? Math.floor(Date.
|
|
114
|
+
const iat = opts.now ?? Math.floor(Date.parse(worldNow()) / 1000);
|
|
115
115
|
const exp = iat + (opts.expiresInSeconds ?? 60);
|
|
116
116
|
const header = { alg: 'HS256', typ: 'JWT' };
|
|
117
117
|
const payload = { iat, exp, nbf: iat, ...claims };
|
|
@@ -143,7 +143,7 @@ export function verifyJwtHs256(token, secret, opts = {}) {
|
|
|
143
143
|
const b = Buffer.from(expected, 'utf8');
|
|
144
144
|
if (a.length !== b.length || !timingSafeEqual(a, b))
|
|
145
145
|
return { valid: false, reason: 'bad_signature' };
|
|
146
|
-
const now = opts.now ?? Math.floor(Date.
|
|
146
|
+
const now = opts.now ?? Math.floor(Date.parse(worldNow()) / 1000);
|
|
147
147
|
if (typeof payload.exp === 'number' && now >= payload.exp)
|
|
148
148
|
return { valid: false, reason: 'expired' };
|
|
149
149
|
if (typeof payload.nbf === 'number' && now < payload.nbf)
|
|
@@ -189,7 +189,7 @@ export function verifyJwtWithJwks(token, jwks, opts = {}) {
|
|
|
189
189
|
const sig = Buffer.from(parts[2].replace(/-/g, '+').replace(/_/g, '/'), 'base64');
|
|
190
190
|
if (!verifier.verify(publicKey, sig))
|
|
191
191
|
return { valid: false, reason: 'bad_signature' };
|
|
192
|
-
const now = opts.now ?? Math.floor(Date.
|
|
192
|
+
const now = opts.now ?? Math.floor(Date.parse(worldNow()) / 1000);
|
|
193
193
|
if (typeof payload.exp === 'number' && now >= payload.exp)
|
|
194
194
|
return { valid: false, reason: 'expired' };
|
|
195
195
|
if (typeof payload.nbf === 'number' && now < payload.nbf)
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// pack's OWN fetch adapter (createClerkTwinFetch) as its API backend and reads every byte of
|
|
7
7
|
// state back over the wire, exactly as the browser client does.
|
|
8
8
|
import { readFile } from 'node:fs/promises';
|
|
9
|
-
import { bundleClient, fileResponse } from '@volter/world-core';
|
|
9
|
+
import { bundleClient, fileResponse, filePathOf } from '@volter/world-core';
|
|
10
10
|
import { serveHttp } from '@volter/world-core';
|
|
11
11
|
import { createClerkTwinFetch } from "./clerk-server.js";
|
|
12
12
|
// Resolved LAZILY (on first serve), not at module load: `new URL(..., import.meta.url)` at
|
|
@@ -14,8 +14,8 @@ import { createClerkTwinFetch } from "./clerk-server.js";
|
|
|
14
14
|
// `import.meta.url` is not a valid file base (e.g. workerd) even though the mirror UI is never
|
|
15
15
|
// served there. Computing them on demand keeps Node/Bun behavior identical while letting a
|
|
16
16
|
// serverless host import this module's barrel without a startup crash.
|
|
17
|
-
const clientEntryPath = () => new URL('../client/clerk-mirror.tsx', import.meta.url)
|
|
18
|
-
const clientCssPath = () => new URL('../client/clerk-mirror.css', import.meta.url)
|
|
17
|
+
const clientEntryPath = () => filePathOf(new URL('../client/clerk-mirror.tsx', import.meta.url));
|
|
18
|
+
const clientCssPath = () => filePathOf(new URL('../client/clerk-mirror.css', import.meta.url));
|
|
19
19
|
/** A user's display name: "First Last", else username, else primary email, else id. */
|
|
20
20
|
export function formatUserName(u) {
|
|
21
21
|
const name = [u?.first_name, u?.last_name].filter(Boolean).join(' ').trim();
|
package/dist/src/clerk-server.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// Personas are world state: pass `personas` (a JSON file of users with passwords) and the
|
|
8
8
|
// server seeds them as ordinary user rows at start — sign-in is a protocol, never a default.
|
|
9
9
|
// JSON bodies pass through to the handler. Writable by default; readOnly rejects writes (R4).
|
|
10
|
-
import { serveHttp } from '@volter/world-core';
|
|
10
|
+
import { runWithRequestTrace, serveHttp } from '@volter/world-core';
|
|
11
11
|
import { handleClerkTwinRequest } from "./clerk-twin.js";
|
|
12
12
|
import { worldNow, statefulTwinManifest, packAsset } from '@volter/world-core';
|
|
13
13
|
import { handleClerkFapiRequest } from "./clerk-fapi.js";
|
|
@@ -89,7 +89,9 @@ export function createClerkTwinFetch(options = {}) {
|
|
|
89
89
|
// the pack CONSTRUCTS on any host (workerd included) and only the local-CDN route itself needs
|
|
90
90
|
// Bun — until the dist rides a pack-asset seam (`TwinPack.assets`, the pack-asset seam), that route is the
|
|
91
91
|
// pack's one host-bound surface, not the whole pack.
|
|
92
|
-
|
|
92
|
+
// a request's W3C trace context scopes everything it writes and delivers (world-core trace-context)
|
|
93
|
+
return (request) => runWithRequestTrace(request, () => clerkTwinFetch(request));
|
|
94
|
+
async function clerkTwinFetch(request) {
|
|
93
95
|
await seeded;
|
|
94
96
|
const url = new URL(request.url);
|
|
95
97
|
// GET /twin — the discovery manifest (education inside the twin).
|
|
@@ -160,7 +162,7 @@ export function createClerkTwinFetch(options = {}) {
|
|
|
160
162
|
...(options.root !== undefined ? { root: options.root } : {}),
|
|
161
163
|
});
|
|
162
164
|
return new Response(JSON.stringify(out), { status, headers: { 'content-type': 'application/json', 'clerk-trace-id': 'trace_twin', ...cors } });
|
|
163
|
-
}
|
|
165
|
+
}
|
|
164
166
|
}
|
|
165
167
|
export async function createClerkTwinServer(options) {
|
|
166
168
|
const server = await serveHttp({
|
package/dist/src/clerk-twin.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
// THE meaty part: session tokens and JWT-template tokens are REAL RS256 JWTs signed from
|
|
13
13
|
// a persisted local keypair, and /.well-known/jwks.json serves the matching JWKS, so an
|
|
14
14
|
// app verifying a twin-issued token against the twin's JWKS succeeds unmodified.
|
|
15
|
-
import { applyTwinWrite, projectResources, nodeBuiltin, resolveSubjectId } from '@volter/world-core';
|
|
15
|
+
import { applyTwinWrite, projectResources, nodeBuiltin, resolveSubjectId, worldNow } from '@volter/world-core';
|
|
16
16
|
import { buildJwks, signJwt, signJwtHs256, verifyJwtWithJwks } from "./clerk-jwt.js";
|
|
17
17
|
import { emitClerkEvent, eventTypeFor } from "./clerk-events.js";
|
|
18
18
|
const SERVICE = 'clerk';
|
|
@@ -53,7 +53,7 @@ function err(message, status = 404, code = 'resource_not_found', longMessage) {
|
|
|
53
53
|
};
|
|
54
54
|
}
|
|
55
55
|
function nowUnixMs(occurredAt) {
|
|
56
|
-
return occurredAt ? Date.parse(occurredAt) : Date.
|
|
56
|
+
return occurredAt ? Date.parse(occurredAt) : Date.parse(worldNow());
|
|
57
57
|
}
|
|
58
58
|
function nodeCrypto() {
|
|
59
59
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/twin-clerk",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Local Clerk twin — a faithful, stateful local Clerk Backend API your real `@clerk/backend` SDK talks to unmodified. Users, sessions, orgs, real signed JWTs + a JWKS endpoint. Mirror, simulate, and fork. Built on @volter/world-core.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"twin",
|
|
@@ -58,10 +58,10 @@
|
|
|
58
58
|
"react-dom": "^19.2.7"
|
|
59
59
|
},
|
|
60
60
|
"peerDependencies": {
|
|
61
|
-
"@volter/world-core": "2.0.
|
|
61
|
+
"@volter/world-core": "2.0.2"
|
|
62
62
|
},
|
|
63
63
|
"devDependencies": {
|
|
64
|
-
"@volter/world-core": "2.0.
|
|
64
|
+
"@volter/world-core": "2.0.2",
|
|
65
65
|
"@volter/world-tooling": "0.1.0",
|
|
66
66
|
"@clerk/backend": "^1.15.0",
|
|
67
67
|
"@types/bun": "^1.2.20",
|
package/src/clerk-events.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { nodeBuiltin } from '@volter/world-core';
|
|
1
|
+
import { nodeBuiltin, worldNow, deliveryTraceHeaders } from '@volter/world-core';
|
|
2
|
+
import { appDestination, appFetch } from '@volter/world-core/app-route';
|
|
2
3
|
import { worldEgressRefusal } from '@volter/world-core/network-policy';
|
|
3
4
|
// Clerk webhook emission — svix-style signed webhooks on user/session/org events.
|
|
4
5
|
//
|
|
@@ -63,7 +64,7 @@ export function verifyWebhook(payload: string, headers: Record<string, string>,
|
|
|
63
64
|
const sigHeader = lower['svix-signature'];
|
|
64
65
|
if (!id || !sigHeader || !Number.isFinite(ts)) throw new ClerkWebhookVerificationError('Missing required svix headers');
|
|
65
66
|
if (opts.tolerance !== undefined) {
|
|
66
|
-
const now = opts.now ?? Math.floor(Date.
|
|
67
|
+
const now = opts.now ?? Math.floor(Date.parse(worldNow()) / 1000);
|
|
67
68
|
if (Math.abs(now - ts) > opts.tolerance) throw new ClerkWebhookVerificationError('Message timestamp too old');
|
|
68
69
|
}
|
|
69
70
|
const expected = computeSvixSignature(id, ts, payload, secret).slice('v1,'.length);
|
|
@@ -121,9 +122,9 @@ export function eventTypeFor(operation: string): string | null {
|
|
|
121
122
|
/** A stable svix message id for one delivery: FNV-1a over the destination, the instant and the body — no process counter (protocol 2). */
|
|
122
123
|
function svixMessageId(url: string, timestamp: number, body: string): string { let h = 0x811c9dc5; const s = `${url}|${timestamp}|${body}`; for (let i = 0; i < s.length; i += 1) { h ^= s.charCodeAt(i); h = Math.imul(h, 0x01000193) >>> 0; } return `msg_twin_${h.toString(16).padStart(8, '0')}`; }
|
|
123
124
|
const httpDelivery: ClerkWebhookDelivery = async (url, body, headers) => {
|
|
124
|
-
if (worldEgressRefusal(url) !== null) return; // the World's egress rule: refused like an unreachable endpoint
|
|
125
|
+
if (!appDestination(url) && worldEgressRefusal(url) !== null) return; // the World's egress rule, the application's own hostnames reaching it inside the World: refused like an unreachable endpoint
|
|
125
126
|
try {
|
|
126
|
-
await
|
|
127
|
+
await appFetch(url, { method: 'POST', headers: { 'content-type': 'application/json', ...headers, ...deliveryTraceHeaders() }, body });
|
|
127
128
|
} catch {
|
|
128
129
|
/* fire-and-forget, like the real vendor */
|
|
129
130
|
}
|
|
@@ -147,7 +148,7 @@ export async function emitClerkEvent(
|
|
|
147
148
|
for (const r of registry) if (!targets.some((t) => t.url === r.url)) targets.push({ ...r });
|
|
148
149
|
if (!type || targets.length === 0) return [];
|
|
149
150
|
const deliver = opts.deliver ?? httpDelivery;
|
|
150
|
-
const timestamp = Math.floor((opts.occurredAt ? Date.parse(opts.occurredAt) : Date.
|
|
151
|
+
const timestamp = Math.floor((opts.occurredAt ? Date.parse(opts.occurredAt) : Date.parse(worldNow())) / 1000) || 0;
|
|
151
152
|
const event: ClerkWebhookEvent = { type, object: 'event', timestamp, data };
|
|
152
153
|
const body = JSON.stringify(event);
|
|
153
154
|
const out: Array<{ url: string; body: string; headers: Record<string, string>; secret: string }> = [];
|
package/src/clerk-jwt.ts
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
// top-level static import, so this module can be pulled into the browser bundle of
|
|
13
13
|
// the UI mirror without Bun's browser target choking on `node:crypto` — the signing
|
|
14
14
|
// helpers are server-only and never reached in the browser path.
|
|
15
|
-
import { applyTwinWrite, projectResources, nodeBuiltin } from '@volter/world-core';
|
|
15
|
+
import { applyTwinWrite, projectResources, nodeBuiltin, worldNow } from '@volter/world-core';
|
|
16
16
|
|
|
17
17
|
type NodeCrypto = typeof import('node:crypto');
|
|
18
18
|
function nodeCrypto(): NodeCrypto {
|
|
@@ -112,7 +112,7 @@ function base64urlJson(obj: unknown): string {
|
|
|
112
112
|
export async function signJwt(claims: Record<string, unknown>, opts: { root?: string; expiresInSeconds?: number; now?: number } = {}): Promise<string> {
|
|
113
113
|
const { createSign } = nodeCrypto();
|
|
114
114
|
const { kid, privateKeyPem } = await ensureKeypair(opts.root);
|
|
115
|
-
const iat = opts.now ?? Math.floor(Date.
|
|
115
|
+
const iat = opts.now ?? Math.floor(Date.parse(worldNow()) / 1000);
|
|
116
116
|
const exp = iat + (opts.expiresInSeconds ?? 60);
|
|
117
117
|
const header = { alg: 'RS256', typ: 'JWT', kid };
|
|
118
118
|
const payload = { iat, exp, nbf: iat, ...claims };
|
|
@@ -133,7 +133,7 @@ export async function signJwt(claims: Record<string, unknown>, opts: { root?: st
|
|
|
133
133
|
*/
|
|
134
134
|
export function signJwtHs256(claims: Record<string, unknown>, secret: string, opts: { expiresInSeconds?: number; now?: number } = {}): string {
|
|
135
135
|
const { createHmac } = nodeCrypto();
|
|
136
|
-
const iat = opts.now ?? Math.floor(Date.
|
|
136
|
+
const iat = opts.now ?? Math.floor(Date.parse(worldNow()) / 1000);
|
|
137
137
|
const exp = iat + (opts.expiresInSeconds ?? 60);
|
|
138
138
|
const header = { alg: 'HS256', typ: 'JWT' };
|
|
139
139
|
const payload = { iat, exp, nbf: iat, ...claims };
|
|
@@ -162,7 +162,7 @@ export function verifyJwtHs256(token: string, secret: string, opts: { now?: numb
|
|
|
162
162
|
const a = Buffer.from(parts[2]!, 'utf8');
|
|
163
163
|
const b = Buffer.from(expected, 'utf8');
|
|
164
164
|
if (a.length !== b.length || !timingSafeEqual(a, b)) return { valid: false, reason: 'bad_signature' };
|
|
165
|
-
const now = opts.now ?? Math.floor(Date.
|
|
165
|
+
const now = opts.now ?? Math.floor(Date.parse(worldNow()) / 1000);
|
|
166
166
|
if (typeof payload.exp === 'number' && now >= payload.exp) return { valid: false, reason: 'expired' };
|
|
167
167
|
if (typeof payload.nbf === 'number' && now < payload.nbf) return { valid: false, reason: 'not_yet_valid' };
|
|
168
168
|
return { valid: true, payload };
|
|
@@ -202,7 +202,7 @@ export function verifyJwtWithJwks(token: string, jwks: Jwks, opts: { now?: numbe
|
|
|
202
202
|
verifier.end();
|
|
203
203
|
const sig = Buffer.from(parts[2]!.replace(/-/g, '+').replace(/_/g, '/'), 'base64');
|
|
204
204
|
if (!verifier.verify(publicKey, sig)) return { valid: false, reason: 'bad_signature' };
|
|
205
|
-
const now = opts.now ?? Math.floor(Date.
|
|
205
|
+
const now = opts.now ?? Math.floor(Date.parse(worldNow()) / 1000);
|
|
206
206
|
if (typeof payload.exp === 'number' && now >= payload.exp) return { valid: false, reason: 'expired' };
|
|
207
207
|
if (typeof payload.nbf === 'number' && now < payload.nbf) return { valid: false, reason: 'not_yet_valid' };
|
|
208
208
|
return { valid: true, payload };
|
package/src/clerk-mirror-ui.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// pack's OWN fetch adapter (createClerkTwinFetch) as its API backend and reads every byte of
|
|
7
7
|
// state back over the wire, exactly as the browser client does.
|
|
8
8
|
import { readFile } from 'node:fs/promises';
|
|
9
|
-
import { bundleClient, fileResponse } from '@volter/world-core';
|
|
9
|
+
import { bundleClient, fileResponse, filePathOf } from '@volter/world-core';
|
|
10
10
|
import { serveHttp } from '@volter/world-core';
|
|
11
11
|
import { createClerkTwinFetch } from './clerk-server.ts';
|
|
12
12
|
|
|
@@ -15,8 +15,8 @@ import { createClerkTwinFetch } from './clerk-server.ts';
|
|
|
15
15
|
// `import.meta.url` is not a valid file base (e.g. workerd) even though the mirror UI is never
|
|
16
16
|
// served there. Computing them on demand keeps Node/Bun behavior identical while letting a
|
|
17
17
|
// serverless host import this module's barrel without a startup crash.
|
|
18
|
-
const clientEntryPath = () => new URL('../client/clerk-mirror.tsx', import.meta.url)
|
|
19
|
-
const clientCssPath = () => new URL('../client/clerk-mirror.css', import.meta.url)
|
|
18
|
+
const clientEntryPath = () => filePathOf(new URL('../client/clerk-mirror.tsx', import.meta.url));
|
|
19
|
+
const clientCssPath = () => filePathOf(new URL('../client/clerk-mirror.css', import.meta.url));
|
|
20
20
|
|
|
21
21
|
// ---------------------------------------------------------------------------
|
|
22
22
|
// Pure, dependency-free render/format helpers. Framework-agnostic (plain data in →
|
package/src/clerk-server.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// Personas are world state: pass `personas` (a JSON file of users with passwords) and the
|
|
8
8
|
// server seeds them as ordinary user rows at start — sign-in is a protocol, never a default.
|
|
9
9
|
// JSON bodies pass through to the handler. Writable by default; readOnly rejects writes (R4).
|
|
10
|
-
import { serveHttp } from '@volter/world-core';
|
|
10
|
+
import { runWithRequestTrace, serveHttp } from '@volter/world-core';
|
|
11
11
|
import { handleClerkTwinRequest } from './clerk-twin.ts';
|
|
12
12
|
import { worldNow, statefulTwinManifest, packAsset } from '@volter/world-core';
|
|
13
13
|
import { handleClerkFapiRequest } from './clerk-fapi.ts';
|
|
@@ -97,7 +97,9 @@ export function createClerkTwinFetch(options: ClerkTwinFetchOptions = {}): (requ
|
|
|
97
97
|
// the pack CONSTRUCTS on any host (workerd included) and only the local-CDN route itself needs
|
|
98
98
|
// Bun — until the dist rides a pack-asset seam (`TwinPack.assets`, the pack-asset seam), that route is the
|
|
99
99
|
// pack's one host-bound surface, not the whole pack.
|
|
100
|
-
|
|
100
|
+
// a request's W3C trace context scopes everything it writes and delivers (world-core trace-context)
|
|
101
|
+
return (request: Request): Promise<Response> => runWithRequestTrace(request, () => clerkTwinFetch(request));
|
|
102
|
+
async function clerkTwinFetch(request: Request): Promise<Response> {
|
|
101
103
|
await seeded;
|
|
102
104
|
const url = new URL(request.url);
|
|
103
105
|
// GET /twin — the discovery manifest (education inside the twin).
|
|
@@ -162,7 +164,7 @@ export function createClerkTwinFetch(options: ClerkTwinFetchOptions = {}): (requ
|
|
|
162
164
|
...(options.root !== undefined ? { root: options.root } : {}),
|
|
163
165
|
});
|
|
164
166
|
return new Response(JSON.stringify(out), { status, headers: { 'content-type': 'application/json', 'clerk-trace-id': 'trace_twin', ...cors } });
|
|
165
|
-
}
|
|
167
|
+
}
|
|
166
168
|
}
|
|
167
169
|
|
|
168
170
|
export async function createClerkTwinServer(options: { root?: string; port?: number; readOnly?: boolean; personas?: Array<Record<string, unknown>> }): Promise<{ port: number; stop: () => void }> {
|
package/src/clerk-twin.ts
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
// THE meaty part: session tokens and JWT-template tokens are REAL RS256 JWTs signed from
|
|
13
13
|
// a persisted local keypair, and /.well-known/jwks.json serves the matching JWKS, so an
|
|
14
14
|
// app verifying a twin-issued token against the twin's JWKS succeeds unmodified.
|
|
15
|
-
import { applyTwinWrite, projectResources, nodeBuiltin, resolveSubjectId } from '@volter/world-core';
|
|
15
|
+
import { applyTwinWrite, projectResources, nodeBuiltin, resolveSubjectId, worldNow } from '@volter/world-core';
|
|
16
16
|
import { buildJwks, signJwt, signJwtHs256, verifyJwtWithJwks } from './clerk-jwt.ts';
|
|
17
17
|
import { emitClerkEvent, eventTypeFor, type ClerkWebhookTarget } from './clerk-events.ts';
|
|
18
18
|
|
|
@@ -71,7 +71,7 @@ function err(message: string, status = 404, code = 'resource_not_found', longMes
|
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
function nowUnixMs(occurredAt?: string): number {
|
|
74
|
-
return occurredAt ? Date.parse(occurredAt) : Date.
|
|
74
|
+
return occurredAt ? Date.parse(occurredAt) : Date.parse(worldNow());
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
// ── password hashing (REAL crypto) ──────────────────────────────────────────────
|