arcway 0.1.30 → 0.2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arcway",
3
- "version": "0.1.30",
3
+ "version": "0.2.1",
4
4
  "description": "A convention-based framework for building modular monoliths with strict domain boundaries.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/server/bin/cli.js CHANGED
@@ -12,6 +12,7 @@ import registerGraphQLSchema from './commands/graphql-schema.js';
12
12
  import registerSchema from './commands/schema.js';
13
13
  import registerMigrate from './commands/migrate.js';
14
14
  import registerBootstrap from './commands/bootstrap.js';
15
+ import registerVault from './commands/vault.js';
15
16
 
16
17
  function getPackageVersion() {
17
18
  try {
@@ -38,6 +39,7 @@ function createProgram() {
38
39
  registerSchema(program);
39
40
  registerMigrate(program);
40
41
  registerBootstrap(program);
42
+ registerVault(program);
41
43
  return program;
42
44
  }
43
45
 
@@ -1,23 +1,28 @@
1
1
  import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
+ import { encodeMasterSecret } from '../../lib/vault/index.js';
3
4
 
4
- const FILES = {
5
- 'arcway.config.js': `export default {
5
+ function createFiles(masterSecret) {
6
+ return {
7
+ 'arcway.config.js': `export default {
6
8
  database: {
7
9
  client: 'better-sqlite3',
8
10
  connection: { filename: './data.db' },
9
11
  },
12
+ vault: {
13
+ masterSecret: process.env.ARCWAY_MASTER_SECRET,
14
+ },
10
15
  };
11
16
  `,
12
17
 
13
- 'api/index.js': `export const GET = {
18
+ 'api/index.js': `export const GET = {
14
19
  handler: async (ctx) => {
15
20
  return { data: { message: 'Hello from Arcway!' } };
16
21
  },
17
22
  };
18
23
  `,
19
24
 
20
- 'api/users/index.js': `export const GET = {
25
+ 'api/users/index.js': `export const GET = {
21
26
  handler: async (ctx) => {
22
27
  const { db } = ctx;
23
28
  const users = await db('users').select('*');
@@ -38,7 +43,7 @@ export const POST = {
38
43
  };
39
44
  `,
40
45
 
41
- 'api/users/[id].js': `export const GET = {
46
+ 'api/users/[id].js': `export const GET = {
42
47
  handler: async (ctx) => {
43
48
  const { req, db } = ctx;
44
49
  const user = await db('users').where('id', req.query.id).first();
@@ -48,13 +53,13 @@ export const POST = {
48
53
  };
49
54
  `,
50
55
 
51
- 'listeners/users/created.js': `export default async (ctx) => {
56
+ 'listeners/users/created.js': `export default async (ctx) => {
52
57
  const { event, log } = ctx;
53
58
  log.info('New user created', { userId: event.payload.id });
54
59
  };
55
60
  `,
56
61
 
57
- 'jobs/cleanup-sessions.js': `export default {
62
+ 'jobs/cleanup-sessions.js': `export default {
58
63
  schedule: '0 3 * * *', // Every day at 3 AM
59
64
  handler: async (ctx) => {
60
65
  const { db, log } = ctx;
@@ -66,7 +71,7 @@ export const POST = {
66
71
  };
67
72
  `,
68
73
 
69
- 'migrations/202601010000-create-users.js': `export async function up(knex) {
74
+ 'migrations/202601010000-create-users.js': `export async function up(knex) {
70
75
  await knex.schema.createTable('users', (table) => {
71
76
  table.increments('id').primary();
72
77
  table.string('name').notNullable();
@@ -80,7 +85,7 @@ export async function down(knex) {
80
85
  }
81
86
  `,
82
87
 
83
- 'pages/index.jsx': `export default function Home() {
88
+ 'pages/index.jsx': `export default function Home() {
84
89
  return (
85
90
  <div style={{ padding: '2rem', fontFamily: 'system-ui' }}>
86
91
  <h1>Welcome to Arcway</h1>
@@ -90,23 +95,25 @@ export async function down(knex) {
90
95
  }
91
96
  `,
92
97
 
93
- '.env': `# NODE_ENV=development
94
- # SESSION_PASSWORD=change-me-to-a-random-32-char-string
98
+ '.env': `# NODE_ENV=development
99
+ ARCWAY_MASTER_SECRET=${masterSecret}
95
100
  `,
96
101
 
97
- '.gitignore': `node_modules/
102
+ '.gitignore': `node_modules/
98
103
  .build/
99
104
  data.db
100
105
  .env.local
101
106
  .env.*.local
102
107
  `,
103
- };
108
+ };
109
+ }
104
110
 
105
111
  function bootstrap(rootDir) {
112
+ const files = createFiles(encodeMasterSecret());
106
113
  let created = 0;
107
114
  let skipped = 0;
108
115
 
109
- for (const [relativePath, content] of Object.entries(FILES)) {
116
+ for (const [relativePath, content] of Object.entries(files)) {
110
117
  const fullPath = join(rootDir, relativePath);
111
118
  if (existsSync(fullPath)) {
112
119
  console.log(` skip ${relativePath} (already exists)`);
@@ -0,0 +1,14 @@
1
+ import { encodeMasterSecret } from '../../lib/vault/index.js';
2
+
3
+ function registerVault(program) {
4
+ const vault = program.command('vault').description('Vault utilities');
5
+
6
+ vault
7
+ .command('generate-key')
8
+ .description('Generate a new Arcway master secret')
9
+ .action(() => {
10
+ console.log(encodeMasterSecret());
11
+ });
12
+ }
13
+
14
+ export default registerVault;
@@ -28,7 +28,10 @@ async function boot(options) {
28
28
  const envFiles = loadEnvFiles(rootDir, mode);
29
29
  const config = await makeConfig(rootDir, { overrides: options.configOverrides, mode });
30
30
 
31
- const plugins = await createPluginManager(config.plugins, { runLifecycle: false });
31
+ const plugins = await createPluginManager(config.plugins, {
32
+ runLifecycle: false,
33
+ bundle: mode === 'development',
34
+ });
32
35
 
33
36
  const infrastructure = await createInfrastructure(config, { runMigrations: true });
34
37
  const { db, redis, queue, cache, files, mail, events, log } = infrastructure;
@@ -43,7 +46,19 @@ async function boot(options) {
43
46
  // framework services and to the init/ready/shutdown hooks. Mutations from
44
47
  // the init hook (e.g. wrapping `appContext.db`) persist for process lifetime
45
48
  // because downstream consumers read properties off this reference at use time.
46
- const appContext = { db, redis, events, queue, cache, files, mail, log, fileWatcher, meta };
49
+ const appContext = {
50
+ db,
51
+ redis,
52
+ events,
53
+ queue,
54
+ cache,
55
+ files,
56
+ mail,
57
+ log,
58
+ fileWatcher,
59
+ meta,
60
+ vault: config.vault,
61
+ };
47
62
 
48
63
  plugins.log = log;
49
64
  appContext.plugins = plugins;
@@ -82,6 +97,7 @@ async function boot(options) {
82
97
  events,
83
98
  log,
84
99
  meta,
100
+ vault: config.vault,
85
101
  workerPool,
86
102
  plugins,
87
103
  });
@@ -97,6 +113,7 @@ async function boot(options) {
97
113
  sessionConfig: config.session,
98
114
  });
99
115
  await apiRouter.init();
116
+ if (fileWatcher) plugins.watch(fileWatcher, { apiRouter, jobRunner });
100
117
 
101
118
  const pagesRouter = new PagesRouter(config, { rootDir, log, mode, fileWatcher, appContext });
102
119
  await pagesRouter.init();
@@ -153,6 +170,7 @@ async function boot(options) {
153
170
  if (workerPool) await workerPool.shutdown();
154
171
  await pagesRouter.close();
155
172
  await apiRouter.close();
173
+ if (fileWatcher) plugins.unwatch(fileWatcher);
156
174
  if (viteRouter) await viteRouter.close();
157
175
  await webServer.close();
158
176
  if (fileWatcher) await fileWatcher.close();
@@ -12,7 +12,7 @@ import resolveRedis from './modules/redis.js';
12
12
  import resolveCache from './modules/cache.js';
13
13
  import resolveEvents from './modules/events.js';
14
14
  import resolveMail from './modules/mail.js';
15
- import resolveSecrets from './modules/secrets.js';
15
+ import resolveVault from './modules/vault.js';
16
16
  import resolveSession from './modules/session.js';
17
17
  import resolvePages from './modules/pages.js';
18
18
  import resolveBuild from './modules/build.js';
@@ -51,7 +51,7 @@ const modules = [
51
51
  resolveFiles,
52
52
  resolveMail,
53
53
  resolveRedis,
54
- resolveSecrets,
54
+ resolveVault,
55
55
  resolveSession,
56
56
  resolvePages,
57
57
  resolveBuild,
@@ -4,8 +4,8 @@ const DEFAULTS = {
4
4
 
5
5
  function resolve(config) {
6
6
  const mcp = { ...DEFAULTS, ...config.mcp };
7
- if (!mcp.secret && config.secrets?.values?.['mcp-api']) {
8
- mcp.secret = config.secrets.values['mcp-api'];
7
+ if (!mcp.secret && config.vault?.values?.['mcp-api']) {
8
+ mcp.secret = config.vault.values['mcp-api'];
9
9
  }
10
10
  return { ...config, mcp };
11
11
  }
@@ -2,9 +2,15 @@ import { resolveSessionConfig } from '../../session/index.js';
2
2
 
3
3
  function resolve(config, { mode } = {}) {
4
4
  if (!config.session) return config;
5
+ const vaultSession = config.vault?.values?.session;
6
+ const sessionKeyring = config.vault?.keyring?.map((entry) => ({
7
+ id: entry.id,
8
+ password: entry.secrets.session,
9
+ }));
5
10
  const sessionInput = {
6
11
  ...config.session,
7
- password: config.session.password || config.secrets?.values?.session,
12
+ password: vaultSession,
13
+ keyring: sessionKeyring,
8
14
  };
9
15
  const session = resolveSessionConfig(sessionInput, mode);
10
16
  return { ...config, session };
@@ -0,0 +1,44 @@
1
+ import {
2
+ deriveInfraSecretRing,
3
+ deriveInfraSecrets,
4
+ normalizeMasterSecretRing,
5
+ } from '../../lib/vault/secrets.js';
6
+
7
+ function loadMasterSecret(rawVault) {
8
+ if (rawVault.masterSecret !== undefined && rawVault.masterSecret !== null) {
9
+ return { value: rawVault.masterSecret, source: 'config' };
10
+ }
11
+
12
+ throw new Error(
13
+ 'vault.masterSecret is required when Arcway vault is enabled. Generate one with `npx arcway vault generate-key`.',
14
+ );
15
+ }
16
+
17
+ function resolve(config) {
18
+ if (!config.vault) return config;
19
+
20
+ const rawVault = config.vault;
21
+ const master = loadMasterSecret(rawVault);
22
+ normalizeMasterSecretRing(master.value);
23
+
24
+ const ring = deriveInfraSecretRing(master.value);
25
+ const derived = deriveInfraSecrets(
26
+ Array.isArray(master.value) ? master.value.at(-1) : master.value,
27
+ );
28
+ const values = ring.active.secrets;
29
+
30
+ return {
31
+ ...config,
32
+ vault: {
33
+ enabled: true,
34
+ masterSecret: Array.isArray(master.value) ? master.value.at(-1) : master.value,
35
+ source: master.source,
36
+ values,
37
+ derived,
38
+ keyring: ring.entries,
39
+ activeKey: ring.active,
40
+ },
41
+ };
42
+ }
43
+
44
+ export default resolve;
package/server/context.js CHANGED
@@ -1,16 +1,28 @@
1
1
  import * as vault from './lib/vault/index.js';
2
+ import { createJwtCodec } from './lib/vault/jwt.js';
2
3
  import { wsBroadcastToPath, wsSendToSocket } from './ws/registry.js';
3
4
 
4
5
  function trackDb(db) {
5
- if (!db || typeof db !== 'function' && typeof db !== 'object') return { db, stats: { queries: 0, durationMs: 0 } };
6
+ if (!db || (typeof db !== 'function' && typeof db !== 'object'))
7
+ return { db, stats: { queries: 0, durationMs: 0 } };
6
8
  const stats = { queries: 0, durationMs: 0 };
7
9
  const handler = {
8
10
  get(target, prop, receiver) {
9
11
  const value = Reflect.get(target, prop, receiver);
10
12
  if (typeof value !== 'function') return value;
11
- if (prop === 'raw' || prop === 'select' || prop === 'insert' || prop === 'update' ||
12
- prop === 'delete' || prop === 'del' || prop === 'from' || prop === 'into' ||
13
- prop === 'table' || prop === 'with' || prop === 'withRecursive') {
13
+ if (
14
+ prop === 'raw' ||
15
+ prop === 'select' ||
16
+ prop === 'insert' ||
17
+ prop === 'update' ||
18
+ prop === 'delete' ||
19
+ prop === 'del' ||
20
+ prop === 'from' ||
21
+ prop === 'into' ||
22
+ prop === 'table' ||
23
+ prop === 'with' ||
24
+ prop === 'withRecursive'
25
+ ) {
14
26
  return (...args) => {
15
27
  const builder = value.apply(target, args);
16
28
  return wrapBuilder(builder, stats);
@@ -101,6 +113,18 @@ function normalizeMeta(meta) {
101
113
  };
102
114
  }
103
115
 
116
+ function buildVaultToolkit(vaultConfig) {
117
+ if (!vaultConfig?.masterSecret) return vault;
118
+ const jwtCodec = createJwtCodec(vaultConfig.values?.jwt);
119
+
120
+ return {
121
+ ...vault,
122
+ ...jwtCodec,
123
+ deriveKey: (label) => vault.deriveKey(vaultConfig.masterSecret, label),
124
+ deriveKeyPair: (label, opts) => vault.deriveKeyPair(vaultConfig.masterSecret, label, opts),
125
+ };
126
+ }
127
+
104
128
  function buildContext(appContext, extras) {
105
129
  const tracked = trackDb(appContext.db);
106
130
  const normalizedExtras = { ...extras };
@@ -124,12 +148,13 @@ function buildContext(appContext, extras) {
124
148
  files: appContext.files,
125
149
  mail: appContext.mail,
126
150
  meta: normalizeMeta(appContext.meta),
127
- vault,
151
+ vault: buildVaultToolkit(appContext.vault),
128
152
  ws,
129
153
  err: (input, status) => buildErrResponse(input, status),
130
154
  _dbStats: tracked.stats,
131
155
  ...normalizedExtras,
132
156
  };
157
+ ctx.plugins = appContext.plugins?.buildContextProxy?.(ctx);
133
158
  return ctx;
134
159
  }
135
160
 
@@ -5,8 +5,7 @@ import { toError, calculateBackoff } from './queue.js';
5
5
  import JobDispatcher from './drivers/memory-queue.js';
6
6
  import { SYSTEM_JOB_DOMAIN, registerSystemJobs } from '../system-jobs/index.js';
7
7
 
8
- async function discoverJobs(jobsDir) {
9
- const entries = await discoverModules(jobsDir, { recursive: true, label: 'job file' });
8
+ function buildJobsFromModules(entries) {
10
9
  const jobs = [];
11
10
  for (const { name, filePath, relativePath, module } of entries) {
12
11
  const job = module.default;
@@ -31,6 +30,11 @@ async function discoverJobs(jobsDir) {
31
30
  return jobs;
32
31
  }
33
32
 
33
+ async function discoverJobs(jobsDir) {
34
+ const entries = await discoverModules(jobsDir, { recursive: true, label: 'job file' });
35
+ return buildJobsFromModules(entries);
36
+ }
37
+
34
38
  class JobRunner {
35
39
  _dispatcher;
36
40
  _config;
@@ -42,11 +46,12 @@ class JobRunner {
42
46
  _timer = null;
43
47
  _started = false;
44
48
  _stopped = false;
49
+ _runGeneration = 0;
45
50
  _lastEnqueuedMinute = new Map();
46
51
 
47
52
  constructor(
48
53
  config,
49
- { db, queue, cache, files, mail, events, log, meta, workerPool, plugins } = {},
54
+ { db, queue, cache, files, mail, events, log, meta, vault, workerPool, plugins } = {},
50
55
  ) {
51
56
  this._config = config;
52
57
  this._log = log;
@@ -55,7 +60,7 @@ class JobRunner {
55
60
  staleTimeoutMs: config?.staleTimeoutMs,
56
61
  workerPool,
57
62
  });
58
- this._appContext = { db, queue, cache, files, mail, events, log, meta, plugins };
63
+ this._appContext = { db, queue, cache, files, mail, events, log, meta, vault, plugins };
59
64
  }
60
65
 
61
66
  get dispatcher() {
@@ -135,10 +140,23 @@ class JobRunner {
135
140
  }
136
141
  }
137
142
 
143
+ async reload() {
144
+ const wasStarted = this._started;
145
+ this.stop();
146
+ this._jobs = [];
147
+ this._cronEntries = [];
148
+ this._continuousJobs = [];
149
+ this._lastEnqueuedMinute = new Map();
150
+ this._dispatcher.registered.clear();
151
+ await this.init();
152
+ if (wasStarted) this.start();
153
+ }
154
+
138
155
  start(intervalMs) {
139
156
  if (this._started) return;
140
157
  this._started = true;
141
158
  this._stopped = false;
159
+ const generation = ++this._runGeneration;
142
160
  const pollMs = intervalMs ?? this._config?.pollIntervalMs ?? 60000;
143
161
  this._timer = setInterval(() => {
144
162
  this.tick().catch((err) => {
@@ -146,7 +164,7 @@ class JobRunner {
146
164
  });
147
165
  }, pollMs);
148
166
  for (const entry of this._continuousJobs) {
149
- this._runContinuousLoop(entry);
167
+ this._runContinuousLoop(entry, generation);
150
168
  }
151
169
 
152
170
  const parts = [];
@@ -158,6 +176,7 @@ class JobRunner {
158
176
  stop() {
159
177
  this._stopped = true;
160
178
  this._started = false;
179
+ this._runGeneration++;
161
180
  if (this._timer) {
162
181
  clearInterval(this._timer);
163
182
  this._timer = null;
@@ -188,12 +207,12 @@ class JobRunner {
188
207
  await this._dispatcher.process();
189
208
  }
190
209
 
191
- async _runContinuousLoop(entry) {
210
+ async _runContinuousLoop(entry, generation) {
192
211
  const backoffMs = this._config?.backoffMs ?? 1000;
193
212
  const cooldownMs = entry.cooldownMs ?? this._config?.cooldownMs ?? 1000;
194
213
  let consecutiveErrors = 0;
195
214
  console.log(`[job-runner] continuous: ${entry.qualifiedName} started`);
196
- while (!this._stopped) {
215
+ while (!this._stopped && generation === this._runGeneration) {
197
216
  try {
198
217
  const start = Date.now();
199
218
  const ctx = buildContext(entry.appContext, { payload: void 0 });
@@ -206,7 +225,7 @@ class JobRunner {
206
225
  if (cooldownMs > 0 && elapsed < cooldownMs) {
207
226
  const remaining = cooldownMs - elapsed;
208
227
  const deadline = Date.now() + remaining;
209
- while (!this._stopped && Date.now() < deadline) {
228
+ while (!this._stopped && generation === this._runGeneration && Date.now() < deadline) {
210
229
  await new Promise((r) => setTimeout(r, Math.min(100, deadline - Date.now())));
211
230
  }
212
231
  }
@@ -219,7 +238,7 @@ class JobRunner {
219
238
  error.message,
220
239
  );
221
240
  const deadline = Date.now() + delay;
222
- while (!this._stopped && Date.now() < deadline) {
241
+ while (!this._stopped && generation === this._runGeneration && Date.now() < deadline) {
223
242
  await new Promise((r) => setTimeout(r, Math.min(100, deadline - Date.now())));
224
243
  }
225
244
  }
@@ -228,4 +247,4 @@ class JobRunner {
228
247
  }
229
248
  }
230
249
 
231
- export { JobRunner, discoverJobs };
250
+ export { JobRunner, buildJobsFromModules, discoverJobs };
@@ -17,13 +17,17 @@ if (!parentPort) {
17
17
  const moduleCache = new Map();
18
18
  let infrastructure = null;
19
19
  let infrastructurePromise = null;
20
+ let resolvedConfig = null;
20
21
  let shuttingDown = false;
21
22
 
22
23
  async function ensureInfrastructure() {
23
24
  if (infrastructure) return infrastructure;
24
25
  if (!infrastructurePromise) {
25
26
  infrastructurePromise = resolveWorkerConfig(workerData)
26
- .then((config) => createInfrastructure(config, { runMigrations: false }))
27
+ .then((config) => {
28
+ resolvedConfig = config;
29
+ return createInfrastructure(config, { runMigrations: false });
30
+ })
27
31
  .then((services) => {
28
32
  infrastructure = services;
29
33
  return services;
@@ -66,7 +70,10 @@ async function runTask(task) {
66
70
  // and any future cpu-only caller) gets the raw payload — no DB spin-up.
67
71
  if (withContext) {
68
72
  const services = await ensureInfrastructure();
69
- const ctx = buildContext({ ...services, meta: workerData?.meta }, { payload });
73
+ const ctx = buildContext(
74
+ { ...services, meta: workerData?.meta, vault: resolvedConfig?.vault },
75
+ { payload },
76
+ );
70
77
  return fn(ctx);
71
78
  }
72
79
  return fn(payload);
@@ -4,22 +4,28 @@ import { encrypt, decrypt } from './encrypt.js';
4
4
  import { jwtEncode, jwtDecode } from './jwt.js';
5
5
  import {
6
6
  decryptWithRotation,
7
+ deriveKey,
8
+ deriveKeyPair,
7
9
  deriveBytes,
8
10
  deriveInfraSecret,
11
+ deriveInfraSecretRing,
9
12
  deriveInfraSecrets,
10
- deriveVapidKeyPair,
11
13
  encodeMasterSecret,
12
14
  formatVaultKey,
13
15
  normalizeMasterSecret,
16
+ normalizeMasterSecretRing,
17
+ stableSessionPasswordId,
14
18
  validateVaultKey,
15
19
  } from './secrets.js';
16
20
  export {
17
21
  decryptWithRotation,
18
22
  decrypt,
23
+ deriveKey,
24
+ deriveKeyPair,
19
25
  deriveBytes,
20
26
  deriveInfraSecret,
27
+ deriveInfraSecretRing,
21
28
  deriveInfraSecrets,
22
- deriveVapidKeyPair,
23
29
  encodeMasterSecret,
24
30
  encrypt,
25
31
  formatVaultKey,
@@ -29,6 +35,8 @@ export {
29
35
  jwtDecode,
30
36
  jwtEncode,
31
37
  normalizeMasterSecret,
38
+ normalizeMasterSecretRing,
39
+ stableSessionPasswordId,
32
40
  validateVaultKey,
33
41
  verifyPassword,
34
42
  };
@@ -1,19 +1,26 @@
1
- import { SignJWT, jwtVerify, errors } from 'jose';
1
+ import { SignJWT, jwtVerify } from 'jose';
2
+
2
3
  function secretToKey(secret) {
3
4
  return new TextEncoder().encode(secret);
4
5
  }
6
+
5
7
  function parseExpiration(expiresIn) {
6
8
  if (typeof expiresIn === 'number') {
7
9
  return `${expiresIn}s`;
8
10
  }
9
11
  return expiresIn;
10
12
  }
11
- async function jwtEncode(payload, secret, options) {
13
+
14
+ function requireJwtSecret(secret) {
12
15
  if (!secret) {
13
- throw new Error('JWT secret is required');
16
+ throw new Error('JWT key is required');
14
17
  }
18
+ return secret;
19
+ }
20
+
21
+ async function encodeWithSecret(payload, secret, options) {
15
22
  const alg = options?.algorithm ?? 'HS256';
16
- const key = secretToKey(secret);
23
+ const key = secretToKey(requireJwtSecret(secret));
17
24
  let builder = new SignJWT(payload).setProtectedHeader({ alg }).setIssuedAt();
18
25
  if (options?.expiresIn !== void 0) {
19
26
  builder = builder.setExpirationTime(parseExpiration(options.expiresIn));
@@ -26,11 +33,9 @@ async function jwtEncode(payload, secret, options) {
26
33
  }
27
34
  return builder.sign(key);
28
35
  }
29
- async function jwtDecode(token, secret, options) {
30
- if (!secret) {
31
- throw new Error('JWT secret is required');
32
- }
33
- const key = secretToKey(secret);
36
+
37
+ async function decodeWithSecret(token, secret, options) {
38
+ const key = secretToKey(requireJwtSecret(secret));
34
39
  const algorithms = options?.algorithms ?? ['HS256'];
35
40
  try {
36
41
  const { payload } = await jwtVerify(token, key, {
@@ -39,17 +44,25 @@ async function jwtDecode(token, secret, options) {
39
44
  audience: options?.audience,
40
45
  });
41
46
  return payload;
42
- } catch (err) {
43
- if (err instanceof errors.JWTExpired) {
44
- throw new Error('JWT token has expired');
45
- }
46
- if (err instanceof errors.JWSSignatureVerificationFailed) {
47
- throw new Error('JWT signature verification failed');
48
- }
49
- if (err instanceof errors.JWTClaimValidationFailed) {
50
- throw new Error(`JWT claim validation failed: ${err.message}`);
51
- }
52
- throw new Error(`JWT verification failed: ${err.message}`);
47
+ } catch {
48
+ throw new Error('JWT verification failed');
53
49
  }
54
50
  }
55
- export { jwtDecode, jwtEncode };
51
+
52
+ function createJwtCodec(secret) {
53
+ const jwtSecret = requireJwtSecret(secret);
54
+ return {
55
+ jwtEncode: (payload, options) => encodeWithSecret(payload, jwtSecret, options),
56
+ jwtDecode: (token, options) => decodeWithSecret(token, jwtSecret, options),
57
+ };
58
+ }
59
+
60
+ async function jwtEncode(_payload, _options) {
61
+ throw new Error('JWT key is required; use ctx.vault.jwtEncode() with configured vault');
62
+ }
63
+
64
+ async function jwtDecode(_token, _options) {
65
+ throw new Error('JWT key is required; use ctx.vault.jwtDecode() with configured vault');
66
+ }
67
+
68
+ export { createJwtCodec, jwtDecode, jwtEncode };