@svadmin/lite 0.3.12 → 0.3.14

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 +241 -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,65 @@
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 fieldFilters = resource.fields
26
+ .filter((field) => field.searchable)
27
+ .map((field) => ({
28
+ field: field.key,
29
+ operator: 'contains',
30
+ value: search,
31
+ }));
32
+ if (fieldFilters.length === 1)
33
+ return fieldFilters;
34
+ return fieldFilters.length > 1 ? [{ operator: 'or', value: fieldFilters }] : [];
35
+ }
3
36
  /**
4
37
  * Creates a SvelteKit `load` function that fetches a resource list
5
38
  * via the DataProvider. All state is driven by URL search params.
6
39
  */
7
40
  export function createListLoader(dp, resource) {
8
41
  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({
42
+ const { page, pageSize, sort, order, search } = listRequestState(url, resource);
43
+ const sorters = sort ? [{ field: sort, order }] : [];
44
+ const listResponse = await dp.getList({
24
45
  resource: resource.name,
25
46
  pagination: { current: page, pageSize },
26
47
  sorters,
27
- filters,
48
+ filters: listSearchFilters(resource, search),
28
49
  });
29
50
  return {
30
- records: result.data,
31
- total: result.total,
51
+ records: listResponse.data,
52
+ total: listResponse.total,
32
53
  page,
33
54
  pageSize,
34
- totalPages: Math.ceil(result.total / pageSize),
55
+ totalPages: Math.ceil(listResponse.total / pageSize),
35
56
  sort,
36
57
  order,
37
58
  search,
59
+ pagination: { page, perPage: pageSize },
60
+ currentSort: sort,
61
+ currentOrder: order,
62
+ currentSearch: search,
38
63
  resource,
39
64
  };
40
65
  };
@@ -58,6 +83,9 @@ export function createCrudActions(dp, resource) {
58
83
  const pk = resource.primaryKey ?? 'id';
59
84
  return {
60
85
  create: async ({ request }) => {
86
+ if (resource.canCreate === false) {
87
+ return { success: false, error: 'Create is disabled for this resource' };
88
+ }
61
89
  const formData = await request.formData();
62
90
  const submittedValues = formDataToObject(formData, resource.fields);
63
91
  const validation = validateFormVariables(resource, 'create', submittedValues);
@@ -68,15 +96,24 @@ export function createCrudActions(dp, resource) {
68
96
  const result = await dp.create({ resource: resource.name, variables });
69
97
  return { success: true, id: result.data[pk] };
70
98
  }
71
- catch (e) {
72
- if (isRedirect(e))
73
- throw e;
74
- return { success: false, error: e.message, values: variables };
99
+ catch (caughtError) {
100
+ if (isRedirect(caughtError))
101
+ throw caughtError;
102
+ return {
103
+ success: false,
104
+ error: 'Create failed',
105
+ values: formValuesForResponse(resource.fields, variables),
106
+ };
75
107
  }
76
108
  },
77
109
  update: async ({ request }) => {
110
+ if (resource.canEdit === false) {
111
+ return { success: false, error: 'Edit is disabled for this resource' };
112
+ }
78
113
  const formData = await request.formData();
79
- const id = formData.get('_id');
114
+ const id = readRecordId(formData.get('_id'));
115
+ if (!id)
116
+ return { success: false, error: 'Missing record id' };
80
117
  formData.delete('_id');
81
118
  const submittedValues = formDataToObject(formData, resource.fields);
82
119
  const validation = validateFormVariables(resource, 'edit', submittedValues);
@@ -87,26 +124,35 @@ export function createCrudActions(dp, resource) {
87
124
  await dp.update({ resource: resource.name, id, variables });
88
125
  return { success: true };
89
126
  }
90
- catch (e) {
91
- if (isRedirect(e))
92
- throw e;
93
- return { success: false, error: e.message, values: variables };
127
+ catch (caughtError) {
128
+ if (isRedirect(caughtError))
129
+ throw caughtError;
130
+ return {
131
+ success: false,
132
+ error: 'Update failed',
133
+ values: formValuesForResponse(resource.fields, variables),
134
+ };
94
135
  }
95
136
  },
96
137
  delete: async ({ request }) => {
138
+ if (resource.canDelete === false) {
139
+ return { success: false, error: 'Delete is disabled for this resource' };
140
+ }
97
141
  const formData = await request.formData();
98
- const id = formData.get('id');
99
- const redirectTo = formData.get('redirect');
142
+ const id = readRecordId(formData.get('id'));
143
+ if (!id)
144
+ return { success: false, error: 'Missing record id' };
145
+ const redirectTo = toSafeLocalRedirect(formData.get('redirect'));
100
146
  try {
101
147
  await dp.deleteOne({ resource: resource.name, id });
102
148
  if (redirectTo)
103
149
  throw redirect(303, redirectTo);
104
150
  return { success: true };
105
151
  }
106
- catch (e) {
107
- if (isRedirect(e))
108
- throw e;
109
- return { success: false, error: e.message };
152
+ catch (caughtError) {
153
+ if (isRedirect(caughtError))
154
+ throw caughtError;
155
+ return { success: false, error: 'Delete failed' };
110
156
  }
111
157
  },
112
158
  };
@@ -117,30 +163,32 @@ export function createCrudActions(dp, resource) {
117
163
  * and redirects unauthenticated users to a login page.
118
164
  */
119
165
  export function createAuthGuard(authProvider, loginPath = '/lite/login') {
166
+ const loginSegmentStart = loginPath.lastIndexOf('/');
167
+ const authBasePath = loginSegmentStart > 0
168
+ ? loginPath.slice(0, loginSegmentStart)
169
+ : '';
170
+ const allowedPublicPaths = new Set([
171
+ loginPath,
172
+ `${authBasePath}/register`,
173
+ `${authBasePath}/forgot-password`,
174
+ `${authBasePath}/update-password`,
175
+ ]);
120
176
  return async ({ event, resolve }) => {
121
- // Skip auth check for the login page itself
122
- if (event.url.pathname === loginPath) {
177
+ if (allowedPublicPaths.has(event.url.pathname)) {
123
178
  return resolve(event);
124
179
  }
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
180
  try {
134
181
  const check = await authProvider.check();
135
182
  if (!check.authenticated) {
136
183
  event.cookies.delete('svadmin-session', { path: '/' });
137
184
  return new Response(null, {
138
185
  status: 302,
139
- headers: { Location: loginPath },
186
+ headers: { Location: toSafeLocalRedirect(check.redirectTo) ?? loginPath },
140
187
  });
141
188
  }
142
189
  }
143
190
  catch {
191
+ // Authentication checks fail closed so provider errors never expose a protected page.
144
192
  event.cookies.delete('svadmin-session', { path: '/' });
145
193
  return new Response(null, {
146
194
  status: 302,
@@ -151,41 +199,98 @@ export function createAuthGuard(authProvider, loginPath = '/lite/login') {
151
199
  };
152
200
  }
153
201
  /**
154
- * Creates login/logout form actions using AuthProvider.
202
+ * Creates the form actions used by all exported Lite authentication pages.
155
203
  */
156
204
  export function createAuthActions(authProvider) {
205
+ async function readAuthParams(request) {
206
+ return Object.fromEntries(await request.formData());
207
+ }
208
+ function validatePasswordConfirmation(authParams) {
209
+ const { confirmPassword, ...providerParams } = authParams;
210
+ const password = authParams.password;
211
+ if (typeof password !== 'string'
212
+ || password.length === 0
213
+ || typeof confirmPassword !== 'string'
214
+ || confirmPassword.length === 0) {
215
+ return { valid: false, error: 'Password and confirmation are required' };
216
+ }
217
+ if (password !== confirmPassword) {
218
+ return { valid: false, error: 'Passwords do not match' };
219
+ }
220
+ return { valid: true, providerParams };
221
+ }
222
+ async function runAuthFormAction(providerMethod, authParams, unsupportedMessage, failureMessage) {
223
+ if (!providerMethod)
224
+ return { success: false, error: unsupportedMessage };
225
+ try {
226
+ const authResult = await providerMethod.call(authProvider, authParams);
227
+ if (!authResult.success) {
228
+ return { success: false, error: authResult.error?.message ?? 'Authentication action failed' };
229
+ }
230
+ if (authResult.redirectTo)
231
+ throw redirect(303, authResult.redirectTo);
232
+ return { success: true };
233
+ }
234
+ catch (caughtError) {
235
+ if (isRedirect(caughtError))
236
+ throw caughtError;
237
+ return { success: false, error: failureMessage };
238
+ }
239
+ }
157
240
  return {
158
- login: async ({ request, cookies }) => {
159
- const formData = await request.formData();
160
- const params = Object.fromEntries(formData);
241
+ login: async ({ request, cookies, url }) => {
242
+ const authParams = await readAuthParams(request);
161
243
  try {
162
- const result = await authProvider.login(params);
163
- if (result.success) {
164
- // Store a session indicator in a cookie
244
+ const loginResult = await authProvider.login(authParams);
245
+ if (loginResult.success) {
165
246
  cookies.set('svadmin-session', 'active', {
166
247
  path: '/',
167
248
  httpOnly: true,
168
249
  sameSite: 'lax',
250
+ secure: url.protocol === 'https:',
169
251
  maxAge: 60 * 60 * 24 * 7, // 7 days
170
252
  });
171
- throw redirect(303, result.redirectTo ?? '/lite');
253
+ throw redirect(303, loginResult.redirectTo ?? '/lite');
172
254
  }
173
- return { success: false, error: result.error?.message ?? 'Login failed' };
255
+ return { success: false, error: loginResult.error?.message ?? 'Login failed' };
174
256
  }
175
- catch (e) {
176
- if (isRedirect(e))
177
- throw e;
178
- return { success: false, error: e.message };
257
+ catch (caughtError) {
258
+ if (isRedirect(caughtError))
259
+ throw caughtError;
260
+ return { success: false, error: 'Login failed' };
179
261
  }
180
262
  },
181
263
  logout: async ({ cookies }) => {
264
+ let logoutResult;
182
265
  try {
183
- await authProvider.logout();
266
+ logoutResult = await authProvider.logout();
267
+ }
268
+ catch {
269
+ cookies.delete('svadmin-session', { path: '/' });
270
+ return { success: false, error: 'Logout failed' };
184
271
  }
185
- catch { /* ignore */ }
186
272
  cookies.delete('svadmin-session', { path: '/' });
273
+ if (!logoutResult.success) {
274
+ return { success: false, error: logoutResult.error?.message ?? 'Logout failed' };
275
+ }
276
+ if (logoutResult.redirectTo)
277
+ throw redirect(303, logoutResult.redirectTo);
187
278
  return { success: true };
188
279
  },
280
+ register: async ({ request }) => {
281
+ const confirmation = validatePasswordConfirmation(await readAuthParams(request));
282
+ if (!confirmation.valid)
283
+ return { success: false, error: confirmation.error };
284
+ return runAuthFormAction(authProvider.register, confirmation.providerParams, 'Registration is not supported by this AuthProvider', 'Registration failed');
285
+ },
286
+ forgot_password: async ({ request }) => runAuthFormAction(authProvider.forgotPassword, await readAuthParams(request), 'Password recovery is not supported by this AuthProvider', 'Password recovery failed'),
287
+ update_password: async ({ request }) => {
288
+ const confirmation = validatePasswordConfirmation(await readAuthParams(request));
289
+ if (!confirmation.valid)
290
+ return { success: false, error: confirmation.error };
291
+ return runAuthFormAction(authProvider.updatePassword, confirmation.providerParams, 'Password updates are not supported by this AuthProvider', 'Password update failed');
292
+ },
293
+ update_profile: async ({ request }) => runAuthFormAction((authProvider.updateProfile ?? authProvider.updateIdentity), await readAuthParams(request), 'Profile updates are not supported by this AuthProvider', 'Profile update failed'),
189
294
  };
190
295
  }
191
296
  // ─── UA Detection ─────────────────────────────────────────────
@@ -201,17 +306,61 @@ export function isLegacyBrowser(userAgent) {
201
306
  * Consumers remain responsible for their own transpilation and browser support.
202
307
  */
203
308
  export function createLegacyRedirectHook(litePrefix = '/lite') {
309
+ const prefixSegments = litePrefix.split('/').filter(Boolean);
310
+ const normalizedPrefix = prefixSegments.length > 0 ? `/${prefixSegments.join('/')}` : '/';
204
311
  return async ({ event, resolve }) => {
205
312
  const ua = event.request.headers.get('user-agent') ?? '';
206
- if (isLegacyBrowser(ua) && !event.url.pathname.startsWith(litePrefix)) {
313
+ const acceptsHtml = event.request.headers.get('accept')?.includes('text/html') === true;
314
+ const isDocumentRequest = (event.request.method === 'GET' || event.request.method === 'HEAD')
315
+ && acceptsHtml;
316
+ const isWithinLite = normalizedPrefix === '/'
317
+ || event.url.pathname === normalizedPrefix
318
+ || event.url.pathname.startsWith(`${normalizedPrefix}/`);
319
+ if (isLegacyBrowser(ua) && isDocumentRequest && !isWithinLite) {
320
+ const targetPath = normalizedPrefix === '/'
321
+ ? event.url.pathname
322
+ : `${normalizedPrefix}${event.url.pathname}`;
207
323
  return new Response(null, {
208
324
  status: 302,
209
- headers: { Location: `${litePrefix}${event.url.pathname}` },
325
+ headers: { Location: `${targetPath}${event.url.search}` },
210
326
  });
211
327
  }
212
328
  return resolve(event);
213
329
  };
214
330
  }
331
+ // ─── Utilities ────────────────────────────────────────────────
332
+ function readRecordId(submittedId) {
333
+ if (typeof submittedId !== 'string' || submittedId.trim().length === 0)
334
+ return undefined;
335
+ return submittedId.trim();
336
+ }
337
+ function containsControlCharacter(untrustedRedirect) {
338
+ return Array.from(untrustedRedirect).some((character) => {
339
+ const codePoint = character.codePointAt(0) ?? 0;
340
+ return codePoint <= 31 || (codePoint >= 127 && codePoint <= 159);
341
+ });
342
+ }
343
+ function toSafeLocalRedirect(submittedRedirect) {
344
+ if (typeof submittedRedirect !== 'string'
345
+ || !submittedRedirect.startsWith('/')
346
+ || submittedRedirect.startsWith('//')) {
347
+ return undefined;
348
+ }
349
+ if (submittedRedirect.includes('\\') || containsControlCharacter(submittedRedirect)) {
350
+ return undefined;
351
+ }
352
+ const base = new URL('https://svadmin.local');
353
+ try {
354
+ const target = new URL(submittedRedirect, base);
355
+ if (target.origin !== base.origin)
356
+ return undefined;
357
+ return `${target.pathname}${target.search}${target.hash}`;
358
+ }
359
+ catch {
360
+ // Malformed untrusted redirect input is intentionally treated as no redirect.
361
+ return undefined;
362
+ }
363
+ }
215
364
  function formatValidationErrors(issues) {
216
365
  const errors = {};
217
366
  for (const issue of issues) {
@@ -233,7 +382,7 @@ function validateFormVariables(resource, mode, values) {
233
382
  failure: {
234
383
  success: false,
235
384
  error: 'Validation failed',
236
- values,
385
+ values: formValuesForResponse(resource.fields, values),
237
386
  errors: formatValidationErrors(result.error.issues),
238
387
  },
239
388
  };
@@ -241,6 +390,28 @@ function validateFormVariables(resource, mode, values) {
241
390
  function isNativeFile(value) {
242
391
  return typeof File !== 'undefined' && value instanceof File;
243
392
  }
393
+ function formValuesForResponse(fields, values) {
394
+ const responseValues = {};
395
+ for (const field of fields) {
396
+ if (field.type === 'password' || !(field.key in values))
397
+ continue;
398
+ const value = values[field.key];
399
+ if (field.type === 'array' && Array.isArray(value)) {
400
+ responseValues[field.key] = value.map((row) => {
401
+ if (typeof row !== 'object' || row === null || Array.isArray(row))
402
+ return row;
403
+ return formValuesForResponse(field.subFields ?? [], row);
404
+ });
405
+ continue;
406
+ }
407
+ if (isNativeFile(value))
408
+ continue;
409
+ responseValues[field.key] = Array.isArray(value)
410
+ ? value.filter((entry) => !isNativeFile(entry))
411
+ : value;
412
+ }
413
+ return responseValues;
414
+ }
244
415
  function isNonEmptyNativeFile(value) {
245
416
  return isNativeFile(value) && value.size > 0 && value.name !== '';
246
417
  }
@@ -290,7 +461,7 @@ function formDataToObject(formData, fields) {
290
461
  }
291
462
  break;
292
463
  case 'boolean':
293
- obj[field.key] = strRaw === 'on' || strRaw === 'true' || strRaw === '1';
464
+ obj[field.key] = parseExplicitBoolean(strRaw) ?? strRaw;
294
465
  break;
295
466
  case 'tags':
296
467
  obj[field.key] = strRaw ? strRaw.split(',').map(s => s.trim()).filter(Boolean) : [];
@@ -336,9 +507,12 @@ function parseArrayField(formData, field) {
336
507
  for (const subField of field.subFields ?? []) {
337
508
  const rawValues = entries.get(subField.key) ?? [];
338
509
  if (subField.type === 'boolean') {
339
- const value = isChecked(rawValues);
510
+ const rawValue = rawValues.at(-1);
511
+ const value = rawValue === undefined
512
+ ? false
513
+ : parseExplicitBoolean(rawValue) ?? rawValue;
340
514
  item[subField.key] = value;
341
- hasMeaningfulValue ||= value;
515
+ hasMeaningfulValue ||= value !== false;
342
516
  continue;
343
517
  }
344
518
  if (rawValues.length === 0)
@@ -406,7 +580,7 @@ function coerceFieldValues(field, rawValues) {
406
580
  return undefined;
407
581
  return Number.isNaN(Number(raw)) ? raw : Number(raw);
408
582
  case 'boolean':
409
- return raw === 'on' || raw === 'true' || raw === '1';
583
+ return parseExplicitBoolean(raw) ?? raw;
410
584
  case 'tags':
411
585
  return raw ? raw.split(',').map((value) => value.trim()).filter(Boolean) : [];
412
586
  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.14",
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
  }