@axium/core 0.7.0 → 0.9.0

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.d.ts CHANGED
@@ -2,10 +2,21 @@ import type { PublicKeyCredentialCreationOptionsJSON, PublicKeyCredentialRequest
2
2
  import type z from 'zod';
3
3
  import type { AccessControl } from './access.js';
4
4
  import type { App } from './apps.js';
5
+ import type { AuditEvent, AuditFilter, Severity } from './audit.js';
5
6
  import type { NewSessionResponse, Session, Verification } from './auth.js';
6
7
  import type { Passkey, PasskeyAuthenticationResponse, PasskeyChangeable, PasskeyRegistration } from './passkeys.js';
7
8
  import type { RequestMethod } from './requests.js';
8
- import type { LogoutSessions, User, UserAuthOptions, UserChangeable, UserPublic, UserRegistration } from './user.js';
9
+ import type { LogoutSessions, User, UserAuthOptions, UserChangeable, UserInternal, UserPublic, UserRegistration } from './user.js';
10
+ import type { PluginInternal } from './plugins.js';
11
+ export interface AdminSummary {
12
+ users: number;
13
+ passkeys: number;
14
+ sessions: number;
15
+ auditEvents: Record<keyof typeof Severity, number>;
16
+ configFiles: number;
17
+ plugins: number;
18
+ version: string;
19
+ }
9
20
  /**
10
21
  * Types for all API endpoints
11
22
  * @internal
@@ -88,9 +99,41 @@ export interface $API {
88
99
  permission: number;
89
100
  }, AccessControl];
90
101
  };
102
+ 'admin/summary': {
103
+ GET: AdminSummary;
104
+ };
105
+ 'admin/users/all': {
106
+ GET: UserInternal[];
107
+ };
108
+ 'admin/users/:userId': {
109
+ GET: UserInternal & {
110
+ sessions: Session[];
111
+ };
112
+ };
113
+ 'admin/config': {
114
+ GET: {
115
+ files: Record<string, object>;
116
+ config: Record<string, unknown>;
117
+ };
118
+ };
119
+ 'admin/plugins': {
120
+ GET: PluginInternal[];
121
+ };
122
+ 'admin/audit/events': {
123
+ OPTIONS: {
124
+ name: string[];
125
+ source: string[];
126
+ tags: string[];
127
+ } | false;
128
+ GET: [filter: z.input<typeof AuditFilter>, result: AuditEvent[]];
129
+ };
130
+ 'admin/audit/:eventId': {
131
+ GET: AuditEvent;
132
+ };
91
133
  }
92
134
  export type Endpoint = keyof $API;
93
135
  export type APIFunction<Method extends RequestMethod, E extends Endpoint> = Method extends keyof $API[E] ? $API[E][Method] extends [infer Body, infer Result] ? (body: Body) => Promise<Result> : () => $API[E][Method] : unknown;
94
136
  export type RequestBody<Method extends RequestMethod, E extends Endpoint> = Method extends keyof $API[E] ? $API[E][Method] extends [infer Body, unknown] ? Body : any : unknown;
95
- export type Result<Method extends RequestMethod, E extends Endpoint> = Promise<Method extends keyof $API[E] ? ($API[E][Method] extends [unknown, infer R] ? R : $API[E][Method]) : unknown>;
137
+ export type Result<Method extends RequestMethod & keyof $API[E], E extends Endpoint> = $API[E][Method] extends [unknown, infer R] ? R : $API[E][Method];
138
+ export type AsyncResult<Method extends RequestMethod & keyof $API[E], E extends Endpoint> = Promise<Result<Method, E>>;
96
139
  export type APIParameters<S extends string> = S extends `${string}/:${infer Right}` ? [string, ...APIParameters<Right>] : [];
package/dist/apps.d.ts CHANGED
@@ -7,3 +7,4 @@ export declare const App: z.ZodObject<{
7
7
  }, z.core.$strip>;
8
8
  export interface App extends z.infer<typeof App> {
9
9
  }
10
+ export declare const apps: Map<string, App>;
package/dist/apps.js CHANGED
@@ -5,3 +5,4 @@ export const App = z.object({
5
5
  image: z.string().optional(),
6
6
  icon: z.string().optional(),
7
7
  });
8
+ export const apps = new Map();
@@ -0,0 +1,43 @@
1
+ import * as z from 'zod';
2
+ import type { User } from './user.js';
3
+ export declare enum Severity {
4
+ Emergency = 0,
5
+ Alert = 1,
6
+ Critical = 2,
7
+ Error = 3,
8
+ Warning = 4,
9
+ Notice = 5,
10
+ Info = 6,
11
+ Debug = 7
12
+ }
13
+ export declare const severityNames: Lowercase<keyof typeof Severity>[];
14
+ export interface AuditEvent<T extends Record<string, unknown> = Record<string, unknown>> {
15
+ /** UUID of the event */
16
+ id: string;
17
+ /** UUID of the user that triggered the event. `null` for events triggered via the server CLI */
18
+ userId: string | null;
19
+ user?: (Pick<User, 'id' | 'name'> & Partial<User>) | null;
20
+ /** When the event happened */
21
+ timestamp: Date;
22
+ /** How severe the event is */
23
+ severity: Severity;
24
+ /** Snake case name for the event */
25
+ name: string;
26
+ /** The source of the event. This should be a package name */
27
+ source: string;
28
+ /** Tags for the event. For example, `auth` for authentication events */
29
+ tags: string[];
30
+ /** Additional event specific data. */
31
+ extra: T;
32
+ }
33
+ export declare const AuditFilter: z.ZodObject<{
34
+ since: z.ZodOptional<z.ZodCoercedDate<unknown>>;
35
+ until: z.ZodOptional<z.ZodCoercedDate<unknown>>;
36
+ user: z.ZodOptional<z.ZodNullable<z.ZodUnion<readonly [z.ZodUUID, z.ZodPipe<z.ZodLiteral<"null">, z.ZodTransform<null, "null">>]>>>;
37
+ severity: z.ZodOptional<z.ZodPipe<z.ZodLiteral<"debug" | "error" | "emergency" | "alert" | "critical" | "warning" | "notice" | "info">, z.ZodTransform<Severity, "debug" | "error" | "emergency" | "alert" | "critical" | "warning" | "notice" | "info">>>;
38
+ source: z.ZodOptional<z.ZodString>;
39
+ tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
40
+ event: z.ZodOptional<z.ZodString>;
41
+ }, z.core.$strip>;
42
+ export interface AuditFilter extends z.infer<typeof AuditFilter> {
43
+ }
package/dist/audit.js ADDED
@@ -0,0 +1,28 @@
1
+ import { capitalize, uncapitalize } from 'utilium/string.js';
2
+ import * as z from 'zod';
3
+ export var Severity;
4
+ (function (Severity) {
5
+ Severity[Severity["Emergency"] = 0] = "Emergency";
6
+ Severity[Severity["Alert"] = 1] = "Alert";
7
+ Severity[Severity["Critical"] = 2] = "Critical";
8
+ Severity[Severity["Error"] = 3] = "Error";
9
+ Severity[Severity["Warning"] = 4] = "Warning";
10
+ Severity[Severity["Notice"] = 5] = "Notice";
11
+ Severity[Severity["Info"] = 6] = "Info";
12
+ Severity[Severity["Debug"] = 7] = "Debug";
13
+ })(Severity || (Severity = {}));
14
+ export const severityNames = Object.keys(Severity)
15
+ .filter(k => isNaN(Number(k)))
16
+ .map(uncapitalize);
17
+ export const AuditFilter = z.object({
18
+ since: z.coerce.date().optional(),
19
+ until: z.coerce.date().optional(),
20
+ user: z.union([z.uuid(), z.literal(['null']).transform(() => null)]).nullish(),
21
+ severity: z
22
+ .literal(severityNames)
23
+ .transform(v => Severity[capitalize(v)])
24
+ .optional(),
25
+ source: z.string().optional(),
26
+ tags: z.string().array().optional(),
27
+ event: z.string().optional(),
28
+ });
package/dist/auth.d.ts CHANGED
@@ -1,9 +1,12 @@
1
- export interface Session {
2
- id: string;
3
- userId: string;
4
- expires: Date;
5
- created: Date;
6
- elevated: boolean;
1
+ import * as z from 'zod';
2
+ export declare const Session: z.ZodObject<{
3
+ id: z.ZodUUID;
4
+ userId: z.ZodUUID;
5
+ expires: z.ZodCoercedDate<unknown>;
6
+ created: z.ZodCoercedDate<unknown>;
7
+ elevated: z.ZodBoolean;
8
+ }, z.core.$strip>;
9
+ export interface Session extends z.infer<typeof Session> {
7
10
  }
8
11
  export interface Verification {
9
12
  userId: string;
package/dist/auth.js CHANGED
@@ -1 +1,8 @@
1
- export {};
1
+ import * as z from 'zod';
2
+ export const Session = z.object({
3
+ id: z.uuid(),
4
+ userId: z.uuid(),
5
+ expires: z.coerce.date(),
6
+ created: z.coerce.date(),
7
+ elevated: z.boolean(),
8
+ });
@@ -0,0 +1 @@
1
+ export declare function colorHash(input: string): `rgb(${number}, ${number}, ${number})`;
package/dist/color.js ADDED
@@ -0,0 +1,11 @@
1
+ export function colorHash(input) {
2
+ let color = input.charCodeAt(0);
3
+ for (let i = 1; i < input.length; i++) {
4
+ color *= input.charCodeAt(i);
5
+ }
6
+ color &= 0xbfbfbf;
7
+ const r = (color >> 16) & 0xff;
8
+ const g = (color >> 8) & 0xff;
9
+ const b = color & 0xff;
10
+ return `rgb(${r}, ${g}, ${b})`;
11
+ }
package/dist/index.d.ts CHANGED
@@ -1,9 +1,11 @@
1
1
  export * from './access.js';
2
2
  export * from './api.js';
3
3
  export * from './apps.js';
4
+ export * from './audit.js';
4
5
  export * from './auth.js';
5
6
  export * as icons from './icons.js';
6
7
  export * from './passkeys.js';
8
+ export * from './plugins.js';
7
9
  export * from './preferences.js';
8
10
  export * from './requests.js';
9
11
  export * from './schemas.js';
package/dist/index.js CHANGED
@@ -1,9 +1,11 @@
1
1
  export * from './access.js';
2
2
  export * from './api.js';
3
3
  export * from './apps.js';
4
+ export * from './audit.js';
4
5
  export * from './auth.js';
5
6
  export * as icons from './icons.js';
6
7
  export * from './passkeys.js';
8
+ export * from './plugins.js';
7
9
  export * from './preferences.js';
8
10
  export * from './requests.js';
9
11
  export * from './schemas.js';
package/dist/io.d.ts ADDED
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Get a human-readable string for a date that also fits into CLIs well (fixed-width)
3
+ */
4
+ export declare function prettyDate(date: Date): string;
5
+ export declare let _debugOutput: boolean;
6
+ /**
7
+ * Enable or disable debug output.
8
+ */
9
+ export declare function _setDebugOutput(enabled: boolean): void;
10
+ export declare let start: (message: string) => void;
11
+ export declare let progress: (value: number, max: number, message?: any) => void;
12
+ export declare let done: () => void;
13
+ export interface ProgressIO {
14
+ start(message: string): void;
15
+ progress(value: number, max: number, message?: any): void;
16
+ done(): void;
17
+ }
18
+ export declare function useProgressIO(io: ProgressIO): void;
19
+ export declare let debug: (...args: any[]) => void;
20
+ export declare let log: (...args: any[]) => void;
21
+ export declare let info: (...args: any[]) => void;
22
+ export declare let warn: (...args: any[]) => void;
23
+ export declare let error: (...args: any[]) => void;
24
+ export interface ConsoleLike {
25
+ debug(...args: any[]): void;
26
+ log(...args: any[]): void;
27
+ info(...args: any[]): void;
28
+ warn(...args: any[]): void;
29
+ error(...args: any[]): void;
30
+ }
31
+ export declare function useOutput(output: ConsoleLike): void;
32
+ /**
33
+ * This is a factory for handling errors when performing operations.
34
+ * The handler will allow the parent scope to continue if certain errors occur,
35
+ * rather than fatally exiting.
36
+ */
37
+ export declare function someWarnings(...allowList: [RegExp, string?][]): (error: string | Error) => void;
38
+ /** @hidden @internal for Logzen */
39
+ export declare const constructor: {
40
+ name: string;
41
+ };
package/dist/io.js ADDED
@@ -0,0 +1,57 @@
1
+ const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
2
+ // Shortcut to convert to 2-digit. Mostly used to make the line shorter.
3
+ const _2 = (v) => v.toString().padStart(2, '0');
4
+ /**
5
+ * Get a human-readable string for a date that also fits into CLIs well (fixed-width)
6
+ */
7
+ export function prettyDate(date) {
8
+ return `${date.getFullYear()} ${months[date.getMonth()]} ${_2(date.getDate())} ${_2(date.getHours())}:${_2(date.getMinutes())}:${_2(date.getSeconds())}.${date.getMilliseconds().toString().padStart(3, '0')}`;
9
+ }
10
+ export let _debugOutput = false;
11
+ /**
12
+ * Enable or disable debug output.
13
+ */
14
+ export function _setDebugOutput(enabled) {
15
+ _debugOutput = enabled;
16
+ }
17
+ // I/O for "progressive" actions
18
+ export let start;
19
+ export let progress;
20
+ export let done;
21
+ export function useProgressIO(io) {
22
+ start = io.start.bind(io);
23
+ progress = io.progress.bind(io);
24
+ done = io.done.bind(io);
25
+ }
26
+ // User-facing messaging
27
+ export let debug;
28
+ export let log;
29
+ export let info;
30
+ export let warn;
31
+ export let error;
32
+ export function useOutput(output) {
33
+ debug = output.debug.bind(output);
34
+ log = output.log.bind(output);
35
+ info = output.info.bind(output);
36
+ warn = output.warn.bind(output);
37
+ error = output.error.bind(output);
38
+ }
39
+ /**
40
+ * This is a factory for handling errors when performing operations.
41
+ * The handler will allow the parent scope to continue if certain errors occur,
42
+ * rather than fatally exiting.
43
+ */
44
+ export function someWarnings(...allowList) {
45
+ return (error) => {
46
+ error = typeof error == 'object' && 'message' in error ? error.message : error;
47
+ for (const [pattern, message = error] of allowList) {
48
+ if (!pattern.test(error))
49
+ continue;
50
+ warn(message);
51
+ return;
52
+ }
53
+ throw error;
54
+ };
55
+ }
56
+ /** @hidden @internal for Logzen */
57
+ export const constructor = { name: 'Console' };
@@ -0,0 +1 @@
1
+ export declare function outputDaemonStatus(name: string): void;
@@ -0,0 +1,21 @@
1
+ import { styleText } from 'node:util';
2
+ import { spawnSync } from 'node:child_process';
3
+ export function outputDaemonStatus(name) {
4
+ process.stdout.write(styleText('whiteBright', 'Daemon: '));
5
+ const daemonIs = (sub) => spawnSync('systemctl', ['is-' + sub, name], { stdio: 'pipe', encoding: 'utf8' });
6
+ const { status: dNotActive, stdout: dStatus } = daemonIs('active');
7
+ const { status: dNotFailed } = daemonIs('failed');
8
+ const { stdout: dEnabled } = daemonIs('enabled');
9
+ if (dEnabled.trim() == 'not-found')
10
+ console.log(styleText('dim', 'not found'));
11
+ else {
12
+ process.stdout.write(dEnabled.trim() + ', ');
13
+ const status = dStatus.trim();
14
+ if (!dNotFailed)
15
+ console.log(styleText('red', status));
16
+ else if (!dNotActive)
17
+ console.log(styleText('green', status));
18
+ else
19
+ console.log(styleText('yellow', status));
20
+ }
21
+ }
@@ -0,0 +1,3 @@
1
+ export * from './daemon.js';
2
+ export * as io from './io.js';
3
+ export * from './plugins.js';
@@ -0,0 +1,3 @@
1
+ export * from './daemon.js';
2
+ export * as io from './io.js';
3
+ export * from './plugins.js';
@@ -0,0 +1,11 @@
1
+ export * from '../io.js';
2
+ export declare function setCommandTimeout(value: number): void;
3
+ /**
4
+ * Run a system command with the fancy "Example... done."
5
+ * @internal
6
+ */
7
+ export declare function run(message: string, command: string): Promise<string>;
8
+ /** Yet another convenience function */
9
+ export declare function exit(message: string | Error, code?: number): never;
10
+ export declare function handleError(e: number | string | Error): void;
11
+ export declare function writeJSON(path: string, data: any): void;
@@ -0,0 +1,84 @@
1
+ import { exec } from 'node:child_process';
2
+ import { writeFileSync } from 'node:fs';
3
+ import { styleText } from 'node:util';
4
+ import { _debugOutput, done, error, start, useOutput, useProgressIO } from '../io.js';
5
+ export * from '../io.js';
6
+ useProgressIO({
7
+ start(message) {
8
+ process.stdout.write(message + '... \x1b[s');
9
+ },
10
+ /** @todo implement additional messaging */
11
+ progress(value, max, message) {
12
+ process.stdout.write(`\x1b[u\x1b[K${value.toString().padStart(max.toString().length)}/${max}`);
13
+ if (value >= max)
14
+ console.log();
15
+ },
16
+ done() {
17
+ console.log('done.');
18
+ },
19
+ });
20
+ useOutput({
21
+ error(message) {
22
+ console.error(message.startsWith('\x1b') ? message : styleText('red', message));
23
+ },
24
+ warn(message) {
25
+ console.warn(message.startsWith('\x1b') ? message : styleText('yellow', message));
26
+ },
27
+ info(message) {
28
+ console.info(message.startsWith('\x1b') ? message : styleText('blue', message));
29
+ },
30
+ log(message) {
31
+ console.log(message);
32
+ },
33
+ debug(message) {
34
+ _debugOutput && console.debug(message.startsWith('\x1b') ? message : styleText('gray', message));
35
+ },
36
+ });
37
+ let timeout = 1000;
38
+ export function setCommandTimeout(value) {
39
+ timeout = value;
40
+ }
41
+ /**
42
+ * Run a system command with the fancy "Example... done."
43
+ * @internal
44
+ */
45
+ export async function run(message, command) {
46
+ let stderr;
47
+ try {
48
+ start(message);
49
+ const { promise, resolve, reject } = Promise.withResolvers();
50
+ exec(command, { timeout }, (err, stdout, _stderr) => {
51
+ stderr = _stderr.startsWith('ERROR:') ? _stderr.slice(6).trim() : _stderr;
52
+ if (err)
53
+ reject('[command]');
54
+ else
55
+ resolve(stdout);
56
+ });
57
+ const value = await promise;
58
+ done();
59
+ return value;
60
+ }
61
+ catch (error) {
62
+ throw error == '[command]'
63
+ ? stderr?.slice(0, 100) || 'failed.'
64
+ : typeof error == 'object' && 'message' in error
65
+ ? error.message
66
+ : error;
67
+ }
68
+ }
69
+ /** Yet another convenience function */
70
+ export function exit(message, code = 1) {
71
+ if (message instanceof Error)
72
+ message = message.message;
73
+ error(message);
74
+ process.exit(code);
75
+ }
76
+ export function handleError(e) {
77
+ if (typeof e == 'number')
78
+ process.exit(e);
79
+ else
80
+ exit(e);
81
+ }
82
+ export function writeJSON(path, data) {
83
+ writeFileSync(path, JSON.stringify(data, null, 4).replaceAll(/^( {4})+/g, match => '\t'.repeat(match.length / 4)), 'utf-8');
84
+ }
@@ -0,0 +1,3 @@
1
+ import { type PluginInternal } from '@axium/core/plugins';
2
+ export declare function pluginText(plugin: PluginInternal): Generator<string>;
3
+ export declare function loadPlugin<const T extends 'client' | 'server'>(mode: T, specifier: string, _loadedBy: string, safeMode?: boolean): Promise<void>;
@@ -0,0 +1,77 @@
1
+ import * as io from '@axium/core/node/io';
2
+ import { Plugin, plugins } from '@axium/core/plugins';
3
+ import * as fs from 'node:fs';
4
+ import { dirname, join, resolve } from 'node:path/posix';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { styleText } from 'node:util';
7
+ import * as z from 'zod';
8
+ import { apps } from '../apps.js';
9
+ export function* pluginText(plugin) {
10
+ yield styleText('whiteBright', plugin.name);
11
+ yield `Version: ${plugin.version}`;
12
+ yield `Description: ${plugin.description ?? styleText('dim', '(none)')}`;
13
+ yield `CLI Integration: ${plugin.cli ? 'Yes' : 'No'}`;
14
+ if (plugin.isServer) {
15
+ yield `Hooks: ${plugin._hooks ? styleText(['dim', 'bold'], `(${Object.keys(plugin._hooks).length}) `) + Object.keys(plugin._hooks).join(', ') : plugin.server.hooks || styleText('dim', '(none)')}`;
16
+ yield `HTTP Handler: ${plugin.server.http_handler ?? styleText('dim', '(none)')}`;
17
+ }
18
+ else {
19
+ yield `Hooks: ${plugin._client ? styleText(['dim', 'bold'], `(${Object.keys(plugin._client).length}) `) + Object.keys(plugin._client).join(', ') : plugin.client.hooks || styleText('dim', '(none)')}`;
20
+ }
21
+ }
22
+ function _locatePlugin(specifier, _loadedBy) {
23
+ if (specifier[0] == '/' || specifier.startsWith('./') || specifier.startsWith('../')) {
24
+ return resolve(dirname(_loadedBy), specifier);
25
+ }
26
+ let packageDir = dirname(fileURLToPath(import.meta.resolve(specifier)));
27
+ for (; !fs.existsSync(join(packageDir, 'package.json')); packageDir = dirname(packageDir))
28
+ ;
29
+ return join(packageDir, 'package.json');
30
+ }
31
+ export async function loadPlugin(mode, specifier, _loadedBy, safeMode = false) {
32
+ try {
33
+ const path = _locatePlugin(specifier, _loadedBy);
34
+ let imported;
35
+ try {
36
+ imported = JSON.parse(fs.readFileSync(path, 'utf8'));
37
+ }
38
+ catch {
39
+ throw new Error('Invalid or missing metadata for ' + specifier);
40
+ }
41
+ if ('axium' in imported)
42
+ Object.assign(imported, imported.axium); // support axium field in package.json
43
+ const plugin = Object.assign(await Plugin.parseAsync(imported).catch(e => {
44
+ throw e instanceof z.core.$ZodError ? z.prettifyError(e) : e;
45
+ }), { path, specifier, _loadedBy, dirname: dirname(path), cli: imported[mode]?.cli, isServer: mode === 'server' });
46
+ if (!plugin[mode])
47
+ throw `Plugin does not support running ${mode}-side`;
48
+ if (!safeMode) {
49
+ if (plugin.cli)
50
+ await import(resolve(plugin.dirname, plugin.cli));
51
+ if (mode == 'client') {
52
+ if (plugin.client.hooks)
53
+ Object.assign(plugin, { _client: await import(resolve(plugin.dirname, plugin.client.hooks)) });
54
+ }
55
+ if (mode == 'server') {
56
+ if (plugin.server.hooks)
57
+ Object.assign(plugin, { _hooks: await import(resolve(plugin.dirname, plugin.server.hooks)) });
58
+ }
59
+ }
60
+ Object.freeze(plugin);
61
+ if (plugins.has(plugin.name))
62
+ throw 'Plugin already loaded';
63
+ if (plugin.name.startsWith('#') || plugin.name.includes(' ')) {
64
+ throw 'Invalid plugin name. Plugin names can not start with a hash or contain spaces.';
65
+ }
66
+ for (const app of plugin.apps ?? []) {
67
+ if (apps.has(app.id))
68
+ throw new ReferenceError(`App with ID "${app.id}" already exists.`);
69
+ apps.set(app.id, app);
70
+ }
71
+ plugins.set(plugin.name, plugin);
72
+ io.debug(`Loaded plugin: ${plugin.name} ${plugin.version}`);
73
+ }
74
+ catch (e) {
75
+ io.warn(`Failed to load plugin from ${specifier}: ${e ? (e instanceof Error ? e.message : e.toString()) : e}`);
76
+ }
77
+ }
@@ -11,12 +11,12 @@ export declare const PasskeyRegistration: z.ZodObject<{
11
11
  authenticatorData: z.ZodOptional<z.ZodString>;
12
12
  transports: z.ZodOptional<z.ZodArray<z.ZodEnum<{
13
13
  ble: "ble";
14
+ cable: "cable";
14
15
  hybrid: "hybrid";
15
16
  internal: "internal";
16
17
  nfc: "nfc";
17
- usb: "usb";
18
- cable: "cable";
19
18
  "smart-card": "smart-card";
19
+ usb: "usb";
20
20
  }>>>;
21
21
  publicKeyAlgorithm: z.ZodOptional<z.ZodNumber>;
22
22
  publicKey: z.ZodOptional<z.ZodString>;
@@ -0,0 +1,67 @@
1
+ import * as z from 'zod';
2
+ export declare const Plugin: z.ZodObject<{
3
+ name: z.ZodString;
4
+ version: z.ZodString;
5
+ description: z.ZodOptional<z.ZodString>;
6
+ apps: z.ZodOptional<z.ZodArray<z.ZodObject<{
7
+ id: z.ZodString;
8
+ name: z.ZodOptional<z.ZodString>;
9
+ image: z.ZodOptional<z.ZodString>;
10
+ icon: z.ZodOptional<z.ZodString>;
11
+ }, z.core.$strip>>>;
12
+ client: z.ZodOptional<z.ZodObject<{
13
+ cli: z.ZodOptional<z.ZodString>;
14
+ hooks: z.ZodOptional<z.ZodString>;
15
+ }, z.core.$strip>>;
16
+ server: z.ZodOptional<z.ZodObject<{
17
+ hooks: z.ZodOptional<z.ZodString>;
18
+ http_handler: z.ZodOptional<z.ZodString>;
19
+ routes: z.ZodOptional<z.ZodString>;
20
+ cli: z.ZodOptional<z.ZodString>;
21
+ }, z.core.$strip>>;
22
+ }, z.core.$loose>;
23
+ export type Plugin = z.infer<typeof Plugin>;
24
+ export interface PluginInternal extends Plugin {
25
+ readonly path: string;
26
+ readonly dirname: string;
27
+ readonly specifier: string;
28
+ readonly _loadedBy: string;
29
+ readonly cli?: string;
30
+ /** @internal */
31
+ readonly _hooks?: ServerHooks;
32
+ /** @internal */
33
+ readonly _client?: ClientHooks;
34
+ readonly isServer: boolean;
35
+ }
36
+ export declare const plugins: Map<string, PluginInternal>;
37
+ /**
38
+ * @internal
39
+ */
40
+ export declare function _findPlugin(search: string): PluginInternal;
41
+ export declare const PluginServerHooks: z.ZodObject<{
42
+ statusText: z.ZodOptional<z.ZodCustom<z.core.$InferInnerFunctionTypeAsync<z.ZodTuple<readonly [], null>, z.ZodString>, z.core.$InferInnerFunctionTypeAsync<z.ZodTuple<readonly [], null>, z.ZodString>>>;
43
+ db_init: z.ZodOptional<z.ZodCustom<(...args: any[]) => any, (...args: any[]) => any>>;
44
+ remove: z.ZodOptional<z.ZodCustom<(...args: any[]) => any, (...args: any[]) => any>>;
45
+ db_wipe: z.ZodOptional<z.ZodCustom<(...args: any[]) => any, (...args: any[]) => any>>;
46
+ clean: z.ZodOptional<z.ZodCustom<(...args: any[]) => any, (...args: any[]) => any>>;
47
+ }, z.core.$strip>;
48
+ interface _InitOptions {
49
+ force?: boolean;
50
+ skip: boolean;
51
+ check: boolean;
52
+ }
53
+ export interface ServerHooks {
54
+ statusText?(): string | Promise<string>;
55
+ db_init?: (opt: _InitOptions) => void | Promise<void>;
56
+ remove?: (opt: {
57
+ force?: boolean;
58
+ }) => void | Promise<void>;
59
+ db_wipe?: (opt: {
60
+ force?: boolean;
61
+ }) => void | Promise<void>;
62
+ clean?: (opt: Partial<_InitOptions>) => void | Promise<void>;
63
+ }
64
+ export interface ClientHooks {
65
+ run(): void | Promise<void>;
66
+ }
67
+ export {};
@@ -0,0 +1,46 @@
1
+ import * as z from 'zod';
2
+ import { zAsyncFunction } from './schemas.js';
3
+ import { App } from './apps.js';
4
+ export const Plugin = z.looseObject({
5
+ name: z.string(),
6
+ version: z.string(),
7
+ description: z.string().optional(),
8
+ apps: z.array(App).optional(),
9
+ client: z
10
+ .object({
11
+ /** CLI mixin path */
12
+ cli: z.string().optional(),
13
+ /** The path to the hooks script */
14
+ hooks: z.string().optional(),
15
+ })
16
+ .optional(),
17
+ server: z
18
+ .object({
19
+ /** The path to the hooks script */
20
+ hooks: z.string().optional(),
21
+ http_handler: z.string().optional(),
22
+ /** The path to the HTTP handler used by the server */
23
+ routes: z.string().optional(),
24
+ /** CLI mixin path */
25
+ cli: z.string().optional(),
26
+ })
27
+ .optional(),
28
+ });
29
+ export const plugins = new Map();
30
+ /**
31
+ * @internal
32
+ */
33
+ export function _findPlugin(search) {
34
+ const plugin = plugins.get(search) ?? plugins.values().find(p => p.specifier.toLowerCase() == search.toLowerCase());
35
+ if (!plugin)
36
+ throw `Can't find a plugin matching "${search}"`;
37
+ return plugin;
38
+ }
39
+ const fn = z.custom(data => typeof data === 'function');
40
+ export const PluginServerHooks = z.object({
41
+ statusText: zAsyncFunction(z.function({ input: [], output: z.string() })).optional(),
42
+ db_init: fn.optional(),
43
+ remove: fn.optional(),
44
+ db_wipe: fn.optional(),
45
+ clean: fn.optional(),
46
+ });
package/dist/user.d.ts CHANGED
@@ -8,16 +8,22 @@ export declare const User: z.ZodObject<{
8
8
  image: z.ZodOptional<z.ZodNullable<z.ZodURL>>;
9
9
  preferences: z.ZodRecord<z.ZodString, z.ZodAny>;
10
10
  roles: z.ZodArray<z.ZodString>;
11
- registeredAt: z.ZodDate;
11
+ registeredAt: z.ZodCoercedDate<unknown>;
12
+ isAdmin: z.ZodBoolean;
12
13
  }, z.core.$strip>;
13
14
  export interface User extends z.infer<typeof User> {
14
15
  preferences: Preferences;
15
16
  }
17
+ export interface UserInternal extends User {
18
+ isSuspended: boolean;
19
+ /** Tags are internal, roles are public */
20
+ tags: string[];
21
+ }
16
22
  export declare const userPublicFields: ["id", "image", "name", "registeredAt", "roles"];
17
23
  type UserPublicField = (typeof userPublicFields)[number];
18
24
  export interface UserPublic extends Pick<User, UserPublicField> {
19
25
  }
20
- export declare const userProtectedFields: ["email", "emailVerified", "preferences"];
26
+ export declare const userProtectedFields: ["email", "emailVerified", "preferences", "isAdmin"];
21
27
  export declare const UserChangeable: z.ZodObject<{
22
28
  email: z.ZodOptional<z.ZodEmail>;
23
29
  name: z.ZodOptional<z.ZodString>;
@@ -41,12 +47,12 @@ export declare const UserRegistration: z.ZodObject<{
41
47
  authenticatorData: z.ZodOptional<z.ZodString>;
42
48
  transports: z.ZodOptional<z.ZodArray<z.ZodEnum<{
43
49
  ble: "ble";
50
+ cable: "cable";
44
51
  hybrid: "hybrid";
45
52
  internal: "internal";
46
53
  nfc: "nfc";
47
- usb: "usb";
48
- cable: "cable";
49
54
  "smart-card": "smart-card";
55
+ usb: "usb";
50
56
  }>>>;
51
57
  publicKeyAlgorithm: z.ZodOptional<z.ZodNumber>;
52
58
  publicKey: z.ZodOptional<z.ZodString>;
@@ -57,7 +63,7 @@ export declare const UserRegistration: z.ZodObject<{
57
63
  }, z.core.$strip>;
58
64
  }, z.core.$strip>;
59
65
  export declare const UserAuthOptions: z.ZodObject<{
60
- type: z.ZodLiteral<"login" | "action">;
66
+ type: z.ZodLiteral<"login" | "action" | "client_login">;
61
67
  }, z.core.$strip>;
62
68
  export type UserAuthOptions = z.infer<typeof UserAuthOptions>;
63
69
  export declare const LogoutSessions: z.ZodObject<{
package/dist/user.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import * as z from 'zod';
2
2
  import { PasskeyRegistration } from './passkeys.js';
3
+ import { colorHash } from './color.js';
3
4
  export const User = z.object({
4
5
  id: z.uuid(),
5
6
  name: z.string().min(1, 'Name is required').max(255, 'Name is too long'),
@@ -8,10 +9,11 @@ export const User = z.object({
8
9
  image: z.url().nullish(),
9
10
  preferences: z.record(z.string(), z.any()),
10
11
  roles: z.array(z.string()),
11
- registeredAt: z.date(),
12
+ registeredAt: z.coerce.date(),
13
+ isAdmin: z.boolean(),
12
14
  });
13
15
  export const userPublicFields = ['id', 'image', 'name', 'registeredAt', 'roles'];
14
- export const userProtectedFields = ['email', 'emailVerified', 'preferences'];
16
+ export const userProtectedFields = ['email', 'emailVerified', 'preferences', 'isAdmin'];
15
17
  export const UserChangeable = User.pick({
16
18
  name: true,
17
19
  email: true,
@@ -21,17 +23,9 @@ export const UserChangeable = User.pick({
21
23
  export function getUserImage(user) {
22
24
  if (user.image)
23
25
  return user.image;
24
- user.name ??= '\0';
25
- let color = user.name.charCodeAt(0);
26
- for (let i = 1; i < user.name.length; i++) {
27
- color *= user.name.charCodeAt(i);
28
- }
29
- color &= 0xbfbfbf;
30
- const r = (color >> 16) & 0xff;
31
- const g = (color >> 8) & 0xff;
32
- const b = color & 0xff;
33
- return `data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50" style="background-color:rgb(${r},${g},${b});display:flex;align-items:center;justify-content:center;">
34
- <text x="23" y="28" style="font-family:sans-serif;font-weight:bold;" fill="white">${user.name.replaceAll(/\W/g, '')[0]}</text>
26
+ const color = colorHash(user.name ?? '\0');
27
+ return `data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50" style="background-color:${color};display:flex;align-items:center;justify-content:center;">
28
+ <text x="23" y="28" style="font-family:sans-serif;font-weight:bold;" fill="white">${(user.name ?? '?').replaceAll(/\W/g, '')[0]}</text>
35
29
  </svg>`.replaceAll(/[\t\n]/g, '');
36
30
  }
37
31
  export const UserRegistration = z.object({
@@ -40,7 +34,7 @@ export const UserRegistration = z.object({
40
34
  userId: z.uuid(),
41
35
  response: PasskeyRegistration,
42
36
  });
43
- export const UserAuthOptions = z.object({ type: z.literal(['login', 'action']) });
37
+ export const UserAuthOptions = z.object({ type: z.literal(['login', 'action', 'client_login']) });
44
38
  export const LogoutSessions = z.object({
45
39
  id: z.array(z.uuid()).optional(),
46
40
  confirm_all: z.boolean().optional(),
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@axium/core",
3
- "version": "0.7.0",
4
- "author": "James Prevett <axium@jamespre.dev> (https://jamespre.dev)",
3
+ "version": "0.9.0",
4
+ "author": "James Prevett <axium@jamespre.dev>",
5
5
  "funding": {
6
6
  "type": "individual",
7
7
  "url": "https://github.com/sponsors/james-pre"
@@ -20,7 +20,8 @@
20
20
  "types": "dist/index.d.ts",
21
21
  "exports": {
22
22
  ".": "./dist/index.js",
23
- "./*": "./dist/*.js"
23
+ "./*": "./dist/*.js",
24
+ "./node": "./dist/node/index.js"
24
25
  },
25
26
  "files": [
26
27
  "dist",