@svadmin/lite 0.3.12 → 0.3.13

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.
Files changed (55) hide show
  1. package/README.md +30 -5
  2. package/dist/components/LiteArrayItem.svelte +31 -10
  3. package/dist/components/LiteAuditLog.svelte +8 -17
  4. package/dist/components/LiteAuditLog.svelte.d.ts +5 -13
  5. package/dist/components/LiteForm.svelte +37 -7
  6. package/dist/components/LiteForm.svelte.d.ts +1 -1
  7. package/dist/components/LiteLayout.svelte +12 -7
  8. package/dist/components/LiteLayout.svelte.d.ts +2 -0
  9. package/dist/components/LitePermissionMatrix.svelte +48 -15
  10. package/dist/components/LitePermissionMatrix.svelte.d.ts +10 -9
  11. package/dist/components/LiteShow.svelte +3 -2
  12. package/dist/components/LiteShowField.svelte +5 -3
  13. package/dist/components/LiteTable.svelte +19 -9
  14. package/dist/components/LiteTable.svelte.d.ts +1 -0
  15. package/dist/components/advanced/LiteVirtualTable.svelte +5 -2
  16. package/dist/components/advanced/LiteVirtualTable.svelte.d.ts +1 -0
  17. package/dist/components/buttons/LiteCloneButton.svelte +2 -2
  18. package/dist/components/buttons/LiteCreateButton.svelte +2 -2
  19. package/dist/components/buttons/LiteDeleteButton.svelte +2 -2
  20. package/dist/components/buttons/LiteEditButton.svelte +2 -2
  21. package/dist/components/buttons/LiteExportButton.svelte +2 -2
  22. package/dist/components/buttons/LiteImportButton.svelte +2 -2
  23. package/dist/components/buttons/LiteListButton.svelte +2 -2
  24. package/dist/components/buttons/LiteRefreshButton.svelte +7 -11
  25. package/dist/components/buttons/LiteSaveButton.svelte +2 -2
  26. package/dist/components/buttons/LiteShowButton.svelte +2 -2
  27. package/dist/components/fields/LiteBooleanField.svelte +9 -8
  28. package/dist/components/fields/LiteDateField.svelte +2 -2
  29. package/dist/components/fields/LiteEmailField.svelte +1 -2
  30. package/dist/components/fields/LiteImageField.svelte +20 -11
  31. package/dist/components/fields/LiteNumberField.svelte +1 -2
  32. package/dist/components/fields/LiteRelationField.svelte +15 -7
  33. package/dist/components/fields/LiteSelectField.svelte +2 -1
  34. package/dist/components/fields/LiteTextField.svelte +1 -2
  35. package/dist/components/fields/LiteUrlField.svelte +1 -5
  36. package/dist/components/layout/LiteCatchAllNavigate.svelte +3 -9
  37. package/dist/components/layout/LiteNavigateToResource.svelte +5 -16
  38. package/dist/components/layout/LiteSidebar.svelte +9 -5
  39. package/dist/components/layout/LiteSidebar.svelte.d.ts +1 -0
  40. package/dist/components/pages/LiteEditPage.svelte +6 -4
  41. package/dist/components/pages/LiteListPage.svelte +14 -6
  42. package/dist/components/pages/LiteListPage.svelte.d.ts +1 -0
  43. package/dist/components/pages/LiteShowPage.svelte +6 -4
  44. package/dist/components/widgets/LiteAnomalyBadge.svelte +5 -1
  45. package/dist/index.js +1 -1
  46. package/dist/lite.css +2 -1
  47. package/dist/menu-visibility.d.ts +4 -0
  48. package/dist/menu-visibility.js +34 -0
  49. package/dist/schema-generator.d.ts +1 -1
  50. package/dist/schema-generator.js +112 -51
  51. package/dist/server-adapter.d.ts +53 -4
  52. package/dist/server-adapter.js +235 -67
  53. package/dist/value-normalization.d.ts +2 -0
  54. package/dist/value-normalization.js +21 -0
  55. package/package.json +6 -14
@@ -1,40 +1,59 @@
1
1
  import { redirect, isRedirect } from '@sveltejs/kit';
2
2
  import { resourceToZodSchema } from './schema-generator';
3
+ import { parseExplicitBoolean } from './value-normalization';
4
+ function listRequestState(url, resource) {
5
+ const requestedPage = Number(url.searchParams.get('page'));
6
+ const page = Number.isSafeInteger(requestedPage) && requestedPage > 0 ? requestedPage : 1;
7
+ const configuredPageSize = resource.pageSize ?? 10;
8
+ const pageSize = Number.isSafeInteger(configuredPageSize) && configuredPageSize > 0
9
+ ? configuredPageSize
10
+ : 10;
11
+ const requestedSort = url.searchParams.get('sort') ?? undefined;
12
+ const sortableField = requestedSort
13
+ ? resource.fields.find((field) => field.key === requestedSort && field.sortable !== false)
14
+ : undefined;
15
+ const sort = sortableField?.key ?? resource.defaultSort?.field;
16
+ const requestedOrder = url.searchParams.get('order');
17
+ const order = sortableField
18
+ ? requestedOrder === 'asc' || requestedOrder === 'desc' ? requestedOrder : 'asc'
19
+ : resource.defaultSort?.order ?? 'asc';
20
+ return { page, pageSize, sort, order, search: url.searchParams.get('q') ?? undefined };
21
+ }
22
+ function listSearchFilters(resource, search) {
23
+ if (!search)
24
+ return [];
25
+ const searchableField = resource.fields.find((field) => field.searchable);
26
+ return searchableField
27
+ ? [{ field: searchableField.key, operator: 'contains', value: search }]
28
+ : [];
29
+ }
3
30
  /**
4
31
  * Creates a SvelteKit `load` function that fetches a resource list
5
32
  * via the DataProvider. All state is driven by URL search params.
6
33
  */
7
34
  export function createListLoader(dp, resource) {
8
35
  return async ({ url }) => {
9
- const page = Math.max(1, Number(url.searchParams.get('page')) || 1);
10
- const pageSize = resource.pageSize ?? 20;
11
- const sort = url.searchParams.get('sort') ?? undefined;
12
- const order = url.searchParams.get('order') ?? 'asc';
13
- const search = url.searchParams.get('q') ?? undefined;
14
- const sorters = sort ? [{ field: sort, order }] :
15
- resource.defaultSort ? [resource.defaultSort] : [];
16
- const filters = [];
17
- if (search) {
18
- const searchable = resource.fields.find((f) => f.searchable);
19
- if (searchable) {
20
- filters.push({ field: searchable.key, operator: 'contains', value: search });
21
- }
22
- }
23
- const result = await dp.getList({
36
+ const { page, pageSize, sort, order, search } = listRequestState(url, resource);
37
+ const sorters = sort ? [{ field: sort, order }] : [];
38
+ const listResponse = await dp.getList({
24
39
  resource: resource.name,
25
40
  pagination: { current: page, pageSize },
26
41
  sorters,
27
- filters,
42
+ filters: listSearchFilters(resource, search),
28
43
  });
29
44
  return {
30
- records: result.data,
31
- total: result.total,
45
+ records: listResponse.data,
46
+ total: listResponse.total,
32
47
  page,
33
48
  pageSize,
34
- totalPages: Math.ceil(result.total / pageSize),
49
+ totalPages: Math.ceil(listResponse.total / pageSize),
35
50
  sort,
36
51
  order,
37
52
  search,
53
+ pagination: { page, perPage: pageSize },
54
+ currentSort: sort,
55
+ currentOrder: order,
56
+ currentSearch: search,
38
57
  resource,
39
58
  };
40
59
  };
@@ -58,6 +77,9 @@ export function createCrudActions(dp, resource) {
58
77
  const pk = resource.primaryKey ?? 'id';
59
78
  return {
60
79
  create: async ({ request }) => {
80
+ if (resource.canCreate === false) {
81
+ return { success: false, error: 'Create is disabled for this resource' };
82
+ }
61
83
  const formData = await request.formData();
62
84
  const submittedValues = formDataToObject(formData, resource.fields);
63
85
  const validation = validateFormVariables(resource, 'create', submittedValues);
@@ -68,15 +90,24 @@ export function createCrudActions(dp, resource) {
68
90
  const result = await dp.create({ resource: resource.name, variables });
69
91
  return { success: true, id: result.data[pk] };
70
92
  }
71
- catch (e) {
72
- if (isRedirect(e))
73
- throw e;
74
- return { success: false, error: e.message, values: variables };
93
+ catch (caughtError) {
94
+ if (isRedirect(caughtError))
95
+ throw caughtError;
96
+ return {
97
+ success: false,
98
+ error: 'Create failed',
99
+ values: formValuesForResponse(resource.fields, variables),
100
+ };
75
101
  }
76
102
  },
77
103
  update: async ({ request }) => {
104
+ if (resource.canEdit === false) {
105
+ return { success: false, error: 'Edit is disabled for this resource' };
106
+ }
78
107
  const formData = await request.formData();
79
- const id = formData.get('_id');
108
+ const id = readRecordId(formData.get('_id'));
109
+ if (!id)
110
+ return { success: false, error: 'Missing record id' };
80
111
  formData.delete('_id');
81
112
  const submittedValues = formDataToObject(formData, resource.fields);
82
113
  const validation = validateFormVariables(resource, 'edit', submittedValues);
@@ -87,26 +118,35 @@ export function createCrudActions(dp, resource) {
87
118
  await dp.update({ resource: resource.name, id, variables });
88
119
  return { success: true };
89
120
  }
90
- catch (e) {
91
- if (isRedirect(e))
92
- throw e;
93
- return { success: false, error: e.message, values: variables };
121
+ catch (caughtError) {
122
+ if (isRedirect(caughtError))
123
+ throw caughtError;
124
+ return {
125
+ success: false,
126
+ error: 'Update failed',
127
+ values: formValuesForResponse(resource.fields, variables),
128
+ };
94
129
  }
95
130
  },
96
131
  delete: async ({ request }) => {
132
+ if (resource.canDelete === false) {
133
+ return { success: false, error: 'Delete is disabled for this resource' };
134
+ }
97
135
  const formData = await request.formData();
98
- const id = formData.get('id');
99
- const redirectTo = formData.get('redirect');
136
+ const id = readRecordId(formData.get('id'));
137
+ if (!id)
138
+ return { success: false, error: 'Missing record id' };
139
+ const redirectTo = toSafeLocalRedirect(formData.get('redirect'));
100
140
  try {
101
141
  await dp.deleteOne({ resource: resource.name, id });
102
142
  if (redirectTo)
103
143
  throw redirect(303, redirectTo);
104
144
  return { success: true };
105
145
  }
106
- catch (e) {
107
- if (isRedirect(e))
108
- throw e;
109
- return { success: false, error: e.message };
146
+ catch (caughtError) {
147
+ if (isRedirect(caughtError))
148
+ throw caughtError;
149
+ return { success: false, error: 'Delete failed' };
110
150
  }
111
151
  },
112
152
  };
@@ -117,30 +157,32 @@ export function createCrudActions(dp, resource) {
117
157
  * and redirects unauthenticated users to a login page.
118
158
  */
119
159
  export function createAuthGuard(authProvider, loginPath = '/lite/login') {
160
+ const loginSegmentStart = loginPath.lastIndexOf('/');
161
+ const authBasePath = loginSegmentStart > 0
162
+ ? loginPath.slice(0, loginSegmentStart)
163
+ : '';
164
+ const allowedPublicPaths = new Set([
165
+ loginPath,
166
+ `${authBasePath}/register`,
167
+ `${authBasePath}/forgot-password`,
168
+ `${authBasePath}/update-password`,
169
+ ]);
120
170
  return async ({ event, resolve }) => {
121
- // Skip auth check for the login page itself
122
- if (event.url.pathname === loginPath) {
171
+ if (allowedPublicPaths.has(event.url.pathname)) {
123
172
  return resolve(event);
124
173
  }
125
- // Local Lite Session Verify
126
- const session = event.cookies.get('svadmin-session');
127
- if (!session) {
128
- return new Response(null, {
129
- status: 302,
130
- headers: { Location: loginPath },
131
- });
132
- }
133
174
  try {
134
175
  const check = await authProvider.check();
135
176
  if (!check.authenticated) {
136
177
  event.cookies.delete('svadmin-session', { path: '/' });
137
178
  return new Response(null, {
138
179
  status: 302,
139
- headers: { Location: loginPath },
180
+ headers: { Location: toSafeLocalRedirect(check.redirectTo) ?? loginPath },
140
181
  });
141
182
  }
142
183
  }
143
184
  catch {
185
+ // Authentication checks fail closed so provider errors never expose a protected page.
144
186
  event.cookies.delete('svadmin-session', { path: '/' });
145
187
  return new Response(null, {
146
188
  status: 302,
@@ -151,41 +193,98 @@ export function createAuthGuard(authProvider, loginPath = '/lite/login') {
151
193
  };
152
194
  }
153
195
  /**
154
- * Creates login/logout form actions using AuthProvider.
196
+ * Creates the form actions used by all exported Lite authentication pages.
155
197
  */
156
198
  export function createAuthActions(authProvider) {
199
+ async function readAuthParams(request) {
200
+ return Object.fromEntries(await request.formData());
201
+ }
202
+ function validatePasswordConfirmation(authParams) {
203
+ const { confirmPassword, ...providerParams } = authParams;
204
+ const password = authParams.password;
205
+ if (typeof password !== 'string'
206
+ || password.length === 0
207
+ || typeof confirmPassword !== 'string'
208
+ || confirmPassword.length === 0) {
209
+ return { valid: false, error: 'Password and confirmation are required' };
210
+ }
211
+ if (password !== confirmPassword) {
212
+ return { valid: false, error: 'Passwords do not match' };
213
+ }
214
+ return { valid: true, providerParams };
215
+ }
216
+ async function runAuthFormAction(providerMethod, authParams, unsupportedMessage, failureMessage) {
217
+ if (!providerMethod)
218
+ return { success: false, error: unsupportedMessage };
219
+ try {
220
+ const authResult = await providerMethod.call(authProvider, authParams);
221
+ if (!authResult.success) {
222
+ return { success: false, error: authResult.error?.message ?? 'Authentication action failed' };
223
+ }
224
+ if (authResult.redirectTo)
225
+ throw redirect(303, authResult.redirectTo);
226
+ return { success: true };
227
+ }
228
+ catch (caughtError) {
229
+ if (isRedirect(caughtError))
230
+ throw caughtError;
231
+ return { success: false, error: failureMessage };
232
+ }
233
+ }
157
234
  return {
158
- login: async ({ request, cookies }) => {
159
- const formData = await request.formData();
160
- const params = Object.fromEntries(formData);
235
+ login: async ({ request, cookies, url }) => {
236
+ const authParams = await readAuthParams(request);
161
237
  try {
162
- const result = await authProvider.login(params);
163
- if (result.success) {
164
- // Store a session indicator in a cookie
238
+ const loginResult = await authProvider.login(authParams);
239
+ if (loginResult.success) {
165
240
  cookies.set('svadmin-session', 'active', {
166
241
  path: '/',
167
242
  httpOnly: true,
168
243
  sameSite: 'lax',
244
+ secure: url.protocol === 'https:',
169
245
  maxAge: 60 * 60 * 24 * 7, // 7 days
170
246
  });
171
- throw redirect(303, result.redirectTo ?? '/lite');
247
+ throw redirect(303, loginResult.redirectTo ?? '/lite');
172
248
  }
173
- return { success: false, error: result.error?.message ?? 'Login failed' };
249
+ return { success: false, error: loginResult.error?.message ?? 'Login failed' };
174
250
  }
175
- catch (e) {
176
- if (isRedirect(e))
177
- throw e;
178
- return { success: false, error: e.message };
251
+ catch (caughtError) {
252
+ if (isRedirect(caughtError))
253
+ throw caughtError;
254
+ return { success: false, error: 'Login failed' };
179
255
  }
180
256
  },
181
257
  logout: async ({ cookies }) => {
258
+ let logoutResult;
182
259
  try {
183
- await authProvider.logout();
260
+ logoutResult = await authProvider.logout();
261
+ }
262
+ catch {
263
+ cookies.delete('svadmin-session', { path: '/' });
264
+ return { success: false, error: 'Logout failed' };
184
265
  }
185
- catch { /* ignore */ }
186
266
  cookies.delete('svadmin-session', { path: '/' });
267
+ if (!logoutResult.success) {
268
+ return { success: false, error: logoutResult.error?.message ?? 'Logout failed' };
269
+ }
270
+ if (logoutResult.redirectTo)
271
+ throw redirect(303, logoutResult.redirectTo);
187
272
  return { success: true };
188
273
  },
274
+ register: async ({ request }) => {
275
+ const confirmation = validatePasswordConfirmation(await readAuthParams(request));
276
+ if (!confirmation.valid)
277
+ return { success: false, error: confirmation.error };
278
+ return runAuthFormAction(authProvider.register, confirmation.providerParams, 'Registration is not supported by this AuthProvider', 'Registration failed');
279
+ },
280
+ forgot_password: async ({ request }) => runAuthFormAction(authProvider.forgotPassword, await readAuthParams(request), 'Password recovery is not supported by this AuthProvider', 'Password recovery failed'),
281
+ update_password: async ({ request }) => {
282
+ const confirmation = validatePasswordConfirmation(await readAuthParams(request));
283
+ if (!confirmation.valid)
284
+ return { success: false, error: confirmation.error };
285
+ return runAuthFormAction(authProvider.updatePassword, confirmation.providerParams, 'Password updates are not supported by this AuthProvider', 'Password update failed');
286
+ },
287
+ update_profile: async ({ request }) => runAuthFormAction((authProvider.updateProfile ?? authProvider.updateIdentity), await readAuthParams(request), 'Profile updates are not supported by this AuthProvider', 'Profile update failed'),
189
288
  };
190
289
  }
191
290
  // ─── UA Detection ─────────────────────────────────────────────
@@ -201,17 +300,61 @@ export function isLegacyBrowser(userAgent) {
201
300
  * Consumers remain responsible for their own transpilation and browser support.
202
301
  */
203
302
  export function createLegacyRedirectHook(litePrefix = '/lite') {
303
+ const prefixSegments = litePrefix.split('/').filter(Boolean);
304
+ const normalizedPrefix = prefixSegments.length > 0 ? `/${prefixSegments.join('/')}` : '/';
204
305
  return async ({ event, resolve }) => {
205
306
  const ua = event.request.headers.get('user-agent') ?? '';
206
- if (isLegacyBrowser(ua) && !event.url.pathname.startsWith(litePrefix)) {
307
+ const acceptsHtml = event.request.headers.get('accept')?.includes('text/html') === true;
308
+ const isDocumentRequest = (event.request.method === 'GET' || event.request.method === 'HEAD')
309
+ && acceptsHtml;
310
+ const isWithinLite = normalizedPrefix === '/'
311
+ || event.url.pathname === normalizedPrefix
312
+ || event.url.pathname.startsWith(`${normalizedPrefix}/`);
313
+ if (isLegacyBrowser(ua) && isDocumentRequest && !isWithinLite) {
314
+ const targetPath = normalizedPrefix === '/'
315
+ ? event.url.pathname
316
+ : `${normalizedPrefix}${event.url.pathname}`;
207
317
  return new Response(null, {
208
318
  status: 302,
209
- headers: { Location: `${litePrefix}${event.url.pathname}` },
319
+ headers: { Location: `${targetPath}${event.url.search}` },
210
320
  });
211
321
  }
212
322
  return resolve(event);
213
323
  };
214
324
  }
325
+ // ─── Utilities ────────────────────────────────────────────────
326
+ function readRecordId(submittedId) {
327
+ if (typeof submittedId !== 'string' || submittedId.trim().length === 0)
328
+ return undefined;
329
+ return submittedId.trim();
330
+ }
331
+ function containsControlCharacter(untrustedRedirect) {
332
+ return Array.from(untrustedRedirect).some((character) => {
333
+ const codePoint = character.codePointAt(0) ?? 0;
334
+ return codePoint <= 31 || (codePoint >= 127 && codePoint <= 159);
335
+ });
336
+ }
337
+ function toSafeLocalRedirect(submittedRedirect) {
338
+ if (typeof submittedRedirect !== 'string'
339
+ || !submittedRedirect.startsWith('/')
340
+ || submittedRedirect.startsWith('//')) {
341
+ return undefined;
342
+ }
343
+ if (submittedRedirect.includes('\\') || containsControlCharacter(submittedRedirect)) {
344
+ return undefined;
345
+ }
346
+ const base = new URL('https://svadmin.local');
347
+ try {
348
+ const target = new URL(submittedRedirect, base);
349
+ if (target.origin !== base.origin)
350
+ return undefined;
351
+ return `${target.pathname}${target.search}${target.hash}`;
352
+ }
353
+ catch {
354
+ // Malformed untrusted redirect input is intentionally treated as no redirect.
355
+ return undefined;
356
+ }
357
+ }
215
358
  function formatValidationErrors(issues) {
216
359
  const errors = {};
217
360
  for (const issue of issues) {
@@ -233,7 +376,7 @@ function validateFormVariables(resource, mode, values) {
233
376
  failure: {
234
377
  success: false,
235
378
  error: 'Validation failed',
236
- values,
379
+ values: formValuesForResponse(resource.fields, values),
237
380
  errors: formatValidationErrors(result.error.issues),
238
381
  },
239
382
  };
@@ -241,6 +384,28 @@ function validateFormVariables(resource, mode, values) {
241
384
  function isNativeFile(value) {
242
385
  return typeof File !== 'undefined' && value instanceof File;
243
386
  }
387
+ function formValuesForResponse(fields, values) {
388
+ const responseValues = {};
389
+ for (const field of fields) {
390
+ if (field.type === 'password' || !(field.key in values))
391
+ continue;
392
+ const value = values[field.key];
393
+ if (field.type === 'array' && Array.isArray(value)) {
394
+ responseValues[field.key] = value.map((row) => {
395
+ if (typeof row !== 'object' || row === null || Array.isArray(row))
396
+ return row;
397
+ return formValuesForResponse(field.subFields ?? [], row);
398
+ });
399
+ continue;
400
+ }
401
+ if (isNativeFile(value))
402
+ continue;
403
+ responseValues[field.key] = Array.isArray(value)
404
+ ? value.filter((entry) => !isNativeFile(entry))
405
+ : value;
406
+ }
407
+ return responseValues;
408
+ }
244
409
  function isNonEmptyNativeFile(value) {
245
410
  return isNativeFile(value) && value.size > 0 && value.name !== '';
246
411
  }
@@ -290,7 +455,7 @@ function formDataToObject(formData, fields) {
290
455
  }
291
456
  break;
292
457
  case 'boolean':
293
- obj[field.key] = strRaw === 'on' || strRaw === 'true' || strRaw === '1';
458
+ obj[field.key] = parseExplicitBoolean(strRaw) ?? strRaw;
294
459
  break;
295
460
  case 'tags':
296
461
  obj[field.key] = strRaw ? strRaw.split(',').map(s => s.trim()).filter(Boolean) : [];
@@ -336,9 +501,12 @@ function parseArrayField(formData, field) {
336
501
  for (const subField of field.subFields ?? []) {
337
502
  const rawValues = entries.get(subField.key) ?? [];
338
503
  if (subField.type === 'boolean') {
339
- const value = isChecked(rawValues);
504
+ const rawValue = rawValues.at(-1);
505
+ const value = rawValue === undefined
506
+ ? false
507
+ : parseExplicitBoolean(rawValue) ?? rawValue;
340
508
  item[subField.key] = value;
341
- hasMeaningfulValue ||= value;
509
+ hasMeaningfulValue ||= value !== false;
342
510
  continue;
343
511
  }
344
512
  if (rawValues.length === 0)
@@ -406,7 +574,7 @@ function coerceFieldValues(field, rawValues) {
406
574
  return undefined;
407
575
  return Number.isNaN(Number(raw)) ? raw : Number(raw);
408
576
  case 'boolean':
409
- return raw === 'on' || raw === 'true' || raw === '1';
577
+ return parseExplicitBoolean(raw) ?? raw;
410
578
  case 'tags':
411
579
  return raw ? raw.split(',').map((value) => value.trim()).filter(Boolean) : [];
412
580
  case 'json':
@@ -0,0 +1,2 @@
1
+ export declare function parseExplicitBoolean(rawBoolean: unknown): boolean | undefined;
2
+ export declare function isExplicitBooleanTrue(rawBoolean: unknown): boolean;
@@ -0,0 +1,21 @@
1
+ const TRUE_BOOLEAN_STRINGS = new Set(['1', 'true', 'on']);
2
+ const FALSE_BOOLEAN_STRINGS = new Set(['0', 'false', 'off']);
3
+ export function parseExplicitBoolean(rawBoolean) {
4
+ if (typeof rawBoolean === 'boolean')
5
+ return rawBoolean;
6
+ if (rawBoolean === 1)
7
+ return true;
8
+ if (rawBoolean === 0)
9
+ return false;
10
+ if (typeof rawBoolean !== 'string')
11
+ return undefined;
12
+ const normalizedBoolean = rawBoolean.trim().toLowerCase();
13
+ if (TRUE_BOOLEAN_STRINGS.has(normalizedBoolean))
14
+ return true;
15
+ if (FALSE_BOOLEAN_STRINGS.has(normalizedBoolean))
16
+ return false;
17
+ return undefined;
18
+ }
19
+ export function isExplicitBooleanTrue(rawBoolean) {
20
+ return parseExplicitBoolean(rawBoolean) === true;
21
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@svadmin/lite",
3
- "version": "0.3.12",
3
+ "version": "0.3.13",
4
4
  "description": "SSR-first lightweight admin UI for @svadmin with optional progressive enhancement",
5
5
  "type": "module",
6
6
  "sideEffects": [
@@ -54,10 +54,10 @@
54
54
  },
55
55
  "dependencies": {
56
56
  "@lucide/svelte": "^1.30.0",
57
- "sveltekit-superforms": "^2.30.2",
58
57
  "zod": "^4.4.3"
59
58
  },
60
59
  "devDependencies": {
60
+ "@types/bun": "^1.3.14",
61
61
  "@happy-dom/global-registrator": "^20.11.2",
62
62
  "@sveltejs/vite-plugin-svelte": "^7.3.0",
63
63
  "@testing-library/dom": "^10.4.1",
@@ -67,7 +67,10 @@
67
67
  },
68
68
  "scripts": {
69
69
  "build": "node ../../node_modules/@sveltejs/package/svelte-package.js -i src -o dist && bun ../../scripts/copy-package-assets.ts static/enhance.js dist/enhance.js",
70
- "test": "vitest run --config ./vitest.config.ts"
70
+ "test": "bun run test:components && bun run test:server && bun run test:security",
71
+ "test:components": "vitest run --config ./vitest.config.ts",
72
+ "test:server": "bun test ./src/schema-generator.test.ts ./src/server-adapter.test.ts",
73
+ "test:security": "vitest run --config ./src/security.test.config.ts"
71
74
  },
72
75
  "license": "MIT",
73
76
  "publishConfig": {
@@ -79,16 +82,5 @@
79
82
  "type": "git",
80
83
  "url": "https://github.com/zuohuadong/svadmin.git",
81
84
  "directory": "packages/lite"
82
- },
83
- "peerDependenciesMeta": {
84
- "svelte": {
85
- "optional": true
86
- },
87
- "@svadmin/core": {
88
- "optional": true
89
- },
90
- "@sveltejs/kit": {
91
- "optional": true
92
- }
93
85
  }
94
86
  }