@volter/twin-clerk 0.1.0 → 0.1.1
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 +228 -158
- package/dist/src/clerk-events.js +7 -6
- package/dist/src/clerk-jwt.js +5 -5
- 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-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 } 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 }, 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)
|
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.1",
|
|
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.1"
|
|
62
62
|
},
|
|
63
63
|
"devDependencies": {
|
|
64
|
-
"@volter/world-core": "2.0.
|
|
64
|
+
"@volter/world-core": "2.0.1",
|
|
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 } 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 }, 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-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) ──────────────────────────────────────────────
|