arcway 0.1.27 → 0.1.29
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/boot/index.js +10 -0
- package/server/build.js +12 -1
- package/server/config/loader.js +4 -0
- package/server/config/modules/mcp.js +5 -1
- package/server/config/modules/plugins.js +18 -0
- package/server/config/modules/secrets.js +145 -0
- package/server/config/modules/session.js +5 -1
- package/server/db/index.js +33 -8
- package/server/discovery.js +45 -3
- package/server/index.js +2 -0
- package/server/internals.js +8 -0
- package/server/jobs/drivers/run-handler.js +3 -0
- package/server/jobs/runner.js +12 -4
- package/server/lib/vault/index.js +20 -0
- package/server/lib/vault/secrets.js +149 -0
- package/server/plugins/capabilities.js +66 -0
- package/server/plugins/discovery.js +111 -0
- package/server/plugins/graph.js +114 -0
- package/server/plugins/manager.js +210 -0
- package/server/plugins/manifest.js +94 -0
- package/server/router/api-router.js +43 -17
- package/server/router/routes.js +14 -2
package/package.json
CHANGED
package/server/boot/index.js
CHANGED
|
@@ -18,6 +18,7 @@ import { runHook } from './hooks.js';
|
|
|
18
18
|
import { createViteDevRouter } from '../pages/vite-dev.js';
|
|
19
19
|
import { resolvePagesOutDir } from '../pages/out-dir.js';
|
|
20
20
|
import { collectRuntimeMeta } from '../meta.js';
|
|
21
|
+
import { createPluginManager } from '../plugins/manager.js';
|
|
21
22
|
import path from 'node:path';
|
|
22
23
|
|
|
23
24
|
async function boot(options) {
|
|
@@ -27,6 +28,9 @@ async function boot(options) {
|
|
|
27
28
|
const envFiles = loadEnvFiles(rootDir, mode);
|
|
28
29
|
const config = await makeConfig(rootDir, { overrides: options.configOverrides, mode });
|
|
29
30
|
|
|
31
|
+
const plugins = await createPluginManager(config.plugins, { runLifecycle: false });
|
|
32
|
+
config.database.pluginMigrations = plugins.migrations();
|
|
33
|
+
|
|
30
34
|
const infrastructure = await createInfrastructure(config, { runMigrations: true });
|
|
31
35
|
const { db, redis, queue, cache, files, mail, events, log } = infrastructure;
|
|
32
36
|
const meta = await collectRuntimeMeta(rootDir);
|
|
@@ -42,6 +46,11 @@ async function boot(options) {
|
|
|
42
46
|
// because downstream consumers read properties off this reference at use time.
|
|
43
47
|
const appContext = { db, redis, events, queue, cache, files, mail, log, fileWatcher, meta };
|
|
44
48
|
|
|
49
|
+
plugins.log = log;
|
|
50
|
+
appContext.plugins = plugins;
|
|
51
|
+
|
|
52
|
+
await plugins.runEnabledLifecycle();
|
|
53
|
+
|
|
45
54
|
await runHook('init', appContext, rootDir);
|
|
46
55
|
|
|
47
56
|
const mcpRouter = new McpRouter(config.mcp, { log });
|
|
@@ -75,6 +84,7 @@ async function boot(options) {
|
|
|
75
84
|
log,
|
|
76
85
|
meta,
|
|
77
86
|
workerPool,
|
|
87
|
+
plugins,
|
|
78
88
|
});
|
|
79
89
|
await jobRunner.init();
|
|
80
90
|
|
package/server/build.js
CHANGED
|
@@ -12,6 +12,7 @@ import { discoverMiddleware } from './router/middleware.js';
|
|
|
12
12
|
import { EventHandler } from './events/handler.js';
|
|
13
13
|
import { discoverJobs } from './jobs/runner.js';
|
|
14
14
|
import { collectRuntimeMeta } from './meta.js';
|
|
15
|
+
import { createPluginManager } from './plugins/manager.js';
|
|
15
16
|
|
|
16
17
|
async function writeBuildMeta(rootDir, meta) {
|
|
17
18
|
const buildDir = path.join(rootDir, '.build');
|
|
@@ -23,9 +24,11 @@ async function writeBuildMeta(rootDir, meta) {
|
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
async function validateServerModules(config) {
|
|
27
|
+
const plugins = await createPluginManager(config.plugins, { runLifecycle: false });
|
|
26
28
|
if (config?.api?.enabled !== false && config?.api?.dir) {
|
|
27
29
|
await discoverRoutes(config.api.dir);
|
|
28
30
|
await discoverMiddleware(config.api.dir);
|
|
31
|
+
await plugins.discoverRoutes();
|
|
29
32
|
}
|
|
30
33
|
|
|
31
34
|
if (config?.events?.dir) {
|
|
@@ -33,7 +36,14 @@ async function validateServerModules(config) {
|
|
|
33
36
|
{ dir: config.events.dir },
|
|
34
37
|
{
|
|
35
38
|
events: { subscribe() {} },
|
|
36
|
-
log: {
|
|
39
|
+
log: {
|
|
40
|
+
info() {},
|
|
41
|
+
warn() {},
|
|
42
|
+
error() {},
|
|
43
|
+
extend() {
|
|
44
|
+
return this;
|
|
45
|
+
},
|
|
46
|
+
},
|
|
37
47
|
},
|
|
38
48
|
);
|
|
39
49
|
await handler.init();
|
|
@@ -41,6 +51,7 @@ async function validateServerModules(config) {
|
|
|
41
51
|
|
|
42
52
|
if (config?.jobs?.enabled !== false && config?.jobs?.dir) {
|
|
43
53
|
await discoverJobs(config.jobs.dir);
|
|
54
|
+
await plugins.discoverJobs();
|
|
44
55
|
}
|
|
45
56
|
}
|
|
46
57
|
|
package/server/config/loader.js
CHANGED
|
@@ -12,12 +12,14 @@ 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
16
|
import resolveSession from './modules/session.js';
|
|
16
17
|
import resolvePages from './modules/pages.js';
|
|
17
18
|
import resolveBuild from './modules/build.js';
|
|
18
19
|
import resolveMcp from './modules/mcp.js';
|
|
19
20
|
import resolveWebsocket from './modules/websocket.js';
|
|
20
21
|
import resolveSeeds from './modules/seeds.js';
|
|
22
|
+
import resolvePlugins from './modules/plugins.js';
|
|
21
23
|
|
|
22
24
|
function deepMerge(target, source) {
|
|
23
25
|
const result = { ...target };
|
|
@@ -49,12 +51,14 @@ const modules = [
|
|
|
49
51
|
resolveFiles,
|
|
50
52
|
resolveMail,
|
|
51
53
|
resolveRedis,
|
|
54
|
+
resolveSecrets,
|
|
52
55
|
resolveSession,
|
|
53
56
|
resolvePages,
|
|
54
57
|
resolveBuild,
|
|
55
58
|
resolveMcp,
|
|
56
59
|
resolveWebsocket,
|
|
57
60
|
resolveSeeds,
|
|
61
|
+
resolvePlugins,
|
|
58
62
|
];
|
|
59
63
|
|
|
60
64
|
async function makeConfig(rootDir, { overrides, mode } = {}) {
|
|
@@ -3,7 +3,11 @@ const DEFAULTS = {
|
|
|
3
3
|
};
|
|
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'];
|
|
9
|
+
}
|
|
10
|
+
return { ...config, mcp };
|
|
7
11
|
}
|
|
8
12
|
|
|
9
13
|
export default resolve;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
|
|
3
|
+
const DEFAULTS = {
|
|
4
|
+
enabled: false,
|
|
5
|
+
dirs: ['plugins'],
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
function resolve(config, { rootDir } = {}) {
|
|
9
|
+
const plugins = { ...DEFAULTS, ...config.plugins };
|
|
10
|
+
const dirs = Array.isArray(plugins.dirs) ? plugins.dirs : [plugins.dirs];
|
|
11
|
+
plugins.dirs = dirs
|
|
12
|
+
.filter((dir) => typeof dir === 'string' && dir.length > 0)
|
|
13
|
+
.map((dir) => (rootDir && !path.isAbsolute(dir) ? path.resolve(rootDir, dir) : dir));
|
|
14
|
+
plugins.enabled = plugins.enabled === true;
|
|
15
|
+
return { ...config, plugins };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export default resolve;
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import {
|
|
4
|
+
deriveInfraSecrets,
|
|
5
|
+
encodeMasterSecret,
|
|
6
|
+
normalizeMasterSecret,
|
|
7
|
+
validateVaultKey,
|
|
8
|
+
} from '../../lib/vault/secrets.js';
|
|
9
|
+
|
|
10
|
+
const DEFAULT_KEY_FILE = '.build/arcway-master-secret.key';
|
|
11
|
+
|
|
12
|
+
const EXPLICIT_ENV = Object.freeze({
|
|
13
|
+
session: 'SESSION_SECRET',
|
|
14
|
+
'plugin-vault': 'PLUGIN_VAULT_KEY',
|
|
15
|
+
'mcp-api': 'MCP_API_KEY',
|
|
16
|
+
'pty-api': 'PTY_API_KEY',
|
|
17
|
+
'pty-server-api': 'PTY_SERVER_API_KEY',
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
function isProduction(mode) {
|
|
21
|
+
return mode === 'production';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function readKeyFile(filePath) {
|
|
25
|
+
try {
|
|
26
|
+
const stat = fs.statSync(filePath);
|
|
27
|
+
if ((stat.mode & 0o077) !== 0) {
|
|
28
|
+
throw new Error('Arcway master secret keyfile permissions must be 0600');
|
|
29
|
+
}
|
|
30
|
+
return fs.readFileSync(filePath, 'utf8').trim();
|
|
31
|
+
} catch (error) {
|
|
32
|
+
if (error.code === 'ENOENT') return null;
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function createDevKeyFile(filePath) {
|
|
38
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
39
|
+
const encoded = encodeMasterSecret();
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
const fd = fs.openSync(
|
|
43
|
+
filePath,
|
|
44
|
+
fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY,
|
|
45
|
+
0o600,
|
|
46
|
+
);
|
|
47
|
+
try {
|
|
48
|
+
fs.writeFileSync(fd, `${encoded}\n`, 'utf8');
|
|
49
|
+
} finally {
|
|
50
|
+
fs.closeSync(fd);
|
|
51
|
+
}
|
|
52
|
+
return encoded;
|
|
53
|
+
} catch (error) {
|
|
54
|
+
if (error.code === 'EEXIST') return readKeyFile(filePath);
|
|
55
|
+
throw error;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function loadMasterSecret(rawSecrets, { rootDir, mode, env }) {
|
|
60
|
+
const configured = rawSecrets.master ?? env.ARCWAY_MASTER_SECRET;
|
|
61
|
+
if (configured) return { value: configured, source: 'config' };
|
|
62
|
+
|
|
63
|
+
if (isProduction(mode)) {
|
|
64
|
+
throw new Error(
|
|
65
|
+
'ARCWAY_MASTER_SECRET is required when arcway.config secrets are enabled in production',
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const keyFile = path.resolve(rootDir, rawSecrets.keyFile ?? DEFAULT_KEY_FILE);
|
|
70
|
+
const existing = readKeyFile(keyFile);
|
|
71
|
+
if (existing) return { value: existing, source: 'keyfile', keyFile };
|
|
72
|
+
|
|
73
|
+
return { value: createDevKeyFile(keyFile), source: 'generated-keyfile', keyFile };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function explicitValue(name, rawSecrets, env) {
|
|
77
|
+
const explicit = rawSecrets.explicit?.[name];
|
|
78
|
+
if (explicit !== undefined && explicit !== null && explicit !== '') return explicit;
|
|
79
|
+
|
|
80
|
+
const envName = EXPLICIT_ENV[name];
|
|
81
|
+
const fromEnv = envName ? env[envName] : undefined;
|
|
82
|
+
return fromEnv || undefined;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function validateExplicit(name, value) {
|
|
86
|
+
if (name === 'plugin-vault') return validateVaultKey(value);
|
|
87
|
+
return String(value);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function resolve(config, { rootDir, mode } = {}) {
|
|
91
|
+
if (!config.secrets) return config;
|
|
92
|
+
|
|
93
|
+
const env = config.secrets.env ?? process.env;
|
|
94
|
+
const rawSecrets = config.secrets;
|
|
95
|
+
const master = loadMasterSecret(rawSecrets, { rootDir, mode, env });
|
|
96
|
+
normalizeMasterSecret(master.value);
|
|
97
|
+
|
|
98
|
+
const derived = deriveInfraSecrets(master.value);
|
|
99
|
+
const values = { ...derived };
|
|
100
|
+
const explicit = {};
|
|
101
|
+
const previousValues = {};
|
|
102
|
+
|
|
103
|
+
const previousMaster = rawSecrets.previous ?? env.ARCWAY_MASTER_SECRET_PREVIOUS;
|
|
104
|
+
if (previousMaster) {
|
|
105
|
+
normalizeMasterSecret(previousMaster, 'Previous Arcway master secret');
|
|
106
|
+
Object.assign(previousValues, deriveInfraSecrets(previousMaster));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
for (const name of Object.keys(EXPLICIT_ENV)) {
|
|
110
|
+
const value = explicitValue(name, rawSecrets, env);
|
|
111
|
+
if (!value) continue;
|
|
112
|
+
|
|
113
|
+
explicit[name] = validateExplicit(name, value);
|
|
114
|
+
values[name] = explicit[name];
|
|
115
|
+
if (derived[name] !== explicit[name]) {
|
|
116
|
+
previousValues[name] = explicit[name];
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const vapidPublic = rawSecrets.explicit?.vapidPublic ?? env.VAPID_PUBLIC_KEY;
|
|
121
|
+
const vapidPrivate = rawSecrets.explicit?.vapidPrivate ?? env.VAPID_PRIVATE_KEY;
|
|
122
|
+
if (vapidPublic || vapidPrivate) {
|
|
123
|
+
if (!vapidPublic || !vapidPrivate) {
|
|
124
|
+
throw new Error('VAPID_PUBLIC_KEY and VAPID_PRIVATE_KEY must be provided together');
|
|
125
|
+
}
|
|
126
|
+
explicit.vapid = { publicKey: String(vapidPublic), privateKey: String(vapidPrivate) };
|
|
127
|
+
values.vapid = explicit.vapid;
|
|
128
|
+
previousValues.vapid = explicit.vapid;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
...config,
|
|
133
|
+
secrets: {
|
|
134
|
+
enabled: true,
|
|
135
|
+
keyFile: master.keyFile,
|
|
136
|
+
source: master.source,
|
|
137
|
+
values,
|
|
138
|
+
derived,
|
|
139
|
+
explicit,
|
|
140
|
+
previousValues,
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export default resolve;
|
|
@@ -2,7 +2,11 @@ import { resolveSessionConfig } from '../../session/index.js';
|
|
|
2
2
|
|
|
3
3
|
function resolve(config, { mode } = {}) {
|
|
4
4
|
if (!config.session) return config;
|
|
5
|
-
const
|
|
5
|
+
const sessionInput = {
|
|
6
|
+
...config.session,
|
|
7
|
+
password: config.session.password || config.secrets?.values?.session,
|
|
8
|
+
};
|
|
9
|
+
const session = resolveSessionConfig(sessionInput, mode);
|
|
6
10
|
return { ...config, session };
|
|
7
11
|
}
|
|
8
12
|
|
package/server/db/index.js
CHANGED
|
@@ -6,18 +6,39 @@ import { loadModule } from '../module-loader.js';
|
|
|
6
6
|
import { introspectSchema, generateSchemaMarkdown } from './schema/index.js';
|
|
7
7
|
|
|
8
8
|
class MigrationSource {
|
|
9
|
-
constructor(directory) {
|
|
9
|
+
constructor(directory, pluginMigrations = []) {
|
|
10
10
|
this.directory = directory;
|
|
11
|
+
this.pluginMigrations = pluginMigrations;
|
|
12
|
+
this.migrationPaths = new Map();
|
|
11
13
|
}
|
|
12
14
|
async getMigrations() {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
+
this.migrationPaths.clear();
|
|
16
|
+
const migrations = [];
|
|
17
|
+
|
|
18
|
+
if (this.directory) {
|
|
19
|
+
const files = await discoverFiles(this.directory, { skipUnderscore: false });
|
|
20
|
+
for (const file of files) {
|
|
21
|
+
migrations.push(file.name);
|
|
22
|
+
this.migrationPaths.set(file.name, file.filePath);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
for (const plugin of this.pluginMigrations ?? []) {
|
|
27
|
+
const files = await discoverFiles(plugin.dir, { skipUnderscore: false });
|
|
28
|
+
for (const file of files) {
|
|
29
|
+
const name = `plugins/${plugin.pluginId}/${file.name}`;
|
|
30
|
+
migrations.push(name);
|
|
31
|
+
this.migrationPaths.set(name, file.filePath);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return migrations;
|
|
15
36
|
}
|
|
16
37
|
getMigrationName(migration) {
|
|
17
38
|
return migration;
|
|
18
39
|
}
|
|
19
40
|
async getMigration(migration) {
|
|
20
|
-
const filePath = path.resolve(this.directory, migration);
|
|
41
|
+
const filePath = this.migrationPaths.get(migration) ?? path.resolve(this.directory, migration);
|
|
21
42
|
return loadModule(filePath, import.meta.url);
|
|
22
43
|
}
|
|
23
44
|
}
|
|
@@ -77,9 +98,10 @@ async function createDB(config, { log } = {}) {
|
|
|
77
98
|
|
|
78
99
|
db.runMigrations = async () => {
|
|
79
100
|
const migrationsDir = config.dir;
|
|
80
|
-
|
|
101
|
+
const pluginMigrations = config.pluginMigrations ?? [];
|
|
102
|
+
if (!migrationsDir && pluginMigrations.length === 0) return;
|
|
81
103
|
const [batch, migrations] = await db.migrate.latest({
|
|
82
|
-
migrationSource: new MigrationSource(migrationsDir),
|
|
104
|
+
migrationSource: new MigrationSource(migrationsDir, pluginMigrations),
|
|
83
105
|
});
|
|
84
106
|
if (migrations.length > 0) {
|
|
85
107
|
log?.info(`Migrations (batch ${batch}): ${migrations.join(', ')}`);
|
|
@@ -90,9 +112,12 @@ async function createDB(config, { log } = {}) {
|
|
|
90
112
|
|
|
91
113
|
db.runRollback = async () => {
|
|
92
114
|
const migrationsDir = config.dir;
|
|
93
|
-
|
|
115
|
+
const pluginMigrations = config.pluginMigrations ?? [];
|
|
116
|
+
if (!migrationsDir && pluginMigrations.length === 0) {
|
|
117
|
+
throw new Error('No migrations dir configured');
|
|
118
|
+
}
|
|
94
119
|
const [batch, entries] = await db.migrate.rollback({
|
|
95
|
-
migrationSource: new MigrationSource(migrationsDir),
|
|
120
|
+
migrationSource: new MigrationSource(migrationsDir, pluginMigrations),
|
|
96
121
|
});
|
|
97
122
|
return { batch, log: entries };
|
|
98
123
|
};
|
package/server/discovery.js
CHANGED
|
@@ -8,7 +8,10 @@ import { loadModule } from './module-loader.js';
|
|
|
8
8
|
* Skips files starting with underscore by default.
|
|
9
9
|
* Returns sorted array of { name, filePath, relativePath }.
|
|
10
10
|
*/
|
|
11
|
-
async function discoverFiles(
|
|
11
|
+
async function discoverFiles(
|
|
12
|
+
dir,
|
|
13
|
+
{ recursive = false, extensions = ['.js'], skipUnderscore = true, filter } = {},
|
|
14
|
+
) {
|
|
12
15
|
if (!dir) return [];
|
|
13
16
|
|
|
14
17
|
let files;
|
|
@@ -49,7 +52,10 @@ async function discoverFiles(dir, { recursive = false, extensions = ['.js'], ski
|
|
|
49
52
|
* Returns sorted array of { name, filePath, relativePath, module }.
|
|
50
53
|
* Throws with a descriptive message if any file fails to import.
|
|
51
54
|
*/
|
|
52
|
-
async function discoverModules(
|
|
55
|
+
async function discoverModules(
|
|
56
|
+
dir,
|
|
57
|
+
{ recursive = false, extensions = ['.js'], skipUnderscore = true, filter, label = 'file' } = {},
|
|
58
|
+
) {
|
|
53
59
|
const files = await discoverFiles(dir, { recursive, extensions, skipUnderscore, filter });
|
|
54
60
|
const results = [];
|
|
55
61
|
for (const entry of files) {
|
|
@@ -64,4 +70,40 @@ async function discoverModules(dir, { recursive = false, extensions = ['.js'], s
|
|
|
64
70
|
return results;
|
|
65
71
|
}
|
|
66
72
|
|
|
67
|
-
|
|
73
|
+
async function discoverPackageDirectories(
|
|
74
|
+
dir,
|
|
75
|
+
{ markerFiles = ['package.json'], label = 'package' } = {},
|
|
76
|
+
) {
|
|
77
|
+
if (!dir) return [];
|
|
78
|
+
|
|
79
|
+
let entries;
|
|
80
|
+
try {
|
|
81
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
82
|
+
} catch (err) {
|
|
83
|
+
if (err.code === 'ENOENT') return [];
|
|
84
|
+
throw err;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const packages = [];
|
|
88
|
+
for (const entry of entries) {
|
|
89
|
+
if (!entry.isDirectory() || entry.name.startsWith('_')) continue;
|
|
90
|
+
const packageDir = path.join(dir, entry.name);
|
|
91
|
+
const hasMarker = await Promise.all(
|
|
92
|
+
markerFiles.map(async (marker) => {
|
|
93
|
+
try {
|
|
94
|
+
await fs.access(path.join(packageDir, marker));
|
|
95
|
+
return true;
|
|
96
|
+
} catch (err) {
|
|
97
|
+
if (err.code === 'ENOENT') return false;
|
|
98
|
+
throw err;
|
|
99
|
+
}
|
|
100
|
+
}),
|
|
101
|
+
);
|
|
102
|
+
if (!hasMarker.some(Boolean)) continue;
|
|
103
|
+
packages.push({ name: entry.name, dir: packageDir, label });
|
|
104
|
+
}
|
|
105
|
+
packages.sort((a, b) => a.name.localeCompare(b.name));
|
|
106
|
+
return packages;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export { discoverFiles, discoverModules, discoverPackageDirectories };
|
package/server/index.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export { type } from 'arktype';
|
|
2
2
|
export { buildContext } from './context.js';
|
|
3
3
|
export { default as boot } from './boot.js';
|
|
4
|
+
export { CapabilityNotAvailable, CapabilityRegistry } from './plugins/capabilities.js';
|
|
5
|
+
export { PluginManager, createPluginManager } from './plugins/manager.js';
|
|
4
6
|
export {
|
|
5
7
|
Arcway,
|
|
6
8
|
Solo,
|
package/server/internals.js
CHANGED
|
@@ -42,6 +42,9 @@ import { createRateLimitMiddleware } from './rate-limit/index.js';
|
|
|
42
42
|
import { createHttpServer, listen, closeServer } from './server.js';
|
|
43
43
|
import { startWatcher } from './watcher.js';
|
|
44
44
|
import { build } from './build.js';
|
|
45
|
+
import { CapabilityNotAvailable, CapabilityRegistry } from './plugins/capabilities.js';
|
|
46
|
+
import { discoverPlugins } from './plugins/discovery.js';
|
|
47
|
+
import { PluginManager, createPluginManager } from './plugins/manager.js';
|
|
45
48
|
import {
|
|
46
49
|
discoverPages,
|
|
47
50
|
discoverLayouts,
|
|
@@ -68,6 +71,8 @@ import { attachGraphQLSubscriptions } from './graphql/subscriptions.js';
|
|
|
68
71
|
export {
|
|
69
72
|
ApiRouter,
|
|
70
73
|
Cache,
|
|
74
|
+
CapabilityNotAvailable,
|
|
75
|
+
CapabilityRegistry,
|
|
71
76
|
makeConfig,
|
|
72
77
|
Files,
|
|
73
78
|
KnexQueueDriver,
|
|
@@ -101,9 +106,11 @@ export {
|
|
|
101
106
|
createLoaderFactory,
|
|
102
107
|
createPagesHandler,
|
|
103
108
|
createPagesWatcher,
|
|
109
|
+
createPluginManager,
|
|
104
110
|
createRateLimitMiddleware,
|
|
105
111
|
createWebSocketServer,
|
|
106
112
|
discoverJobs,
|
|
113
|
+
discoverPlugins,
|
|
107
114
|
EventHandler,
|
|
108
115
|
discoverErrorPages,
|
|
109
116
|
discoverGraphQL,
|
|
@@ -127,6 +134,7 @@ export {
|
|
|
127
134
|
mergeWebSocketHandlers,
|
|
128
135
|
parseCron,
|
|
129
136
|
patternToFileName,
|
|
137
|
+
PluginManager,
|
|
130
138
|
resolveLayoutChain,
|
|
131
139
|
resolveSessionConfig,
|
|
132
140
|
runSeeds,
|
|
@@ -16,6 +16,9 @@ async function runHandler(reg, payload, workerPool) {
|
|
|
16
16
|
return;
|
|
17
17
|
}
|
|
18
18
|
const ctx = buildContext(reg.store, { payload });
|
|
19
|
+
if (reg.definition.plugin) {
|
|
20
|
+
reg.store.plugins?.injectCapabilities(ctx, reg.definition.plugin.requires);
|
|
21
|
+
}
|
|
19
22
|
await reg.definition.handler(ctx);
|
|
20
23
|
}
|
|
21
24
|
|
package/server/jobs/runner.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import path from 'node:path';
|
|
2
1
|
import { discoverModules } from '../discovery.js';
|
|
3
2
|
import { buildContext } from '../context.js';
|
|
4
3
|
import { parseCron, matchesCron } from './cron.js';
|
|
@@ -45,7 +44,10 @@ class JobRunner {
|
|
|
45
44
|
_stopped = false;
|
|
46
45
|
_lastEnqueuedMinute = new Map();
|
|
47
46
|
|
|
48
|
-
constructor(
|
|
47
|
+
constructor(
|
|
48
|
+
config,
|
|
49
|
+
{ db, queue, cache, files, mail, events, log, meta, workerPool, plugins } = {},
|
|
50
|
+
) {
|
|
49
51
|
this._config = config;
|
|
50
52
|
this._log = log;
|
|
51
53
|
this._dispatcher = new JobDispatcher({
|
|
@@ -53,7 +55,7 @@ class JobRunner {
|
|
|
53
55
|
staleTimeoutMs: config?.staleTimeoutMs,
|
|
54
56
|
workerPool,
|
|
55
57
|
});
|
|
56
|
-
this._appContext = { db, queue, cache, files, mail, events, log, meta };
|
|
58
|
+
this._appContext = { db, queue, cache, files, mail, events, log, meta, plugins };
|
|
57
59
|
}
|
|
58
60
|
|
|
59
61
|
get dispatcher() {
|
|
@@ -75,13 +77,15 @@ class JobRunner {
|
|
|
75
77
|
// Discover and register user jobs. `filePath` is captured so the
|
|
76
78
|
// dispatcher can dynamic-import the handler inside a worker thread.
|
|
77
79
|
const discovered = await discoverJobs(jobsDir);
|
|
78
|
-
|
|
80
|
+
const pluginJobs = (await this._appContext.plugins?.discoverJobs?.()) ?? [];
|
|
81
|
+
for (const { definition, fileName, filePath } of [...discovered, ...pluginJobs]) {
|
|
79
82
|
this._dispatcher.register('app', definition, this._appContext, filePath);
|
|
80
83
|
this._jobs.push({
|
|
81
84
|
jobName: definition.name,
|
|
82
85
|
fileName,
|
|
83
86
|
schedule: definition.schedule,
|
|
84
87
|
handler: definition.handler,
|
|
88
|
+
plugin: definition.plugin,
|
|
85
89
|
cooldownMs: definition.cooldownMs,
|
|
86
90
|
staleTimeout: definition.staleTimeout,
|
|
87
91
|
});
|
|
@@ -110,6 +114,7 @@ class JobRunner {
|
|
|
110
114
|
this._continuousJobs.push({
|
|
111
115
|
qualifiedName: `app/${job.jobName}`,
|
|
112
116
|
handler: job.handler,
|
|
117
|
+
plugin: job.plugin,
|
|
113
118
|
cooldownMs: job.cooldownMs,
|
|
114
119
|
appContext: this._appContext,
|
|
115
120
|
});
|
|
@@ -192,6 +197,9 @@ class JobRunner {
|
|
|
192
197
|
try {
|
|
193
198
|
const start = Date.now();
|
|
194
199
|
const ctx = buildContext(entry.appContext, { payload: void 0 });
|
|
200
|
+
if (entry.plugin) {
|
|
201
|
+
entry.appContext.plugins?.injectCapabilities(ctx, entry.plugin.requires);
|
|
202
|
+
}
|
|
195
203
|
await entry.handler(ctx);
|
|
196
204
|
consecutiveErrors = 0;
|
|
197
205
|
const elapsed = Date.now() - start;
|
|
@@ -2,13 +2,33 @@ import { hashPassword, verifyPassword } from './password.js';
|
|
|
2
2
|
import { generateUUID, generateNanoId } from './ids.js';
|
|
3
3
|
import { encrypt, decrypt } from './encrypt.js';
|
|
4
4
|
import { jwtEncode, jwtDecode } from './jwt.js';
|
|
5
|
+
import {
|
|
6
|
+
decryptWithRotation,
|
|
7
|
+
deriveBytes,
|
|
8
|
+
deriveInfraSecret,
|
|
9
|
+
deriveInfraSecrets,
|
|
10
|
+
deriveVapidKeyPair,
|
|
11
|
+
encodeMasterSecret,
|
|
12
|
+
formatVaultKey,
|
|
13
|
+
normalizeMasterSecret,
|
|
14
|
+
validateVaultKey,
|
|
15
|
+
} from './secrets.js';
|
|
5
16
|
export {
|
|
17
|
+
decryptWithRotation,
|
|
6
18
|
decrypt,
|
|
19
|
+
deriveBytes,
|
|
20
|
+
deriveInfraSecret,
|
|
21
|
+
deriveInfraSecrets,
|
|
22
|
+
deriveVapidKeyPair,
|
|
23
|
+
encodeMasterSecret,
|
|
7
24
|
encrypt,
|
|
25
|
+
formatVaultKey,
|
|
8
26
|
generateNanoId,
|
|
9
27
|
generateUUID,
|
|
10
28
|
hashPassword,
|
|
11
29
|
jwtDecode,
|
|
12
30
|
jwtEncode,
|
|
31
|
+
normalizeMasterSecret,
|
|
32
|
+
validateVaultKey,
|
|
13
33
|
verifyPassword,
|
|
14
34
|
};
|