@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.
@@ -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.now() / 1000);
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 fetch(url, { method: 'POST', headers: { 'content-type': 'application/json', ...headers }, body });
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.now()) / 1000) || 0;
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 = [];
@@ -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.now() / 1000);
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.now() / 1000);
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.now() / 1000);
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.now() / 1000);
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)
@@ -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.now();
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.0",
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.0"
61
+ "@volter/world-core": "2.0.1"
62
62
  },
63
63
  "devDependencies": {
64
- "@volter/world-core": "2.0.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",
@@ -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.now() / 1000);
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 fetch(url, { method: 'POST', headers: { 'content-type': 'application/json', ...headers }, body });
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.now()) / 1000) || 0;
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.now() / 1000);
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.now() / 1000);
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.now() / 1000);
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.now() / 1000);
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.now();
74
+ return occurredAt ? Date.parse(occurredAt) : Date.parse(worldNow());
75
75
  }
76
76
 
77
77
  // ── password hashing (REAL crypto) ──────────────────────────────────────────────