@axium/server 0.32.1 → 0.33.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/api/admin.js CHANGED
@@ -1,8 +1,11 @@
1
1
  import { AuditFilter, Severity } from '@axium/core';
2
+ import { errorText, writeJSON } from '@axium/core/node/io';
2
3
  import { getVersionInfo } from '@axium/core/node/packages';
3
- import { plugins } from '@axium/core/plugins';
4
+ import { _findPlugin, plugins, PluginUpdate, serverConfigs } from '@axium/core/plugins';
4
5
  import { jsonObjectFrom } from 'kysely/helpers/postgres';
5
- import { omit } from 'utilium';
6
+ import { mkdirSync } from 'node:fs';
7
+ import { dirname } from 'node:path/posix';
8
+ import { deepAssign, omit } from 'utilium';
6
9
  import * as z from 'zod';
7
10
  import { audit, events, getEvents } from '../audit.js';
8
11
  import { createVerification, requireSession } from '../auth.js';
@@ -52,6 +55,30 @@ addRoute({
52
55
  .values()
53
56
  .map(async (p) => Object.assign(omit(p, '_hooks', '_client'), p.update_checks ? await getVersionInfo(p.specifier, p.loadedBy) : { latest: null })));
54
57
  },
58
+ async POST(req) {
59
+ await assertAdmin(this, req);
60
+ const { plugin: name, config } = await parseBody(req, PluginUpdate);
61
+ let plugin;
62
+ try {
63
+ plugin = _findPlugin(name);
64
+ }
65
+ catch {
66
+ error(404, 'Plugin not found');
67
+ }
68
+ if (config) {
69
+ const { schema } = serverConfigs.get(name) || {};
70
+ if (!schema)
71
+ error(400, 'Plugin does not have a configuration schema');
72
+ if (!plugin._configPath)
73
+ error(503, 'Plugin configuration path is not set');
74
+ plugin.config ||= {};
75
+ const parsed = await schema.parseAsync(config).catch(e => error(400, errorText(e)));
76
+ deepAssign(plugin.config, parsed);
77
+ mkdirSync(dirname(plugin._configPath), { recursive: true });
78
+ writeJSON(plugin._configPath, plugin.config);
79
+ }
80
+ return {};
81
+ },
55
82
  });
56
83
  addRoute({
57
84
  path: '/api/admin/users',
package/dist/config.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { type DeepRequired } from 'utilium';
2
2
  import * as z from 'zod';
3
- export declare let ConfigSchema: z.ZodObject<{
3
+ export declare const Config: z.ZodObject<{
4
4
  admin_api: z.ZodOptional<z.ZodBoolean>;
5
5
  allow_new_users: z.ZodOptional<z.ZodBoolean>;
6
6
  apps: z.ZodOptional<z.ZodObject<{
@@ -21,7 +21,7 @@ export declare let ConfigSchema: z.ZodObject<{
21
21
  }, z.core.$loose>>;
22
22
  db: z.ZodOptional<z.ZodObject<{
23
23
  host: z.ZodOptional<z.ZodString>;
24
- port: z.ZodOptional<z.ZodNumber>;
24
+ port: z.ZodOptional<z.ZodInt>;
25
25
  password: z.ZodOptional<z.ZodString>;
26
26
  user: z.ZodOptional<z.ZodString>;
27
27
  database: z.ZodOptional<z.ZodString>;
@@ -56,10 +56,9 @@ export declare let ConfigSchema: z.ZodObject<{
56
56
  build: z.ZodOptional<z.ZodString>;
57
57
  }, z.core.$loose>>;
58
58
  }, z.core.$loose>;
59
- export declare function addConfig<T extends z.core.$ZodLooseShape>(shape: T): void;
60
- export interface Config extends z.infer<typeof ConfigSchema> {
59
+ export interface Config extends z.infer<typeof Config> {
61
60
  }
62
- export declare const configFiles: Map<string, File>;
61
+ export declare const configFiles: Map<string, ConfigFile>;
63
62
  export declare function plainConfig(): Omit<DeepRequired<Config>, keyof typeof configShortcuts>;
64
63
  export declare const defaultConfig: DeepRequired<Config>;
65
64
  declare const configShortcuts: {
@@ -70,12 +69,12 @@ declare const configShortcuts: {
70
69
  save: typeof saveConfig;
71
70
  saveTo: typeof saveConfigTo;
72
71
  set: typeof setConfig;
73
- files: Map<string, File>;
72
+ files: Map<string, ConfigFile>;
74
73
  defaults: DeepRequired<Config>;
75
74
  };
76
75
  export declare const config: DeepRequired<Config> & typeof configShortcuts;
77
76
  export default config;
78
- export declare const FileSchema: z.ZodObject<{
77
+ export declare const ConfigFile: z.ZodObject<{
79
78
  include: z.ZodOptional<z.ZodArray<z.ZodString>>;
80
79
  plugins: z.ZodOptional<z.ZodArray<z.ZodString>>;
81
80
  admin_api: z.ZodOptional<z.ZodOptional<z.ZodBoolean>>;
@@ -98,7 +97,7 @@ export declare const FileSchema: z.ZodObject<{
98
97
  }, z.core.$loose>>>;
99
98
  db: z.ZodOptional<z.ZodOptional<z.ZodObject<{
100
99
  host: z.ZodOptional<z.ZodString>;
101
- port: z.ZodOptional<z.ZodNumber>;
100
+ port: z.ZodOptional<z.ZodInt>;
102
101
  password: z.ZodOptional<z.ZodString>;
103
102
  user: z.ZodOptional<z.ZodString>;
104
103
  database: z.ZodOptional<z.ZodString>;
@@ -133,9 +132,8 @@ export declare const FileSchema: z.ZodObject<{
133
132
  build: z.ZodOptional<z.ZodString>;
134
133
  }, z.core.$loose>>>;
135
134
  }, z.core.$loose>;
136
- export interface File extends z.infer<typeof FileSchema> {
135
+ export interface ConfigFile extends z.infer<typeof ConfigFile> {
137
136
  }
138
- export declare function addConfigDefaults(other: Config, _target?: Record<string, any>, _noDefault?: boolean): void;
139
137
  /**
140
138
  * Update the current config
141
139
  */
package/dist/config.js CHANGED
@@ -7,9 +7,10 @@ import { capitalize, deepAssign, omit } from 'utilium';
7
7
  import * as z from 'zod';
8
8
  import { dirs, logger, systemDir } from './io.js';
9
9
  import { _duplicateStateWarnings, _unique } from './state.js';
10
+ import { serverConfigs, toBaseName } from '@axium/core';
10
11
  const audit_severity_levels = ['emergency', 'alert', 'critical', 'error', 'warning', 'notice', 'info', 'debug'];
11
12
  const z_audit_severity = z.literal([...audit_severity_levels, ...audit_severity_levels.map(capitalize)]);
12
- export let ConfigSchema = z
13
+ export const Config = z
13
14
  .looseObject({
14
15
  /** Whether /api/admin is enabled */
15
16
  admin_api: z.boolean(),
@@ -42,7 +43,7 @@ export let ConfigSchema = z
42
43
  db: z
43
44
  .looseObject({
44
45
  host: z.string(),
45
- port: z.number(),
46
+ port: z.int().min(1).max(65535),
46
47
  password: z.string(),
47
48
  user: z.string(),
48
49
  database: z.string(),
@@ -82,9 +83,6 @@ export let ConfigSchema = z
82
83
  .partial(),
83
84
  })
84
85
  .partial();
85
- export function addConfig(shape) {
86
- ConfigSchema = z.looseObject({ ...ConfigSchema.shape, ...shape });
87
- }
88
86
  export const configFiles = _unique('configFiles', new Map());
89
87
  export function plainConfig() {
90
88
  return omit(config, Object.keys(configShortcuts));
@@ -161,26 +159,13 @@ export const config = _unique('config', {
161
159
  });
162
160
  export default config;
163
161
  // config from file
164
- export const FileSchema = z
162
+ export const ConfigFile = z
165
163
  .looseObject({
166
- ...ConfigSchema.shape,
164
+ ...Config.shape,
167
165
  include: z.string().array(),
168
166
  plugins: z.string().array(),
169
167
  })
170
168
  .partial();
171
- export function addConfigDefaults(other, _target = config, _noDefault = false) {
172
- if (!_noDefault)
173
- deepAssign(defaultConfig, other);
174
- for (const [key, value] of Object.entries(other)) {
175
- if (!(key in _target) || _target[key] === null || _target[key] === undefined || Number.isNaN(_target[key])) {
176
- _target[key] = value;
177
- continue;
178
- }
179
- if (typeof value == 'object' && value != null && typeof _target[key] == 'object') {
180
- addConfigDefaults(value, _target[key], true);
181
- }
182
- }
183
- }
184
169
  /**
185
170
  * Update the current config
186
171
  */
@@ -222,7 +207,7 @@ export async function loadConfig(path, options = {}) {
222
207
  }
223
208
  let file;
224
209
  try {
225
- file = FileSchema.parse(json);
210
+ file = ConfigFile.parse(json);
226
211
  if (file.web?.build)
227
212
  file.web.build = resolve(dirname(path), file.web.build);
228
213
  }
@@ -241,6 +226,25 @@ export async function loadConfig(path, options = {}) {
241
226
  const plugin = await loadPlugin('server', pluginPath, path, options.safe);
242
227
  if (!plugin)
243
228
  continue;
229
+ const serverConfig = serverConfigs.get(plugin.name);
230
+ if (serverConfig) {
231
+ plugin.config ||= {};
232
+ let configPath;
233
+ for (const dir of dirs) {
234
+ configPath = join(dir, 'plugins', toBaseName(plugin.name) + '.json');
235
+ if (!existsSync(configPath))
236
+ continue;
237
+ try {
238
+ const data = io.readJSON(configPath, serverConfig.schema.partial());
239
+ deepAssign(plugin.config, data);
240
+ io.debug(`Loaded config for plugin ${plugin.name} from ${configPath}`);
241
+ }
242
+ catch (e) {
243
+ io.warn(`Failed to load config for plugin ${plugin.name} at ${configPath}: ${e}`);
244
+ }
245
+ }
246
+ plugin._configPath = configPath;
247
+ }
244
248
  }
245
249
  }
246
250
  export async function loadDefaultConfigs(safe = false) {
@@ -439,7 +439,7 @@ export declare const SchemaFile: z.ZodObject<{
439
439
  drop_indexes: z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodTemplateLiteral<`${string}:${string}`>>>>;
440
440
  }, z.core.$strict>], "delta">>;
441
441
  wipe: z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodString>>>;
442
- latest: z.ZodOptional<z.ZodNumber>;
442
+ latest: z.ZodOptional<z.ZodInt32>;
443
443
  acl_tables: z.ZodDefault<z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>>;
444
444
  }, z.core.$strip>;
445
445
  export interface SchemaFile extends z.infer<typeof SchemaFile> {
@@ -555,11 +555,11 @@ export declare function getFullSchema(opt?: {
555
555
  versions: Record<string, number>;
556
556
  };
557
557
  export declare const UpgradesInfo: z.ZodObject<{
558
- current: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodInt>>;
558
+ current: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodInt32>>;
559
559
  upgrades: z.ZodDefault<z.ZodArray<z.ZodObject<{
560
560
  timestamp: z.ZodCoercedDate<unknown>;
561
- from: z.ZodRecord<z.ZodString, z.ZodInt>;
562
- to: z.ZodRecord<z.ZodString, z.ZodInt>;
561
+ from: z.ZodRecord<z.ZodString, z.ZodInt32>;
562
+ to: z.ZodRecord<z.ZodString, z.ZodInt32>;
563
563
  }, z.core.$strip>>>;
564
564
  }, z.core.$strip>;
565
565
  export interface UpgradesInfo extends z.infer<typeof UpgradesInfo> {
package/dist/database.js CHANGED
@@ -352,7 +352,7 @@ export const SchemaFile = z.object({
352
352
  /** List of tables to wipe */
353
353
  wipe: z.string().array().optional().default([]),
354
354
  /** Set the latest version, defaults to the last one */
355
- latest: z.number().nonnegative().optional(),
355
+ latest: z.int32().nonnegative().optional(),
356
356
  /** Maps tables to their ACL tables, e.g. `"storage": "acl.storage"` */
357
357
  acl_tables: z.record(z.string(), z.string()).optional().default({}),
358
358
  });
@@ -416,7 +416,7 @@ const schemaToIntrospected = {
416
416
  integer: 'int4',
417
417
  'text[]': '_text',
418
418
  };
419
- const VersionMap = z.record(z.string(), z.int().nonnegative());
419
+ const VersionMap = z.record(z.string(), z.int32().nonnegative());
420
420
  export const UpgradesInfo = z.object({
421
421
  current: VersionMap.default({}),
422
422
  upgrades: z.object({ timestamp: z.coerce.date(), from: VersionMap, to: VersionMap }).array().default([]),
package/dist/linking.d.ts CHANGED
@@ -12,5 +12,6 @@ export declare function listRouteLinks(options?: LinkOptions): Generator<LinkInf
12
12
  * Symlinks .svelte page routes for plugins and the server into a project's routes directory.
13
13
  */
14
14
  export declare function linkRoutes(options?: LinkOptions): void;
15
+ export declare function writePluginHooks(): void;
15
16
  export declare function unlinkRoutes(options?: LinkOptions): void;
16
17
  export {};
package/dist/linking.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import * as io from '@axium/core/node/io';
2
2
  import { plugins } from '@axium/core/plugins';
3
- import { existsSync, symlinkSync, unlinkSync } from 'node:fs';
4
- import { join, resolve } from 'node:path/posix';
3
+ import { existsSync, symlinkSync, unlinkSync, writeFileSync } from 'node:fs';
4
+ import { join, relative, resolve } from 'node:path/posix';
5
5
  import config from './config.js';
6
6
  const textFor = {
7
7
  builtin: 'built-in routes',
@@ -54,6 +54,20 @@ export function linkRoutes(options = {}) {
54
54
  }
55
55
  }
56
56
  }
57
+ export function writePluginHooks() {
58
+ const hooksPath = join(import.meta.dirname, '../.hooks.js');
59
+ io.start('Writing web client hooks for plugins');
60
+ let hooks = `// auto-generated plugin hooks //\n`;
61
+ for (const plugin of plugins.values()) {
62
+ if (!plugin.server?.web_client_hooks)
63
+ continue;
64
+ const specifier = relative(resolve(import.meta.dirname, '..'), resolve(plugin.dirname, plugin.server.web_client_hooks));
65
+ hooks += `import '${specifier}';\n`;
66
+ }
67
+ writeFileSync(hooksPath, hooks, 'utf8');
68
+ io.done();
69
+ io.debug('Wrote', hooksPath);
70
+ }
57
71
  export function unlinkRoutes(options = {}) {
58
72
  for (const info of listRouteLinks(options)) {
59
73
  const { text, from } = info;
package/dist/main.js CHANGED
@@ -66,10 +66,10 @@ import * as z from 'zod';
66
66
  import $pkg from '../package.json' with { type: 'json' };
67
67
  import { audit, getEvents, styleSeverity } from './audit.js';
68
68
  import { diffUpdate, lookupUser, userText } from './cli.js';
69
- import config, { configFiles, FileSchema, saveConfigTo } from './config.js';
69
+ import config, { ConfigFile, configFiles, saveConfigTo } from './config.js';
70
70
  import * as db from './database.js';
71
71
  import { _portActions, _portMethods, restrictedPorts } from './io.js';
72
- import { linkRoutes, listRouteLinks, unlinkRoutes } from './linking.js';
72
+ import { linkRoutes, listRouteLinks, unlinkRoutes, writePluginHooks } from './linking.js';
73
73
  import { serve } from './serve.js';
74
74
  async function rlConfirm(question = 'Is this ok') {
75
75
  const { data, error } = z
@@ -97,7 +97,7 @@ function configReplacer(opt) {
97
97
  return opt.redact && ['password', 'secret'].includes(key) ? '[redacted]' : value;
98
98
  };
99
99
  }
100
- var rl, safe, configFromCLI, noAutoDB, opts, axiumDB, axiumConfig, axiumPlugin, axiumApps, argUserLookup;
100
+ var rl, safe, debug, configFromCLI, noAutoDB, opts, axiumDB, axiumConfig, axiumPlugin, axiumApps, argUserLookup;
101
101
  const env_1 = { stack: [], error: void 0, hasError: false };
102
102
  try {
103
103
  rl = __addDisposableResource(env_1, createInterface({
@@ -105,14 +105,23 @@ try {
105
105
  output: process.stdout,
106
106
  }), false);
107
107
  // Need these before Command is set up (e.g. for CLI integrations)
108
- ({ safe, config: configFromCLI } = parseArgs({
108
+ ({
109
+ safe,
110
+ debug,
111
+ config: configFromCLI
112
+ } = parseArgs({
109
113
  options: {
110
114
  safe: { type: 'boolean', default: z.stringbool().default(false).parse(process.env.SAFE?.toLowerCase()) },
115
+ debug: { type: 'boolean', default: z.stringbool().default(false).parse(process.env.DEBUG?.toLowerCase()) },
111
116
  config: { type: 'string', short: 'c' },
112
117
  },
113
118
  allowPositionals: true,
114
119
  strict: false,
115
120
  }).values);
121
+ if (debug) {
122
+ io._setDebugOutput(true);
123
+ config.set({ debug: true });
124
+ }
116
125
  await config.loadDefaults(safe);
117
126
  if (configFromCLI)
118
127
  await config.load(configFromCLI, { safe });
@@ -495,7 +504,7 @@ try {
495
504
  .action(() => {
496
505
  const opt = axiumConfig.optsWithGlobals();
497
506
  try {
498
- const schema = z.toJSONSchema(FileSchema, { io: 'input' });
507
+ const schema = z.toJSONSchema(ConfigFile, { io: 'input' });
499
508
  console.log(opt.json ? JSON.stringify(schema, configReplacer(opt), 4) : schema);
500
509
  }
501
510
  catch (e) {
@@ -777,6 +786,7 @@ try {
777
786
  return;
778
787
  }
779
788
  linkRoutes(linkOpts);
789
+ writePluginHooks();
780
790
  });
781
791
  program
782
792
  .command('audit')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@axium/server",
3
- "version": "0.32.1",
3
+ "version": "0.33.1",
4
4
  "author": "James Prevett <axium@jamespre.dev>",
5
5
  "funding": {
6
6
  "type": "individual",
@@ -47,8 +47,8 @@
47
47
  "clean": "rm -rf build .svelte-kit node_modules/{.vite,.vite-temp}"
48
48
  },
49
49
  "peerDependencies": {
50
- "@axium/client": ">=0.11.0",
51
- "@axium/core": ">=0.17.0",
50
+ "@axium/client": ">=0.12.0",
51
+ "@axium/core": ">=0.18.0",
52
52
  "kysely": "^0.28.0",
53
53
  "utilium": "^2.6.0",
54
54
  "zod": "^4.0.5"
@@ -1,7 +1,9 @@
1
1
  <script lang="ts">
2
- import { ClipboardCopy, FormDialog, Icon, Logout, Preferences, SessionList } from '@axium/client/components';
2
+ import { ClipboardCopy, FormDialog, Icon, Logout, SessionList, ZodForm } from '@axium/client/components';
3
+ import { fetchAPI } from '@axium/client/requests';
3
4
  import '@axium/client/styles/account';
4
5
  import { createPasskey, deletePasskey, deleteUser, sendVerificationEmail, updatePasskey, updateUser } from '@axium/client/user';
6
+ import { preferenceLabels, Preferences } from '@axium/core/preferences';
5
7
  import { getUserImage } from '@axium/core/user';
6
8
  import type { PageProps } from './$types';
7
9
 
@@ -161,7 +163,13 @@
161
163
 
162
164
  <div id="preferences" class="section main">
163
165
  <h3>Preferences</h3>
164
- <Preferences userId={user.id} bind:preferences={user.preferences} />
166
+ <ZodForm
167
+ bind:rootValue={user.preferences}
168
+ idPrefix="preferences"
169
+ schema={Preferences}
170
+ labels={preferenceLabels}
171
+ updateValue={(preferences: Preferences) => fetchAPI('PATCH', 'users/:id', { preferences }, user.id)}
172
+ />
165
173
  </div>
166
174
  </div>
167
175
 
@@ -1,5 +1,7 @@
1
1
  <script lang="ts">
2
- import { Version } from '@axium/client/components';
2
+ import { Version, ZodForm } from '@axium/client/components';
3
+ import { fetchAPI } from '@axium/client/requests';
4
+ import { serverConfigs } from '@axium/core';
3
5
 
4
6
  const { data } = $props();
5
7
  </script>
@@ -11,6 +13,7 @@
11
13
  <h2>Plugins</h2>
12
14
 
13
15
  {#each data.plugins as plugin}
16
+ {@const cfg = serverConfigs.get(plugin.name)}
14
17
  <div class="plugin">
15
18
  <h3>{plugin.name}<Version v={plugin.version} latest={plugin.latest} /></h3>
16
19
  <p>
@@ -35,6 +38,17 @@
35
38
  {:else}<i>None</i>{/if}
36
39
  </p>
37
40
  <p>{plugin.description}</p>
41
+ {#if cfg && plugin.config}
42
+ <h4>Configuration</h4>
43
+ {@const { schema, labels } = cfg}
44
+ <ZodForm
45
+ rootValue={plugin.config}
46
+ idPrefix={plugin.name}
47
+ {schema}
48
+ {labels}
49
+ updateValue={config => fetchAPI('POST', 'admin/plugins', { plugin: plugin.name, config })}
50
+ />
51
+ {/if}
38
52
  </div>
39
53
  {:else}
40
54
  <i>No plugins loaded.</i>
@@ -60,4 +74,8 @@
60
74
  .apps a {
61
75
  text-decoration: underline;
62
76
  }
77
+
78
+ .plugin :global(label:not(.checkbox)) {
79
+ font-family: monospace;
80
+ }
63
81
  </style>
@@ -1,7 +1,9 @@
1
1
  <script lang="ts">
2
- import { ClipboardCopy, Preferences, SessionList, Icon, FormDialog } from '@axium/client/components';
2
+ import { ClipboardCopy, FormDialog, Icon, SessionList, ZodForm } from '@axium/client/components';
3
+ import { fetchAPI } from '@axium/client/requests';
3
4
  import '@axium/client/styles/account';
4
5
  import { deleteUser } from '@axium/client/user';
6
+ import { preferenceLabels, Preferences } from '@axium/core';
5
7
  import { formatDateRange } from '@axium/core/format';
6
8
 
7
9
  const { data } = $props();
@@ -108,7 +110,12 @@
108
110
 
109
111
  <div id="preferences" class="section main">
110
112
  <h3>Preferences</h3>
111
- <Preferences userId={user.id} bind:preferences={user.preferences} />
113
+ <ZodForm
114
+ bind:rootValue={user.preferences}
115
+ schema={Preferences}
116
+ labels={preferenceLabels}
117
+ updateValue={(preferences: Preferences) => fetchAPI('PATCH', 'users/:id', { preferences }, user.id)}
118
+ />
112
119
  </div>
113
120
 
114
121
  <style>
@@ -103,7 +103,9 @@
103
103
  "type": "string"
104
104
  },
105
105
  "port": {
106
- "type": "number"
106
+ "type": "integer",
107
+ "minimum": 1,
108
+ "maximum": 65535
107
109
  },
108
110
  "password": {
109
111
  "type": "string"
package/schemas/db.json CHANGED
@@ -875,8 +875,9 @@
875
875
  }
876
876
  },
877
877
  "latest": {
878
- "type": "number",
879
- "minimum": 0
878
+ "type": "integer",
879
+ "minimum": 0,
880
+ "maximum": 2147483647
880
881
  },
881
882
  "acl_tables": {
882
883
  "default": {},
package/svelte.config.js CHANGED
@@ -22,6 +22,7 @@ export default {
22
22
  routes: config.web.routes,
23
23
  hooks: {
24
24
  universal: '/dev/null',
25
+ client: join(import.meta.dirname, '.hooks.js'),
25
26
  },
26
27
  },
27
28
  },