@axium/server 0.42.1 → 0.43.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.
@@ -1,11 +1,6 @@
1
- export interface ImageUploadConfig {
2
- enabled: boolean;
3
- /** Max size in KB */
4
- max_size: number;
5
- /** Max pixels per dimension */
6
- max_length: number;
7
- }
8
- export declare function checkImageUpload(request: Request, cfg: ImageUploadConfig, userId: string): Promise<{
1
+ import { type ImageUploadConfig } from '../config.js';
2
+ export interface PreparedImageUpload {
9
3
  data: Uint8Array<ArrayBuffer>;
10
4
  type: string;
11
- }>;
5
+ }
6
+ export declare function prepareImageUpload(request: Request, cfg: ImageUploadConfig, userId: string): Promise<PreparedImageUpload>;
@@ -1,32 +1,13 @@
1
+ import { debug } from 'ioium';
1
2
  import { sql } from 'kysely';
2
- import { warn } from 'ioium/node';
3
- import { execSync } from 'node:child_process';
3
+ import sharp from 'sharp';
4
4
  import * as z from 'zod';
5
5
  import { checkAuthForUser } from '../auth.js';
6
6
  import { config } from '../config.js';
7
7
  import { database as db } from '../database.js';
8
8
  import { error, withError } from '../requests.js';
9
9
  import { addRoute } from '../routes.js';
10
- let imageSize;
11
- try {
12
- const mod = await import('image-size');
13
- imageSize = mod.imageSize;
14
- }
15
- catch {
16
- try {
17
- // Fall back to `identify` from ImageMagick
18
- execSync('command -v identify');
19
- imageSize = function identifyImageSize(input) {
20
- const stdout = execSync('identify -ping -format "%w %h" -', { input, timeout: 1000 });
21
- const [width, height] = stdout.toString().trim().split(' ').map(Number);
22
- return { width, height };
23
- };
24
- }
25
- catch {
26
- warn('Can not determine profile picture dimensions because neither image-size or ImageMagick is available');
27
- }
28
- }
29
- export async function checkImageUpload(request, cfg, userId) {
10
+ export async function prepareImageUpload(request, cfg, userId) {
30
11
  const { enabled, max_size, max_length } = cfg;
31
12
  if (!enabled)
32
13
  error(503, 'Image uploads are disabled');
@@ -36,20 +17,29 @@ export async function checkImageUpload(request, cfg, userId) {
36
17
  error(400, 'Missing Content-Type header');
37
18
  if (!type.startsWith('image/'))
38
19
  error(415, 'Only image files are allowed');
39
- const size = Number(request.headers.get('content-length'));
40
- if (!Number.isSafeInteger(size))
20
+ const contentLength = Number(request.headers.get('content-length'));
21
+ if (!Number.isSafeInteger(contentLength))
41
22
  error(400, 'Invalid Content-Length header');
42
- if (max_size && size / 1000 > max_size)
43
- error(413, `Image must be smaller than ${max_size} KB`);
44
- const data = await request.bytes();
45
- if (data.byteLength != size)
23
+ const rawData = await request.bytes();
24
+ if (rawData.byteLength != contentLength)
46
25
  error(400, 'Content-Length does not match actual data size');
47
- const { width, height } = imageSize?.(data) || { width: 0, height: 0 };
48
- if (imageSize && (!width || !height))
26
+ const { data, info } = await sharp(rawData)
27
+ .autoOrient()
28
+ .timeout({ seconds: 10 })
29
+ .resize({ width: max_length, height: max_length, fit: 'cover', withoutEnlargement: true })
30
+ .toBuffer({ resolveWithObject: true });
31
+ const { width, height, size } = info;
32
+ if (!width || !height)
49
33
  error(400, 'Invalid image dimensions');
50
- if (max_length && (width > max_length || height > max_length))
34
+ debug(`Prepared image upload: ${Math.round(size / 1000)}KB @ ${width}x${height}`);
35
+ if (max_size && size / 1000 > max_size)
36
+ error(413, `Image must be smaller than ${max_size} KB (got ${Math.round(size / 1000)} KB)`);
37
+ if (max_length && (width > max_length || height > max_length)) {
51
38
  error(413, `Image must be smaller than ${max_length}x${max_length} pixels`);
52
- return { data, type };
39
+ }
40
+ if (!(data.buffer instanceof ArrayBuffer))
41
+ error(500, 'Unexpectedly got a shared buffer from sharp.');
42
+ return { data: data, type };
53
43
  }
54
44
  addRoute({
55
45
  path: '/raw/pfp/:id',
@@ -101,7 +91,7 @@ addRoute({
101
91
  });
102
92
  },
103
93
  async POST(request, { id: userId }) {
104
- const { data, type } = await checkImageUpload(request, config.user_pfp, userId);
94
+ const { data, type } = await prepareImageUpload(request, config.user_pfp, userId);
105
95
  const { isInsert } = await db
106
96
  .insertInto('profile_pictures')
107
97
  .values({ userId, data, type })
package/dist/auth.js CHANGED
@@ -181,7 +181,7 @@ export async function authSessionForItem(itemType, itemId, permissions, session,
181
181
  * This will fetch the item, ACLs, users, and the authenticating session.
182
182
  */
183
183
  export async function authRequestForItem(request, itemType, itemId, permissions, recursive = false) {
184
- let session = null;
184
+ let session;
185
185
  try {
186
186
  const token = getToken(request, false);
187
187
  if (!token)
package/dist/config.d.ts CHANGED
@@ -1,5 +1,12 @@
1
1
  import { type DeepRequired } from 'utilium';
2
2
  import * as z from 'zod';
3
+ export declare const ImageUploadConfig: z.ZodObject<{
4
+ enabled: z.ZodBoolean;
5
+ max_size: z.ZodNumber;
6
+ max_length: z.ZodInt;
7
+ }, z.core.$strip>;
8
+ export interface ImageUploadConfig extends z.infer<typeof ImageUploadConfig> {
9
+ }
3
10
  export declare const Config: z.ZodObject<{
4
11
  admin_api: z.ZodOptional<z.ZodBoolean>;
5
12
  allow_new_users: z.ZodOptional<z.ZodBoolean>;
@@ -45,7 +52,7 @@ export declare const Config: z.ZodObject<{
45
52
  user_pfp: z.ZodOptional<z.ZodObject<{
46
53
  enabled: z.ZodOptional<z.ZodBoolean>;
47
54
  max_size: z.ZodOptional<z.ZodNumber>;
48
- max_length: z.ZodOptional<z.ZodNumber>;
55
+ max_length: z.ZodOptional<z.ZodInt>;
49
56
  }, z.core.$loose>>;
50
57
  verifications: z.ZodOptional<z.ZodObject<{
51
58
  timeout: z.ZodOptional<z.ZodNumber>;
@@ -127,7 +134,7 @@ export declare const ConfigFile: z.ZodObject<{
127
134
  user_pfp: z.ZodOptional<z.ZodOptional<z.ZodObject<{
128
135
  enabled: z.ZodOptional<z.ZodBoolean>;
129
136
  max_size: z.ZodOptional<z.ZodNumber>;
130
- max_length: z.ZodOptional<z.ZodNumber>;
137
+ max_length: z.ZodOptional<z.ZodInt>;
131
138
  }, z.core.$loose>>>;
132
139
  verifications: z.ZodOptional<z.ZodOptional<z.ZodObject<{
133
140
  timeout: z.ZodOptional<z.ZodNumber>;
package/dist/config.js CHANGED
@@ -9,6 +9,14 @@ import * as z from 'zod';
9
9
  import { dirs, logger, systemDir } from './io.js';
10
10
  import { _duplicateStateWarnings, _unique } from './state.js';
11
11
  const audit_severity_levels = ['emergency', 'alert', 'critical', 'error', 'warning', 'notice', 'info', 'debug'];
12
+ export const ImageUploadConfig = z.object({
13
+ /** Whether images can be uploaded */
14
+ enabled: z.boolean(),
15
+ /** Max image size in KB. Set to zero for no limit */
16
+ max_size: z.number().min(0),
17
+ /** Max dimensions on a side. Set to zero for no limit */
18
+ max_length: z.int().min(0),
19
+ });
12
20
  export const Config = z
13
21
  .looseObject({
14
22
  /** Whether /api/admin is enabled */
@@ -63,16 +71,8 @@ export const Config = z
63
71
  show_duplicate_state: z.boolean(),
64
72
  /** Who can use the user discovery API. For example, setting to `admin` means regular users need to type a full email in the ACL dialog and won't be shown results */
65
73
  user_discovery: z.literal(['disabled', 'admin', 'user', 'public']),
66
- user_pfp: z
67
- .looseObject({
68
- /** Whether user's can upload custom profile pictures */
69
- enabled: z.boolean(),
70
- /** Max PFP size in KB. Set to zero for no limit */
71
- max_size: z.number().min(0),
72
- /** Max dimensions on a side. Set to zero for no limit */
73
- max_length: z.number().min(0),
74
- })
75
- .partial(),
74
+ /** Configuration for user profile pictures */
75
+ user_pfp: ImageUploadConfig.loose().partial(),
76
76
  verifications: z
77
77
  .looseObject({
78
78
  /** In minutes */
@@ -143,7 +143,7 @@ export const defaultConfig = {
143
143
  user_pfp: {
144
144
  enabled: true,
145
145
  max_size: 500,
146
- max_length: 2000,
146
+ max_length: 750,
147
147
  },
148
148
  verifications: {
149
149
  timeout: 60,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@axium/server",
3
- "version": "0.42.1",
3
+ "version": "0.43.1",
4
4
  "author": "James Prevett <axium@jamespre.dev>",
5
5
  "funding": {
6
6
  "type": "individual",
@@ -62,6 +62,7 @@
62
62
  "ioium": "^0.1.0",
63
63
  "logzen": "^0.7.0",
64
64
  "pg": "^8.14.1",
65
+ "sharp": "^0.34.5",
65
66
  "svelte": "^5.36.0"
66
67
  },
67
68
  "devDependencies": {
@@ -69,7 +70,6 @@
69
70
  "vite-plugin-mkcert": "^1.17.8"
70
71
  },
71
72
  "optionalDependencies": {
72
- "image-size": "^2.0.2",
73
73
  "ua-parser-js": "^2.0.9"
74
74
  }
75
75
  }
@@ -3,7 +3,7 @@
3
3
  import { ClipboardCopy, FormDialog, Icon, Logout, Popover, SessionList, UserPFP, ZodForm } from '@axium/client/components';
4
4
  import '@axium/client/styles/account';
5
5
  import { createPasskey, deletePasskey, deleteUser, sendVerificationEmail, updatePasskey, updateUser } from '@axium/client/user';
6
- import { preferenceLabels, Preferences } from '@axium/core/preferences';
6
+ import { Preferences } from '@axium/core/preferences';
7
7
  import type { PageProps } from './$types';
8
8
  import { toast, toastStatus } from '@axium/client/toast';
9
9
  import { contextMenu } from '@axium/client/attachments';
@@ -221,7 +221,6 @@
221
221
  bind:rootValue={user.preferences}
222
222
  idPrefix="preferences"
223
223
  schema={Preferences}
224
- labels={preferenceLabels}
225
224
  updateValue={(preferences: Preferences) => fetchAPI('PATCH', 'users/:id', { preferences }, user.id)}
226
225
  />
227
226
  </div>
@@ -3,7 +3,7 @@
3
3
  import { ClipboardCopy, FormDialog, Icon, SessionList, UserPFP, ZodForm, ZodInput } from '@axium/client/components';
4
4
  import { toast } from '@axium/client/toast';
5
5
  import '@axium/client/styles/account';
6
- import { preferenceLabels, Preferences, User } from '@axium/core';
6
+ import { Preferences, User } from '@axium/core';
7
7
  import { formatDateRange } from '@axium/core/format';
8
8
 
9
9
  const { data } = $props();
@@ -134,7 +134,6 @@
134
134
  <ZodForm
135
135
  bind:rootValue={user.preferences}
136
136
  schema={Preferences}
137
- labels={preferenceLabels}
138
137
  updateValue={(preferences: Preferences) => fetchAPI('PATCH', 'users/:id', { preferences }, user.id)}
139
138
  />
140
139
  </div>
@@ -158,8 +158,9 @@
158
158
  "minimum": 0
159
159
  },
160
160
  "max_length": {
161
- "type": "number",
162
- "minimum": 0
161
+ "type": "integer",
162
+ "minimum": 0,
163
+ "maximum": 9007199254740991
163
164
  }
164
165
  },
165
166
  "additionalProperties": {}