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 +1 -1
- package/server/bin/cli.js +2 -0
- package/server/bin/commands/bootstrap.js +21 -14
- package/server/bin/commands/vault.js +14 -0
- package/server/boot/index.js +20 -2
- package/server/config/loader.js +2 -2
- package/server/config/modules/mcp.js +2 -2
- package/server/config/modules/session.js +7 -1
- package/server/config/modules/vault.js +44 -0
- package/server/context.js +30 -5
- package/server/jobs/runner.js +29 -10
- package/server/jobs/worker-entry.js +9 -2
- package/server/lib/vault/index.js +10 -2
- package/server/lib/vault/jwt.js +34 -21
- package/server/lib/vault/secrets.js +64 -17
- package/server/plugins/bundle.js +122 -0
- package/server/plugins/discovery.js +45 -6
- package/server/plugins/graph.js +22 -8
- package/server/plugins/manager.js +196 -11
- package/server/plugins/manifest.js +27 -31
- package/server/router/api-router.js +4 -0
- package/server/router/routes.js +7 -2
- package/server/session/index.js +57 -15
- package/server/testing/index.js +1 -0
- package/server/config/modules/secrets.js +0 -145
package/package.json
CHANGED
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
|
-
|
|
5
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
94
|
-
|
|
98
|
+
'.env': `# NODE_ENV=development
|
|
99
|
+
ARCWAY_MASTER_SECRET=${masterSecret}
|
|
95
100
|
`,
|
|
96
101
|
|
|
97
|
-
|
|
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(
|
|
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;
|
package/server/boot/index.js
CHANGED
|
@@ -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, {
|
|
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 = {
|
|
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();
|
package/server/config/loader.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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.
|
|
8
|
-
mcp.secret = config.
|
|
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:
|
|
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')
|
|
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 (
|
|
12
|
-
|
|
13
|
-
|
|
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
|
|
package/server/jobs/runner.js
CHANGED
|
@@ -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
|
-
|
|
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) =>
|
|
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(
|
|
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
|
};
|
package/server/lib/vault/jwt.js
CHANGED
|
@@ -1,19 +1,26 @@
|
|
|
1
|
-
import { SignJWT, jwtVerify
|
|
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
|
-
|
|
13
|
+
|
|
14
|
+
function requireJwtSecret(secret) {
|
|
12
15
|
if (!secret) {
|
|
13
|
-
throw new Error('JWT
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
|
43
|
-
|
|
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
|
-
|
|
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 };
|