@axium/server 0.26.3 → 0.28.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.
@@ -1,4 +1,4 @@
1
- import type { User, UserInternal } from '@axium/core';
1
+ import { type User, type UserInternal } from '@axium/core';
2
2
  import * as z from 'zod';
3
3
  import type { ServerRoute } from './routes.js';
4
4
  /**
package/dist/requests.js CHANGED
@@ -1,7 +1,10 @@
1
+ import {} from '@axium/core';
2
+ import * as io from '@axium/core/io';
1
3
  import { userProtectedFields, userPublicFields } from '@axium/core/user';
2
4
  import * as cookie from 'cookie_v1';
3
5
  import { pick } from 'utilium';
4
6
  import * as z from 'zod';
7
+ import { audit } from './audit.js';
5
8
  import { createSession } from './auth.js';
6
9
  import { config } from './config.js';
7
10
  export function isResponseError(e) {
@@ -25,7 +28,7 @@ export function redirect(location, status = 302) {
25
28
  export function json(data, init) {
26
29
  const response = Response.json(data, init);
27
30
  if (!response.headers.has('content-length')) {
28
- response.headers.set('content-length', JSON.stringify(data).length.toString());
31
+ response.headers.set('content-length', new TextEncoder().encode(JSON.stringify(data)).length.toString());
29
32
  }
30
33
  return response;
31
34
  }
@@ -71,6 +74,10 @@ export function withError(text, code = 500) {
71
74
  return function (e) {
72
75
  if (e.name == 'ResponseError')
73
76
  throw e;
77
+ if (code == 500) {
78
+ void audit('response_error', undefined, { stack: e.stack });
79
+ io.error('(in response) ' + e.stack);
80
+ }
74
81
  error(code, text + (config.debug && e.message ? `: ${e.message}` : ''));
75
82
  };
76
83
  }
package/dist/routes.d.ts CHANGED
@@ -1,53 +1,53 @@
1
1
  import type { RequestMethod } from '@axium/core/requests';
2
2
  import type { Component } from 'svelte';
3
3
  import type z from 'zod';
4
- type _Params = Partial<Record<string, string>>;
4
+ type RouteParams = Record<string, z.ZodType>;
5
+ type ParamValues<P extends RouteParams> = {
6
+ [K in keyof P]: z.infer<P[K]>;
7
+ };
5
8
  export type MaybePromise<T> = T | Promise<T>;
6
- export type EndpointHandlers<Params extends _Params = _Params, This = unknown> = Partial<Record<RequestMethod, (this: This, request: Request, params: Params) => MaybePromise<object | Response>>>;
7
- export type RouteParamOptions = z.ZodType;
8
- export interface CommonRouteOptions<Params extends _Params = _Params> {
9
- path: string;
10
- params?: {
11
- [K in keyof Params]?: RouteParamOptions;
12
- };
13
- }
9
+ export type EndpointHandlers<Params, This = unknown> = Partial<Record<RequestMethod, (this: This, request: Request, params: Params) => MaybePromise<object | Response>>>;
14
10
  /**
15
11
  * A route with server-side handlers for different HTTP methods.
16
12
  */
17
- export interface ServerRouteOptions<Params extends _Params = _Params> extends CommonRouteOptions<Params>, EndpointHandlers<Params, RouteCommon> {
13
+ export interface ServerRouteInit<Params extends RouteParams = RouteParams> extends EndpointHandlers<ParamValues<Params>, RouteCommon<Params>> {
14
+ path: string;
15
+ params?: Params;
18
16
  api?: boolean;
19
17
  }
20
- export interface WebRouteOptions extends CommonRouteOptions {
18
+ export interface WebRouteInit<Params extends RouteParams = RouteParams> {
19
+ path: string;
20
+ params?: Params;
21
21
  load?(request: Request): object | Promise<object>;
22
22
  /** the Svelte page */
23
23
  page?: Component;
24
24
  }
25
- export type RouteOptions = ServerRouteOptions | WebRouteOptions;
26
- export interface RouteCommon {
25
+ export type RouteInit<Params extends RouteParams = RouteParams> = ServerRouteInit<Params> | WebRouteInit<Params>;
26
+ export interface RouteCommon<Params extends RouteParams = RouteParams> {
27
27
  path: string;
28
- params?: Record<string, RouteParamOptions>;
28
+ params?: Params;
29
29
  }
30
- export interface ServerRoute extends RouteCommon, EndpointHandlers {
30
+ export interface ServerRoute<Params extends RouteParams = RouteParams> extends RouteCommon<Params>, EndpointHandlers<ParamValues<Params>> {
31
31
  api: boolean;
32
32
  server: true;
33
33
  }
34
- export interface WebRoute extends RouteCommon {
34
+ export interface WebRoute<Params extends RouteParams = RouteParams> extends RouteCommon<Params> {
35
35
  server: false;
36
36
  load?(request: Request): object | Promise<object>;
37
37
  page: Component;
38
38
  }
39
- export type Route = ServerRoute | WebRoute;
39
+ export type Route<Params extends RouteParams = RouteParams> = ServerRoute<Params> | WebRoute<Params>;
40
40
  /**
41
41
  * @internal
42
42
  */
43
- export declare const routes: Map<string, Route>;
43
+ export declare const routes: Map<string, Route<any>>;
44
44
  /**
45
45
  * @category Plugin API
46
46
  */
47
- export declare function addRoute(opt: RouteOptions): void;
47
+ export declare function addRoute<const P extends RouteParams = RouteParams>(opt: RouteInit<P>): void;
48
48
  /**
49
49
  * Resolve a request URL into a route.
50
50
  * This handles parsing of parameters in the URL.
51
51
  */
52
- export declare function resolveRoute(url: URL): [Route, params: object] | void;
52
+ export declare function resolveRoute<P extends RouteParams = RouteParams>(url: URL): [Route<P>, params: ParamValues<P>] | void;
53
53
  export {};
package/dist/routes.js CHANGED
@@ -27,8 +27,9 @@ export function addRoute(opt) {
27
27
  */
28
28
  export function resolveRoute(url) {
29
29
  const { pathname } = url;
30
- if (routes.has(pathname) && !pathname.split('/').some(p => p.startsWith(':')))
30
+ if (routes.has(pathname) && !pathname.split('/').some(p => p.startsWith(':'))) {
31
31
  return [routes.get(pathname), {}];
32
+ }
32
33
  // Otherwise we must have a parameterized route
33
34
  _routes: for (const route of routes.values()) {
34
35
  const params = {};
package/dist/serve.js CHANGED
@@ -131,7 +131,7 @@ async function _runRoute(run, request, params) {
131
131
  async function _getLinkedBuildHandler(buildPath = '../build/handler.js') {
132
132
  const { handler: handleFrontendRequest } = await import(buildPath);
133
133
  return function handle(req, res) {
134
- const url = new URL(req.url, config.auth.origin);
134
+ const url = new URL(req.url, config.origin);
135
135
  const [route, params = {}] = resolveRoute(url) ?? [];
136
136
  if (!route && url.pathname === '/' && config.debug_home) {
137
137
  res.writeHead(303, { Location: '/_axium/default' }).end();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@axium/server",
3
- "version": "0.26.3",
3
+ "version": "0.28.0",
4
4
  "author": "James Prevett <axium@jamespre.dev>",
5
5
  "funding": {
6
6
  "type": "individual",
@@ -31,6 +31,7 @@
31
31
  "assets",
32
32
  "dist",
33
33
  "routes",
34
+ "schemas",
34
35
  "svelte.config.js",
35
36
  "web.tsconfig.json",
36
37
  "template.html",
@@ -38,15 +39,16 @@
38
39
  "axium.service"
39
40
  ],
40
41
  "bin": {
41
- "axium": "dist/cli.js"
42
+ "axium": "dist/main.js"
42
43
  },
43
44
  "scripts": {
44
45
  "build": "tsc",
46
+ "build:schemas": "mkdir -p schemas && npx axium config schema -j > schemas/config.json && npx axium db schema -j > schemas/db.json",
45
47
  "clean": "rm -rf build .svelte-kit node_modules/{.vite,.vite-temp}"
46
48
  },
47
49
  "peerDependencies": {
48
- "@axium/client": ">=0.6.0",
49
- "@axium/core": ">=0.10.0",
50
+ "@axium/client": ">=0.9.0",
51
+ "@axium/core": ">=0.12.0",
50
52
  "kysely": "^0.28.0",
51
53
  "utilium": "^2.3.8",
52
54
  "zod": "^4.0.5"
@@ -8,8 +8,6 @@
8
8
  const { data }: PageProps = $props();
9
9
  const { canVerify } = data;
10
10
 
11
- const dialogs = $state<Record<string, HTMLDialogElement>>({});
12
-
13
11
  let verificationSent = $state(false);
14
12
  let currentSession = $state(data.currentSession);
15
13
  let user = $state(data.user);
@@ -27,7 +25,7 @@
27
25
  </svelte:head>
28
26
 
29
27
  {#snippet action(name: string, i: string = 'pen')}
30
- <button style:display="contents" onclick={() => dialogs[name].showModal()}>
28
+ <button style:display="contents" commandfor={name} command="show-modal">
31
29
  <Icon {i} --size="16px" />
32
30
  </button>
33
31
  {/snippet}
@@ -45,7 +43,7 @@
45
43
  <p>{user.name}</p>
46
44
  {@render action('edit_name')}
47
45
  </div>
48
- <FormDialog bind:dialog={dialogs.edit_name} submit={_editUser} submitText="Change">
46
+ <FormDialog id="edit_name" submit={_editUser} submitText="Change">
49
47
  <div>
50
48
  <label for="name">What do you want to be called?</label>
51
49
  <input name="name" type="text" value={user.name || ''} required />
@@ -67,7 +65,7 @@
67
65
  </p>
68
66
  {@render action('edit_email')}
69
67
  </div>
70
- <FormDialog bind:dialog={dialogs.edit_email} submit={_editUser} submitText="Change">
68
+ <FormDialog id="edit_email" submit={_editUser} submitText="Change">
71
69
  <div>
72
70
  <label for="email">Email Address</label>
73
71
  <input name="email" type="email" value={user.email || ''} required />
@@ -80,11 +78,11 @@
80
78
  <ClipboardCopy value={user.id} --size="16px" />
81
79
  </div>
82
80
  <span>
83
- <button class="signout" onclick={() => dialogs.logout.showModal()}>Sign Out</button>
84
- <button style:cursor="pointer" onclick={() => dialogs.delete.showModal()} class="danger">Delete Account</button>
85
- <Logout bind:dialog={dialogs.logout} />
81
+ <button class="signout" command="show-modal" commandfor="logout">Sign Out</button>
82
+ <button style:cursor="pointer" command="show-modal" commandfor="delete" class="danger">Delete Account</button>
83
+ <Logout />
86
84
  <FormDialog
87
- bind:dialog={dialogs.delete}
85
+ id="delete"
88
86
  submit={() => deleteUser(user.id).then(() => (window.location.href = '/'))}
89
87
  submitText="Delete Account"
90
88
  submitDanger
@@ -110,9 +108,9 @@
110
108
  <p class="subtle"><i>Unnamed</i></p>
111
109
  {/if}
112
110
  <p>Created {passkey.createdAt.toLocaleString()}</p>
113
- {@render action('edit_passkey#' + passkey.id)}
111
+ {@render action('edit_passkey:' + passkey.id)}
114
112
  {#if passkeys.length > 1}
115
- {@render action('delete_passkey#' + passkey.id, 'trash')}
113
+ {@render action('delete_passkey:' + passkey.id, 'trash')}
116
114
  {:else}
117
115
  <dfn title="You must have at least one passkey" class="disabled">
118
116
  <Icon i="trash-slash" --fill="#888" --size="16px" />
@@ -120,7 +118,7 @@
120
118
  {/if}
121
119
  </div>
122
120
  <FormDialog
123
- bind:dialog={dialogs['edit_passkey#' + passkey.id]}
121
+ id={'edit_passkey:' + passkey.id}
124
122
  submit={data => {
125
123
  if (typeof data.name != 'string') throw 'Passkey name must be a string';
126
124
  passkey.name = data.name;
@@ -134,7 +132,7 @@
134
132
  </div>
135
133
  </FormDialog>
136
134
  <FormDialog
137
- bind:dialog={dialogs['delete_passkey#' + passkey.id]}
135
+ id={'delete_passkey:' + passkey.id}
138
136
  submit={() => deletePasskey(passkey.id).then(() => passkeys.splice(passkeys.indexOf(passkey), 1))}
139
137
  submitText="Delete"
140
138
  submitDanger={true}
@@ -40,4 +40,9 @@
40
40
 
41
41
  <h4>Extra Data</h4>
42
42
 
43
- <pre>{JSON.stringify(event.extra, null, 4)}</pre>
43
+ {#if event.name == 'response_error'}
44
+ <h5>Error Stack</h5>
45
+ <pre>{event.extra.stack}</pre>
46
+ {:else}
47
+ <pre>{JSON.stringify(event.extra, null, 4)}</pre>
48
+ {/if}
@@ -15,7 +15,11 @@
15
15
  <p><strong>Author:</strong> {plugin.author}</p>
16
16
  <p>
17
17
  <strong>Provided apps:</strong>
18
- {#if plugin.apps?.length}{plugin.apps?.map(a => a.name).join(', ')}{:else}<i>None</i>{/if}
18
+ {#if plugin.apps?.length}
19
+ {#each plugin.apps as app, i}
20
+ <a href="/{app.id}">{app.name}</a>{i != plugin.apps.length - 1 ? ', ' : ''}
21
+ {/each}
22
+ {:else}<i>None</i>{/if}
19
23
  </p>
20
24
  <p>{plugin.description}</p>
21
25
  {:else}
@@ -0,0 +1,207 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "type": "object",
4
+ "properties": {
5
+ "admin_api": {
6
+ "type": "boolean"
7
+ },
8
+ "allow_new_users": {
9
+ "type": "boolean"
10
+ },
11
+ "apps": {
12
+ "type": "object",
13
+ "properties": {
14
+ "disabled": {
15
+ "type": "array",
16
+ "items": {
17
+ "type": "string"
18
+ }
19
+ }
20
+ },
21
+ "additionalProperties": {}
22
+ },
23
+ "audit": {
24
+ "type": "object",
25
+ "properties": {
26
+ "allow_raw": {
27
+ "type": "boolean"
28
+ },
29
+ "retention": {
30
+ "type": "number",
31
+ "minimum": 0
32
+ },
33
+ "min_severity": {
34
+ "type": "string",
35
+ "enum": [
36
+ "emergency",
37
+ "alert",
38
+ "critical",
39
+ "error",
40
+ "warning",
41
+ "notice",
42
+ "info",
43
+ "debug",
44
+ "Emergency",
45
+ "Alert",
46
+ "Critical",
47
+ "Error",
48
+ "Warning",
49
+ "Notice",
50
+ "Info",
51
+ "Debug"
52
+ ]
53
+ },
54
+ "auto_suspend": {
55
+ "type": "string",
56
+ "enum": [
57
+ "emergency",
58
+ "alert",
59
+ "critical",
60
+ "error",
61
+ "warning",
62
+ "notice",
63
+ "info",
64
+ "debug",
65
+ "Emergency",
66
+ "Alert",
67
+ "Critical",
68
+ "Error",
69
+ "Warning",
70
+ "Notice",
71
+ "Info",
72
+ "Debug"
73
+ ]
74
+ }
75
+ },
76
+ "additionalProperties": {}
77
+ },
78
+ "auth": {
79
+ "type": "object",
80
+ "properties": {
81
+ "passkey_probation": {
82
+ "type": "number"
83
+ },
84
+ "rp_id": {
85
+ "type": "string"
86
+ },
87
+ "rp_name": {
88
+ "type": "string"
89
+ },
90
+ "secure_cookies": {
91
+ "type": "boolean"
92
+ },
93
+ "verification_timeout": {
94
+ "type": "number"
95
+ },
96
+ "email_verification": {
97
+ "type": "boolean"
98
+ },
99
+ "header_only": {
100
+ "type": "boolean"
101
+ }
102
+ },
103
+ "additionalProperties": {}
104
+ },
105
+ "db": {
106
+ "type": "object",
107
+ "properties": {
108
+ "host": {
109
+ "type": "string"
110
+ },
111
+ "port": {
112
+ "type": "number"
113
+ },
114
+ "password": {
115
+ "type": "string"
116
+ },
117
+ "user": {
118
+ "type": "string"
119
+ },
120
+ "database": {
121
+ "type": "string"
122
+ }
123
+ },
124
+ "additionalProperties": {}
125
+ },
126
+ "debug": {
127
+ "type": "boolean"
128
+ },
129
+ "debug_home": {
130
+ "type": "boolean"
131
+ },
132
+ "log": {
133
+ "type": "object",
134
+ "properties": {
135
+ "level": {
136
+ "type": "string",
137
+ "enum": [
138
+ "error",
139
+ "warn",
140
+ "notice",
141
+ "info",
142
+ "debug"
143
+ ]
144
+ },
145
+ "console": {
146
+ "type": "boolean"
147
+ }
148
+ },
149
+ "additionalProperties": {}
150
+ },
151
+ "origin": {
152
+ "type": "string"
153
+ },
154
+ "request_size_limit": {
155
+ "type": "number",
156
+ "minimum": 0
157
+ },
158
+ "show_duplicate_state": {
159
+ "type": "boolean"
160
+ },
161
+ "web": {
162
+ "type": "object",
163
+ "properties": {
164
+ "disable_cache": {
165
+ "type": "boolean"
166
+ },
167
+ "port": {
168
+ "type": "number",
169
+ "minimum": 1,
170
+ "maximum": 65535
171
+ },
172
+ "prefix": {
173
+ "type": "string"
174
+ },
175
+ "routes": {
176
+ "type": "string"
177
+ },
178
+ "secure": {
179
+ "type": "boolean"
180
+ },
181
+ "ssl_key": {
182
+ "type": "string"
183
+ },
184
+ "ssl_cert": {
185
+ "type": "string"
186
+ },
187
+ "build": {
188
+ "type": "string"
189
+ }
190
+ },
191
+ "additionalProperties": {}
192
+ },
193
+ "include": {
194
+ "type": "array",
195
+ "items": {
196
+ "type": "string"
197
+ }
198
+ },
199
+ "plugins": {
200
+ "type": "array",
201
+ "items": {
202
+ "type": "string"
203
+ }
204
+ }
205
+ },
206
+ "additionalProperties": {}
207
+ }