@svadmin/core 0.58.0 → 0.60.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.
Files changed (47) hide show
  1. package/README.md +1 -1
  2. package/package.json +2 -2
  3. package/src/access-control-contract.ts +1 -1
  4. package/src/admin-tool.ts +11 -5
  5. package/src/approval-contract.ts +1 -1
  6. package/src/audit-contract.ts +3 -3
  7. package/src/audit.ts +1 -1
  8. package/src/auth-mutation-contract.ts +1 -1
  9. package/src/auth-query-contract.ts +1 -1
  10. package/src/command-contract.ts +2 -3
  11. package/src/contract-form.svelte.ts +1 -1
  12. package/src/create-contract.ts +1 -1
  13. package/src/create-many-contract.ts +1 -1
  14. package/src/dashboard-contract.ts +1 -1
  15. package/src/delete-contract.ts +1 -1
  16. package/src/delete-many-contract.ts +1 -1
  17. package/src/export-format.ts +1 -1
  18. package/src/feature-gate-contract.ts +1 -1
  19. package/src/helpers-pure.ts +13 -9
  20. package/src/http.ts +2 -2
  21. package/src/import-contract.ts +1 -1
  22. package/src/index.ts +1 -1
  23. package/src/inferencer.ts +83 -2
  24. package/src/infinite-query.svelte.ts +1 -1
  25. package/src/invalidation-contract.ts +1 -1
  26. package/src/live-sse.ts +1 -1
  27. package/src/live-subscription.svelte.ts +1 -1
  28. package/src/live-transport.ts +1 -1
  29. package/src/permission-catalog.ts +1 -1
  30. package/src/permission-hints-contract.ts +1 -1
  31. package/src/query-invalidation.ts +1 -1
  32. package/src/query-keys.ts +1 -1
  33. package/src/query-session.svelte.ts +1 -1
  34. package/src/query-snapshot.ts +17 -15
  35. package/src/record-decoder.ts +13 -2
  36. package/src/resource-contract.ts +31 -24
  37. package/src/resource-schemas.ts +21 -20
  38. package/src/resource-transport-config.ts +16 -7
  39. package/src/schema-form.ts +2 -2
  40. package/src/schema-validation.ts +119 -36
  41. package/src/spreadsheet-workbook.ts +1 -1
  42. package/src/strict-command-hooks.svelte.ts +1 -1
  43. package/src/task-contract.ts +1 -1
  44. package/src/task-provider.ts +1 -1
  45. package/src/update-contract.ts +1 -1
  46. package/src/update-many-contract.ts +1 -1
  47. package/src/useStepsForm.svelte.ts +1 -1
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  Use the existing `useForm.validate` callback with a shared TypeBox schema:
4
4
 
5
5
  ```ts
6
- import { Type, type Static } from '@sinclair/typebox';
6
+ import { Type, type Static } from 'typebox';
7
7
  import { createSchemaFormValidator, useForm } from '@svadmin/core';
8
8
 
9
9
  const Input = Type.Object({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@svadmin/core",
3
- "version": "0.58.0",
3
+ "version": "0.60.0",
4
4
  "author": "zuohuadong",
5
5
  "repository": {
6
6
  "type": "git",
@@ -135,7 +135,7 @@
135
135
  "dependencies": {
136
136
  "@types/papaparse": "^5.5.2",
137
137
  "@internationalized/date": "^3.12.3",
138
- "@sinclair/typebox": "^0.34.52",
138
+ "typebox": "^1.3.34",
139
139
  "papaparse": "^5.5.4"
140
140
  },
141
141
  "peerDependenciesMeta": {
@@ -1,4 +1,4 @@
1
- import { Type } from '@sinclair/typebox';
1
+ import { Type } from 'typebox';
2
2
  import { checkExact } from './schema-validation';
3
3
  import { snapshotPlainData } from './plain-data';
4
4
  import { HttpError } from './types';
package/src/admin-tool.ts CHANGED
@@ -1,5 +1,5 @@
1
- import type { StaticDecode, TObject } from '@sinclair/typebox';
2
- import { Value } from '@sinclair/typebox/value';
1
+ import type { StaticDecode, TObject } from 'typebox';
2
+ import { Value } from 'typebox/value';
3
3
 
4
4
  /** Result returned by a tool execution. */
5
5
  export interface ToolResult {
@@ -61,7 +61,10 @@ export function decodeAdminToolArgs<const Schema extends TObject>(
61
61
  tool: AdminTool<Schema>,
62
62
  input: unknown,
63
63
  ): StaticDecode<Schema> {
64
- return Value.Decode(strictAdminToolSchema(tool.parameters), input);
64
+ const schema = strictAdminToolSchema(tool.parameters);
65
+ // TypeBox 1.x Value.Decode strips undeclared properties instead of rejecting them.
66
+ if (!Value.Check(schema, input)) throw new Error('Admin tool arguments do not match the declared schema');
67
+ return Value.Decode(schema, input);
65
68
  }
66
69
 
67
70
  /** Decodes and executes a tool without allowing callers to bypass runtime input validation. */
@@ -94,6 +97,9 @@ export function projectAdminToolSchema<Schema extends TObject>(tool: AdminTool<S
94
97
  }
95
98
 
96
99
  function strictAdminToolSchema<Schema extends TObject>(schema: Schema): Schema {
97
- if (schema.additionalProperties === false) return schema;
98
- return { ...schema, additionalProperties: false };
100
+ if ((schema as { additionalProperties?: unknown }).additionalProperties === false) return schema;
101
+ // Spreading would drop TypeBox 1.x non-enumerable markers such as ~codec; clone instead.
102
+ const copy = Value.Clone(schema) as TObject & { additionalProperties?: boolean };
103
+ copy.additionalProperties = false;
104
+ return copy as Schema;
99
105
  }
@@ -1,4 +1,4 @@
1
- import { Type, type Static } from '@sinclair/typebox';
1
+ import { Type, type Static } from 'typebox';
2
2
  import { checkExact } from './schema-validation';
3
3
  import { snapshotPlainData } from './plain-data';
4
4
  import type { EnterpriseProviderRequestContext } from './enterprise';
@@ -1,4 +1,4 @@
1
- import { Type, type Static, type TSchema } from '@sinclair/typebox';
1
+ import { Type, ObjectOptions, type Static, type TSchema } from 'typebox';
2
2
  import { checkExact } from './schema-validation';
3
3
  import { snapshotPlainData } from './plain-data';
4
4
 
@@ -27,8 +27,8 @@ const entrySchema = Type.Object({
27
27
  userAgent: Type.Optional(text),
28
28
  error: Type.Optional(Type.Object({ message: Type.String(), code: Type.Optional(text) }, { additionalProperties: false })),
29
29
  }, { additionalProperties: false });
30
- const draftSchema = Type.Omit(entrySchema, ['id', 'timestamp']);
31
- const createSchema = Type.Omit(entrySchema, ['id']);
30
+ const draftSchema = Type.Omit(entrySchema, ['id', 'timestamp'], ObjectOptions(entrySchema));
31
+ const createSchema = Type.Omit(entrySchema, ['id'], ObjectOptions(entrySchema));
32
32
  const querySchema = Type.Object({
33
33
  resource: Type.Optional(text), action: Type.Optional(text),
34
34
  meta: Type.Optional(record), author: Type.Optional(record),
package/src/audit.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { definedOptions } from './defined-options';
2
- import { Value } from '@sinclair/typebox/value';
2
+ import { Value } from 'typebox/value';
3
3
  import {
4
4
  AuditError, createAuditEntry, decodeAuditCreate, decodeAuditEntries, decodeAuditEntry, decodeAuditQuery,
5
5
  type AuditCreateParams, type AuditDraft, type AuditEntry, type AuditQueryParams,
@@ -1,4 +1,4 @@
1
- import { Type, type Static } from '@sinclair/typebox';
1
+ import { Type, type Static } from 'typebox';
2
2
  import type { AuthProvider } from './types';
3
3
  import { authErrorSchema, authRedirectSchema, decodeIdentity } from './auth-query-contract';
4
4
  import { snapshotPlainData } from './plain-data';
@@ -1,4 +1,4 @@
1
- import { Type, type Static, type TSchema } from '@sinclair/typebox';
1
+ import { Type, type Static, type TSchema } from 'typebox';
2
2
  import { snapshotPlainData } from './plain-data';
3
3
  import { checkExact } from './schema-validation';
4
4
 
@@ -1,5 +1,4 @@
1
- import { Type, type TObject, type TSchema } from '@sinclair/typebox';
2
- import { TypeGuard } from '@sinclair/typebox/type';
1
+ import { Type, type TObject, type TSchema } from 'typebox';
3
2
  import { checkExact } from './schema-validation';
4
3
  import { closeContractSchema, type SafeSchema, type SchemaValue } from './resource-contract';
5
4
  import { snapshotPlainData } from './plain-data';
@@ -101,7 +100,7 @@ export function defineCommand(name: string, definition: {
101
100
  const outputSchema: unknown = fields['output']?.value;
102
101
  if (typeof url !== 'string' || !url.trim() ||
103
102
  (method !== 'get' && method !== 'post' && method !== 'put' && method !== 'patch' && method !== 'delete') ||
104
- !TypeGuard.IsObject(inputSchema) || !TypeGuard.IsSchema(outputSchema)) {
103
+ !Type.IsObject(inputSchema) || !Type.IsSchema(outputSchema)) {
105
104
  return invalidContract();
106
105
  }
107
106
  const input = closeContractSchema(inputSchema);
@@ -16,7 +16,7 @@ import { invalidateOwnedQueries } from './query-invalidation';
16
16
  import { appendListQueryFromPath } from './url-sync';
17
17
  import { HttpError } from './types';
18
18
  import { useTranslation } from './i18n.svelte';
19
- import { Type } from '@sinclair/typebox';
19
+ import { Type } from 'typebox';
20
20
  import { checkExact } from './schema-validation';
21
21
  import { auditWithProvider } from './audit';
22
22
  import { fireSuccessNotification, fireErrorNotification } from './hook-utils.svelte';
@@ -1,4 +1,4 @@
1
- import { Type } from '@sinclair/typebox';
1
+ import { Type } from 'typebox';
2
2
  import { checkExact } from './schema-validation';
3
3
  import { snapshotPlainData } from './plain-data';
4
4
  import { definedOptions } from './defined-options';
@@ -1,4 +1,4 @@
1
- import { Type } from '@sinclair/typebox';
1
+ import { Type } from 'typebox';
2
2
  import { snapshotPlainData } from './plain-data';
3
3
  import { checkExact } from './schema-validation';
4
4
  import { parseContractId, parseContractRecord, parseContractCreateInput, requireContractCreateSchema,
@@ -1,4 +1,4 @@
1
- import { Type, type Static } from '@sinclair/typebox';
1
+ import { Type, type Static } from 'typebox';
2
2
  import { checkExact } from './schema-validation';
3
3
  import { snapshotPlainData } from './plain-data';
4
4
  import type { EnterpriseProviderRequestContext } from './enterprise';
@@ -1,4 +1,4 @@
1
- import { Type } from '@sinclair/typebox';
1
+ import { Type } from 'typebox';
2
2
  import { snapshotPlainData } from './plain-data';
3
3
  import { checkExact } from './schema-validation';
4
4
  import { HttpError } from './types';
@@ -1,4 +1,4 @@
1
- import { Type } from '@sinclair/typebox';
1
+ import { Type } from 'typebox';
2
2
  import { snapshotPlainData } from './plain-data';
3
3
  import { checkExact } from './schema-validation';
4
4
  import { parseContractId, parseContractRecord, parseContractDeleteInput, type ContractSchemas, type ContractId, type ResourceContract } from './resource-contract';
@@ -1,7 +1,7 @@
1
1
  // Pure export formatting helpers with no Svelte rune or browser API dependencies except downloadData.
2
2
  // These helpers can be tested directly with bun:test.
3
3
 
4
- import { Type, type Static } from '@sinclair/typebox';
4
+ import { Type, type Static } from 'typebox';
5
5
  import { checkExact } from './schema-validation';
6
6
  import { snapshotPlainData } from './plain-data';
7
7
 
@@ -1,4 +1,4 @@
1
- import { Type } from '@sinclair/typebox';
1
+ import { Type } from 'typebox';
2
2
  import { checkExact } from './schema-validation';
3
3
  import { snapshotPlainData } from './plain-data';
4
4
  import { HttpError } from './types';
@@ -2,6 +2,7 @@
2
2
  // Extracted to enable unit testing with bun test
3
3
 
4
4
  import type { Filter, Sort, CrudOperator } from './types';
5
+ import { schemaIssues } from './schema-validation';
5
6
 
6
7
  // ─── Table Helpers ────────────────────────────────────────────
7
8
 
@@ -168,21 +169,24 @@ export function parseCSV(text: string): string[][] {
168
169
  // ─── TypeBox Validator Helper ───────────────────────────────────
169
170
 
170
171
  /**
171
- * TypeBox validator interface matching TypeCompiler.Compile or Value.Errors
172
+ * TypeBox validator interface matching Compile or Value.Errors
172
173
  */
173
174
  export type TypeBoxValidatorLike = {
174
175
  Check?: (value: unknown) => boolean;
175
- Errors: (value: unknown) => Iterable<{ path?: string; message: string }>;
176
+ Errors: (value: unknown) => Iterable<{
177
+ path?: string; instancePath?: string; keyword?: string;
178
+ params?: Record<string, unknown>; message: string;
179
+ }>;
176
180
  };
177
181
 
178
182
  /**
179
183
  * Creates a synchronous form validator function specifically optimized for TypeBox
180
- * compiled validators (from TypeCompiler.Compile) or TypeBox value checks.
184
+ * compiled validators (from Compile) or TypeBox value checks.
181
185
  *
182
186
  * @example
183
187
  * ```ts
184
- * import { Type } from '@sinclair/typebox';
185
- * import { TypeCompiler } from '@sinclair/typebox/compiler';
188
+ * import { Type } from 'typebox';
189
+ * import { Compile } from 'typebox/compile';
186
190
  * import { createTypeBoxValidator, useForm } from '@svadmin/core';
187
191
  *
188
192
  * const userSchema = Type.Object({
@@ -190,7 +194,7 @@ export type TypeBoxValidatorLike = {
190
194
  * email: Type.String({ format: 'email' }),
191
195
  * });
192
196
  *
193
- * const compiledUser = TypeCompiler.Compile(userSchema);
197
+ * const compiledUser = Compile(userSchema);
194
198
  *
195
199
  * const form = useForm({
196
200
  * resource: 'users',
@@ -205,10 +209,10 @@ export function createTypeBoxValidator(
205
209
  if (validator.Check?.(values)) return null;
206
210
 
207
211
  const errors: Record<string, string> = {};
208
- for (const error of validator.Errors(values)) {
209
- const path = (error.path ?? '').replace(/^\//, '').replace(/\//g, '.');
212
+ for (const issue of schemaIssues(validator.Errors(values))) {
213
+ const path = issue.path.replace(/^\//, '').replace(/\//g, '.');
210
214
  const key = path || '_root';
211
- if (!errors[key]) errors[key] = error.message;
215
+ if (!errors[key]) errors[key] = issue.message;
212
216
  }
213
217
 
214
218
  return Object.keys(errors).length > 0 ? errors : null;
package/src/http.ts CHANGED
@@ -3,8 +3,8 @@
3
3
  */
4
4
 
5
5
  import { HttpError,type ValidationErrors } from './types';
6
- import { Type } from '@sinclair/typebox';
7
- import { Value } from '@sinclair/typebox/value';
6
+ import { Type } from 'typebox';
7
+ import { Value } from 'typebox/value';
8
8
  import { definedOptions } from './defined-options';
9
9
 
10
10
  export interface FetchWithInterceptorOptions {
@@ -1,5 +1,5 @@
1
1
  import Papa from 'papaparse';
2
- import { Type, type Static } from '@sinclair/typebox';
2
+ import { Type, type Static } from 'typebox';
3
3
  import { checkExact } from './schema-validation';
4
4
  import { snapshotPlainData } from './plain-data';
5
5
  import { HttpError } from './types';
package/src/index.ts CHANGED
@@ -304,7 +304,7 @@ export {
304
304
  generateComponentCode,
305
305
  generateResourceBundle,
306
306
  } from './inferencer';
307
- export type { InferResult } from './inferencer';
307
+ export type { InferResult, InferReview, InferReviewWarning, InferReviewWarningCode } from './inferencer';
308
308
  export { createWebSocketLiveProvider } from './live-websocket';
309
309
  export type { WebSocketLiveProviderOptions } from './live-websocket';
310
310
  export { createSSELiveProvider } from './live-sse';
package/src/inferencer.ts CHANGED
@@ -84,6 +84,7 @@ export interface InferResult {
84
84
  resource: ResourceDefinition;
85
85
  code: string;
86
86
  typeboxCode: string;
87
+ review: InferReview;
87
88
  componentCode: {
88
89
  list: string;
89
90
  create: string;
@@ -92,6 +93,26 @@ export interface InferResult {
92
93
  };
93
94
  }
94
95
 
96
+ export type InferReviewWarningCode =
97
+ | 'empty-sample'
98
+ | 'nullable-field'
99
+ | 'mixed-types'
100
+ | 'heuristic-relation'
101
+ | 'heuristic-select';
102
+
103
+ export interface InferReviewWarning {
104
+ code: InferReviewWarningCode;
105
+ field?: string;
106
+ message: string;
107
+ }
108
+
109
+ export interface InferReview {
110
+ needsReview: boolean;
111
+ confidence: 'high' | 'medium' | 'low';
112
+ sampleSize: number;
113
+ warnings: InferReviewWarning[];
114
+ }
115
+
95
116
  /**
96
117
  * Analyze a sample data array and produce a ResourceDefinition.
97
118
  * @param resourceName - Name of the resource (e.g. "posts")
@@ -116,6 +137,15 @@ export function inferResource(
116
137
  resource: emptyResource,
117
138
  code: `// No data available to infer fields for "${resourceName}".`,
118
139
  typeboxCode: generateTypeBoxSchemaCode(emptyResource),
140
+ review: {
141
+ needsReview: true,
142
+ confidence: 'low',
143
+ sampleSize: 0,
144
+ warnings: [{
145
+ code: 'empty-sample',
146
+ message: `No sample records were available for "${resourceName}".`,
147
+ }],
148
+ },
119
149
  componentCode: {
120
150
  list: generateListPageCode(emptyResource),
121
151
  create: generateCreatePageCode(emptyResource),
@@ -133,6 +163,7 @@ export function inferResource(
133
163
 
134
164
  // For each key, infer from all non-null values
135
165
  const fields: FieldDefinition[] = [];
166
+ const warnings: InferReviewWarning[] = [];
136
167
  for (const key of keySet) {
137
168
  // Collect non-null values
138
169
  const values = sampleData
@@ -141,6 +172,13 @@ export function inferResource(
141
172
 
142
173
  const sampleValue = values[0];
143
174
  let inferredType = inferFieldType(key, sampleValue);
175
+ if (values.length === 0) {
176
+ warnings.push({
177
+ code: 'nullable-field',
178
+ field: key,
179
+ message: `Field "${key}" contains only null or undefined values in the sample.`,
180
+ });
181
+ }
144
182
 
145
183
  // Cross-validate: if most values for this key are of a different type, use majority
146
184
  const typeCounts = new Map<InferredType, number>();
@@ -155,13 +193,34 @@ export function inferResource(
155
193
  inferredType = t;
156
194
  }
157
195
  }
196
+ if (typeCounts.size > 1) {
197
+ warnings.push({
198
+ code: 'mixed-types',
199
+ field: key,
200
+ message: `Field "${key}" contains multiple inferred types; the majority type was selected.`,
201
+ });
202
+ }
158
203
 
159
204
  // Check for relation
160
205
  const relatedResource = isLikelyRelation(key);
206
+ if (relatedResource) {
207
+ warnings.push({
208
+ code: 'heuristic-relation',
209
+ field: key,
210
+ message: `Relation "${key}" was inferred from its field name.`,
211
+ });
212
+ }
161
213
 
162
214
  // Check if it looks like a select (few unique string values)
163
215
  const uniqueStrings = new Set(values.filter(v => typeof v === 'string') as string[]);
164
216
  const isSelect = inferredType === 'text' && uniqueStrings.size > 1 && uniqueStrings.size <= 10 && values.length >= 5;
217
+ if (isSelect) {
218
+ warnings.push({
219
+ code: 'heuristic-select',
220
+ field: key,
221
+ message: `Select options for "${key}" were inferred from repeated sample values.`,
222
+ });
223
+ }
165
224
 
166
225
  const field: FieldDefinition = {
167
226
  key,
@@ -213,7 +272,23 @@ export function inferResource(
213
272
  show: generateShowPageCode(resource),
214
273
  };
215
274
 
216
- return { fields, resource, code, typeboxCode, componentCode };
275
+ return {
276
+ fields,
277
+ resource,
278
+ code,
279
+ typeboxCode,
280
+ review: {
281
+ needsReview: warnings.length > 0,
282
+ confidence: warnings.some((warning) => warning.code === 'mixed-types' || warning.code === 'nullable-field')
283
+ ? 'low'
284
+ : warnings.length > 0
285
+ ? 'medium'
286
+ : 'high',
287
+ sampleSize: sampleData.length,
288
+ warnings,
289
+ },
290
+ componentCode,
291
+ };
217
292
  }
218
293
 
219
294
  // ─── Code Generation ─────────────────────────────────────────
@@ -407,7 +482,7 @@ export function generateTypeBoxSchemaCode(resource: ResourceDefinition): string
407
482
  const baseName = resource.name.endsWith('s') ? resource.name.slice(0, -1) : resource.name;
408
483
  const typeName = capitalize(baseName);
409
484
 
410
- return `import { Type, type Static } from '@sinclair/typebox';
485
+ return `import { Type, type Static } from 'typebox';
411
486
 
412
487
  export const ${typeName}Schema = Type.Object({
413
488
  ${schemaProps}
@@ -462,6 +537,12 @@ export function generateResourceBundle(resource: ResourceDefinition): InferResul
462
537
  resource,
463
538
  code: generateResourceCode(resource),
464
539
  typeboxCode: generateTypeBoxSchemaCode(resource),
540
+ review: {
541
+ needsReview: false,
542
+ confidence: 'high',
543
+ sampleSize: 0,
544
+ warnings: [],
545
+ },
465
546
  componentCode: {
466
547
  list: generateListPageCode(resource),
467
548
  create: generateCreatePageCode(resource),
@@ -1,7 +1,7 @@
1
1
  import { createInfiniteQuery, type InfiniteData, type InfiniteQueryObserverBaseResult, type QueryObserverResult,
2
2
  type RefetchOptions, type FetchNextPageOptions, type FetchPreviousPageOptions } from '@tanstack/svelte-query';
3
3
  import { tick } from 'svelte';
4
- import { Type } from '@sinclair/typebox';
4
+ import { Type } from 'typebox';
5
5
  import { definedOptions } from './defined-options';
6
6
  import { getAdminOptions } from './options.svelte';
7
7
  import { useParsed } from './useParsed.svelte';
@@ -1,4 +1,4 @@
1
- import { Type } from '@sinclair/typebox';
1
+ import { Type } from 'typebox';
2
2
  import { snapshotPlainData } from './plain-data';
3
3
  import { checkExact } from './schema-validation';
4
4
  import { HttpError } from './types';
package/src/live-sse.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import type { LiveProvider } from './live.svelte';
2
2
  import { createLiveSubscribers, decodeLiveMessage, notifyLiveObserver, readLiveOptions, captureLiveObserver } from './live-transport';
3
- import { Type } from '@sinclair/typebox';
3
+ import { Type } from 'typebox';
4
4
  import { checkExact } from './schema-validation';
5
5
  import { snapshotPlainData } from './plain-data';
6
6
 
@@ -1,6 +1,6 @@
1
1
  import { untrack } from 'svelte';
2
2
  import type { QueryClient } from '@tanstack/svelte-query';
3
- import { Type } from '@sinclair/typebox';
3
+ import { Type } from 'typebox';
4
4
  import { captureAdminContext } from './context.svelte';
5
5
  import { contractKey, type ResourceContract } from './resource-contract';
6
6
  import { captureQueryProvider } from './query-snapshot';
@@ -1,4 +1,4 @@
1
- import { Type, type Static } from '@sinclair/typebox';
1
+ import { Type, type Static } from 'typebox';
2
2
  import { checkExact } from './schema-validation';
3
3
  import { snapshotPlainData, type JsonValue } from './plain-data';
4
4
  import { definedOptions } from './defined-options';
@@ -1,4 +1,4 @@
1
- import { Type, type Static } from '@sinclair/typebox';
1
+ import { Type, type Static } from 'typebox';
2
2
  import { checkExact } from './schema-validation';
3
3
  import { snapshotPlainData } from './plain-data';
4
4
  import type { AccessControlOptions, AccessControlProvider, CanParams } from './permissions.svelte';
@@ -1,4 +1,4 @@
1
- import { Type } from '@sinclair/typebox';
1
+ import { Type } from 'typebox';
2
2
  import { checkExact } from './schema-validation';
3
3
  import { snapshotPlainData } from './plain-data';
4
4
  import { decodePermissionSnapshot, permissionSnapshotSchema, type PermissionSnapshot } from './permission-catalog';
@@ -4,7 +4,7 @@ import { snapshotPlainData } from './plain-data';
4
4
  import { snapshotInvalidationParams } from './invalidation-contract';
5
5
  import { definedOptions } from './defined-options';
6
6
  import { HttpError } from './types';
7
- import { Type } from '@sinclair/typebox';
7
+ import { Type } from 'typebox';
8
8
  import { checkExact } from './schema-validation';
9
9
 
10
10
  export interface QueryCacheOwner {
package/src/query-keys.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Type,type Static } from '@sinclair/typebox';
1
+ import { Type,type Static } from 'typebox';
2
2
  import { checkExact } from './schema-validation';
3
3
  import { definedOptions } from './defined-options';
4
4
  import { snapshotPlainData, type JsonValue } from './plain-data';
@@ -1,6 +1,6 @@
1
1
  import { hashKey, useQueryClient, type QueryObserverPendingResult, type QueryObserverResult, type RefetchOptions } from '@tanstack/svelte-query';
2
2
  import { untrack } from 'svelte';
3
- import { Type } from '@sinclair/typebox';
3
+ import { Type } from 'typebox';
4
4
  import type { AdminContextAccessor } from './context.svelte';
5
5
  import { captureAuthLiveScope, clearAuthQueries, handleAuthError } from './auth-hooks.svelte';
6
6
  import { decodeBaseRecord } from './record-decoder';
@@ -1,4 +1,4 @@
1
- import { Type, type Static, type TSchema } from '@sinclair/typebox';
1
+ import { Type, type Static, type TSchema } from 'typebox';
2
2
  import type { AdminContextAccessor } from './context.svelte';
3
3
  import type { DataProvider, GetListParams, GetOneParams, GetManyParams } from './types';
4
4
  import { HttpError } from './types';
@@ -8,20 +8,22 @@ import { attachAbortSignal, detachAbortSignal, snapshotPlainData } from './plain
8
8
  import { checkExact } from './schema-validation';
9
9
  import { decodeBaseRecord, decodeListResult, decodeOneResult, decodeManyResult, rejectProviderResponse } from './record-decoder';
10
10
 
11
- const filters = Type.Recursive(self => Type.Union([
12
- Type.Object({
13
- field: Type.String(),
14
- operator: Type.Union(([
15
- 'eq', 'ne', 'lt', 'gt', 'lte', 'gte', 'in', 'nin', 'contains', 'ncontains',
16
- 'startswith', 'endswith', 'null', 'nnull', 'between', 'nbetween',
17
- ] as const).map(value => Type.Literal(value))),
18
- value: Type.Unknown(),
19
- }, { additionalProperties: false }),
20
- Type.Object({
21
- operator: Type.Union([Type.Literal('and'), Type.Literal('or')]),
22
- value: Type.Array(self),
23
- }, { additionalProperties: false }),
24
- ]));
11
+ const filters = Type.Cyclic({
12
+ Filters: Type.Union([
13
+ Type.Object({
14
+ field: Type.String(),
15
+ operator: Type.Union(([
16
+ 'eq', 'ne', 'lt', 'gt', 'lte', 'gte', 'in', 'nin', 'contains', 'ncontains',
17
+ 'startswith', 'endswith', 'null', 'nnull', 'between', 'nbetween',
18
+ ] as const).map(value => Type.Literal(value))),
19
+ value: Type.Unknown(),
20
+ }, { additionalProperties: false }),
21
+ Type.Object({
22
+ operator: Type.Union([Type.Literal('and'), Type.Literal('or')]),
23
+ value: Type.Array(Type.Ref('Filters')),
24
+ }, { additionalProperties: false }),
25
+ ]),
26
+ }, 'Filters');
25
27
  const base = {
26
28
  resource: Type.String({ minLength: 1 }),
27
29
  meta: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
@@ -1,11 +1,22 @@
1
- import { Type } from '@sinclair/typebox';
1
+ import { Type } from 'typebox';
2
2
  import { createExactSchemaValidator } from './schema-validation';
3
3
  import { snapshotPlainData } from './plain-data';
4
4
  import { HttpError, type BaseRecord, type GetListResult, type GetOneResult, type GetManyResult, type CustomResult } from './types';
5
5
 
6
6
  export type RecordDecoder<T extends BaseRecord> = (value: unknown) => T;
7
7
 
8
- const record = Type.Record(Type.String(), Type.Unknown());
8
+ /** TypeBox 1.x Record validation accepts class instances; keep records limited to plain JSON objects. */
9
+ function isPlainRecord(value: unknown): value is Record<string, unknown> {
10
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) return false;
11
+ const prototype: unknown = Object.getPrototypeOf(value);
12
+ return prototype === Object.prototype || prototype === null;
13
+ }
14
+
15
+ const record = Type.Refine(
16
+ Type.Record(Type.String(), Type.Unknown()),
17
+ isPlainRecord,
18
+ () => 'Expected plain object',
19
+ );
9
20
  const recordValidator = createExactSchemaValidator(record);
10
21
  const customValidator = createExactSchemaValidator(Type.Object({ data: Type.Unknown() }));
11
22
  const oneValidator = createExactSchemaValidator(Type.Object({ data: record }));
@@ -1,6 +1,6 @@
1
- import { CloneType, Kind, TransformKind, Type, type Static, type TObject, type TSchema } from '@sinclair/typebox';
2
- import { checkExact, createExactSchemaValidator } from './schema-validation';
3
- import { TypeGuard } from '@sinclair/typebox/type';
1
+ import { Type, type Static, type TObject, type TSchema } from 'typebox';
2
+ import { Value } from 'typebox/value';
3
+ import { checkExact, createExactSchemaValidator, schemaIssues } from './schema-validation';
4
4
  import { DeleteManyPartialError, HttpError, type DataProvider, type KnownResources, type BaseRecord,
5
5
  type GetListResult, type GetManyParams, type CreateManyParams, type UpdateManyParams, type DeleteManyParams,
6
6
  type GetOneParams, type GetOneResult, type GetManyResult } from './types';
@@ -70,26 +70,33 @@ interface Definition {
70
70
  const definitions = new WeakMap<ResourceContract, Definition>();
71
71
  let nextKey = 0;
72
72
 
73
+ /** TypeBox 1.x stores the schema kind under the non-enumerable `~kind` marker instead of a symbol. */
74
+ function schemaKind(schema: TSchema): string | undefined {
75
+ const kind = (schema as { '~kind'?: unknown })['~kind'];
76
+ return typeof kind === 'string' ? kind : undefined;
77
+ }
78
+
73
79
  // A deliberately closed JSON subset keeps runtime validation aligned with Static.
74
80
  export function closeContractSchema<S extends TSchema>(schema: S): S {
75
- const copy = CloneType(schema);
81
+ const copy = Value.Clone(schema);
76
82
  const visit = (node: TSchema): void => {
77
- if (TransformKind in node) {
83
+ if ('~codec' in node) {
78
84
  throw new HttpError('Contract transforms require a separate decoding boundary', 400, undefined, { code: 'INVALID_RESOURCE_CONTRACT' });
79
85
  }
80
- if (!TypeGuard.IsSchema(node) || !['Object', 'Array', 'Tuple', 'Union', 'String', 'Number', 'Integer', 'Boolean', 'Null', 'Literal', 'Never'].includes(node[Kind])) {
86
+ if (!Type.IsSchema(node) || !['Object', 'Array', 'Tuple', 'Union', 'String', 'Number', 'Integer', 'Boolean', 'Null', 'Literal', 'Never'].includes(schemaKind(node) ?? '')) {
81
87
  throw new HttpError('Unsupported or unconstrained contract schema', 400, undefined, { code: 'INVALID_RESOURCE_CONTRACT' });
82
88
  }
83
- if (TypeGuard.IsObject(node)) {
84
- if (node.additionalProperties !== undefined && node.additionalProperties !== false) {
89
+ if (Type.IsObject(node)) {
90
+ const objectNode = node as TSchema & { additionalProperties?: TSchema | boolean };
91
+ if (objectNode.additionalProperties !== undefined && objectNode.additionalProperties !== false) {
85
92
  throw new HttpError('Contract objects must be closed', 400, undefined, { code: 'INVALID_RESOURCE_CONTRACT' });
86
93
  }
87
- node.additionalProperties = false;
94
+ objectNode.additionalProperties = false;
88
95
  for (const property of Object.values(node.properties)) visit(property);
89
96
  }
90
- if (TypeGuard.IsArray(node)) visit(node.items);
91
- if (TypeGuard.IsTuple(node)) for (const item of node.items ?? []) visit(item);
92
- if (TypeGuard.IsUnion(node)) for (const item of node.anyOf) visit(item);
97
+ if (Type.IsArray(node)) visit(node.items);
98
+ if (Type.IsTuple(node)) for (const item of node.items ?? []) visit(item);
99
+ if (Type.IsUnion(node)) for (const item of node.anyOf) visit(item);
93
100
  };
94
101
  visit(copy);
95
102
  return copy;
@@ -101,11 +108,11 @@ export function defineResource<const S extends ContractSchemas>(
101
108
  ): ResourceContract<S>;
102
109
  export function defineResource(name: KnownResources, schemas: ContractSchemas): ResourceContract {
103
110
  const isId = (schema: TSchema): boolean => (
104
- ['String', 'Number', 'Integer'].includes(schema[Kind])
105
- || (TypeGuard.IsLiteral(schema) && ['string', 'number'].includes(typeof schema.const))
106
- || (TypeGuard.IsUnion(schema) && schema.anyOf.every(isId))
111
+ ['String', 'Number', 'Integer'].includes(schemaKind(schema) ?? '')
112
+ || (Type.IsLiteral(schema) && ['string', 'number'].includes(typeof schema.const))
113
+ || (Type.IsUnion(schema) && schema.anyOf.every(isId))
107
114
  );
108
- if (!name.trim() || schemas.record[Kind] !== 'Object'
115
+ if (!name.trim() || schemaKind(schemas.record) !== 'Object'
109
116
  || !schemas.record.required?.includes('id')
110
117
  || !schemas.record.properties['id']
111
118
  || !isId(schemas.record.properties['id'])) {
@@ -177,7 +184,7 @@ export interface ContractProjectionOptions {
177
184
  }
178
185
 
179
186
  function projectBySchema(schema: TSchema, value: unknown): unknown {
180
- if (TypeGuard.IsObject(schema)) {
187
+ if (Type.IsObject(schema)) {
181
188
  if (typeof value !== 'object' || value === null || Array.isArray(value)) return value;
182
189
  const source = value as Record<string, unknown>;
183
190
  const projected: Record<string, unknown> = {};
@@ -186,15 +193,15 @@ function projectBySchema(schema: TSchema, value: unknown): unknown {
186
193
  }
187
194
  return projected;
188
195
  }
189
- if (TypeGuard.IsArray(schema)) {
196
+ if (Type.IsArray(schema)) {
190
197
  return Array.isArray(value) ? value.map(item => projectBySchema(schema.items, item)) : value;
191
198
  }
192
- if (TypeGuard.IsTuple(schema)) {
199
+ if (Type.IsTuple(schema)) {
193
200
  return Array.isArray(value)
194
201
  ? value.map((item, index) => schema.items?.[index] ? projectBySchema(schema.items[index], item) : item)
195
202
  : value;
196
203
  }
197
- if (TypeGuard.IsUnion(schema)) {
204
+ if (Type.IsUnion(schema)) {
198
205
  const branch = schema.anyOf.find(candidate => checkExact(candidate, value));
199
206
  return branch ? projectBySchema(branch, value) : value;
200
207
  }
@@ -394,7 +401,7 @@ export function parseContractDeleteInput(contract: ResourceContract, value: unkn
394
401
  export function contractFormValues(contract: ResourceContract, action: string, value: Record<string, unknown>): Record<string, unknown> {
395
402
  const schemas = definitionOf(contract).schemas;
396
403
  const schema = action === 'edit' ? schemas.update : schemas.create;
397
- if (!schema || !TypeGuard.IsObject(schema)) {
404
+ if (!schema || !Type.IsObject(schema)) {
398
405
  throw new HttpError('Forms require an object input schema for their action', 400, undefined, { code: 'INVALID_RESOURCE_CONTRACT' });
399
406
  }
400
407
  const projected = Object.fromEntries(Object.keys(schema.properties)
@@ -406,7 +413,7 @@ export function contractFormValues(contract: ResourceContract, action: string, v
406
413
  function formSchema(contract: ResourceContract, action: ContractFormAction): TObject {
407
414
  const schemas = definitionOf(contract).schemas;
408
415
  const schema = action === 'show' ? schemas.record : action === 'edit' ? schemas.update : schemas.create;
409
- if (!schema || !TypeGuard.IsObject(schema)) {
416
+ if (!schema || !Type.IsObject(schema)) {
410
417
  throw new HttpError('Forms require an object schema for their action', 400, undefined, { code: 'INVALID_RESOURCE_CONTRACT' });
411
418
  }
412
419
  return schema;
@@ -445,7 +452,7 @@ export function snapshotContractFormDraft(contract: ResourceContract, action: Co
445
452
  export function contractFormInvalidFields(contract: ResourceContract, action: ContractFormAction, value: unknown): string[] {
446
453
  const schema = formSchema(contract, action);
447
454
  const validator = createExactSchemaValidator(schema);
448
- const paths = Array.from(validator.Errors(value), error => error.path);
455
+ const paths = schemaIssues(validator.Errors(value)).map(issue => issue.path);
449
456
  const fields = Object.keys(schema.properties).filter(field => {
450
457
  const path = `/${field.replaceAll('~', '~0').replaceAll('/', '~1')}`;
451
458
  return paths.some(candidate => candidate === path || candidate.startsWith(`${path}/`));
@@ -460,7 +467,7 @@ export function parseContractFormInput<S extends ContractSchemas, A extends 'cre
460
467
  export function parseContractFormInput(contract: ResourceContract, action: 'create' | 'edit', value: unknown): BaseRecord {
461
468
  const schemas = definitionOf(contract).schemas;
462
469
  const schema = action === 'edit' ? schemas.update : schemas.create;
463
- if (!schema || !TypeGuard.IsObject(schema)) {
470
+ if (!schema || !Type.IsObject(schema)) {
464
471
  throw new HttpError('Forms require an object input schema for their action', 400, undefined, { code: 'INVALID_RESOURCE_CONTRACT' });
465
472
  }
466
473
  check(schema, value, 'values');
@@ -1,5 +1,5 @@
1
- import { Type, type Static, type TSchema } from '@sinclair/typebox';
2
- import { checkExact, createExactSchemaValidator } from './schema-validation';
1
+ import { Type, type Static, type TSchema } from 'typebox';
2
+ import { checkExact, createExactSchemaValidator, schemaIssues } from './schema-validation';
3
3
  import { attachAbortSignal, detachAbortSignal, snapshotPlainData } from './plain-data';
4
4
  import { decodeBaseRecord, decodeOneResult, decodeManyResult, decodeListResult } from './record-decoder';
5
5
  import { HttpError } from './types';
@@ -43,22 +43,24 @@ const common = {
43
43
  resource: Type.String({ minLength: 1 }),
44
44
  meta: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
45
45
  };
46
- const filter = Type.Recursive(Self => Type.Union([
47
- Type.Object({
48
- operator: Type.Union([Type.Literal('or'), Type.Literal('and')]),
49
- value: Type.Array(Self),
50
- }, { additionalProperties: false }),
51
- Type.Object({
52
- field: Type.String(),
53
- operator: Type.Union([
54
- Type.Literal('eq'), Type.Literal('ne'), Type.Literal('lt'), Type.Literal('gt'),
55
- Type.Literal('lte'), Type.Literal('gte'), Type.Literal('contains'), Type.Literal('ncontains'),
56
- Type.Literal('startswith'), Type.Literal('endswith'), Type.Literal('in'), Type.Literal('nin'),
57
- Type.Literal('null'), Type.Literal('nnull'), Type.Literal('between'), Type.Literal('nbetween'),
58
- ]),
59
- value: Type.Unknown(),
60
- }, { additionalProperties: false }),
61
- ]));
46
+ const filter = Type.Cyclic({
47
+ Filter: Type.Union([
48
+ Type.Object({
49
+ operator: Type.Union([Type.Literal('or'), Type.Literal('and')]),
50
+ value: Type.Array(Type.Ref('Filter')),
51
+ }, { additionalProperties: false }),
52
+ Type.Object({
53
+ field: Type.String(),
54
+ operator: Type.Union([
55
+ Type.Literal('eq'), Type.Literal('ne'), Type.Literal('lt'), Type.Literal('gt'),
56
+ Type.Literal('lte'), Type.Literal('gte'), Type.Literal('contains'), Type.Literal('ncontains'),
57
+ Type.Literal('startswith'), Type.Literal('endswith'), Type.Literal('in'), Type.Literal('nin'),
58
+ Type.Literal('null'), Type.Literal('nnull'), Type.Literal('between'), Type.Literal('nbetween'),
59
+ ]),
60
+ value: Type.Unknown(),
61
+ }, { additionalProperties: false }),
62
+ ]),
63
+ }, 'Filter');
62
64
  const requestSchemas = {
63
65
  getList: Type.Object({
64
66
  ...common,
@@ -166,8 +168,7 @@ function createSchemaBoundary(schemas: ResourceSchemaMap) {
166
168
  }
167
169
  if (validator.Check(value)) return;
168
170
  // Do not include rejected values or raw responses in diagnostic details.
169
- issues = [...validator.Errors(value)]
170
- .map(({ path, message }) => ({ path, message }));
171
+ issues = schemaIssues(validator.Errors(value));
171
172
  } catch {
172
173
  throw new HttpError('Unable to evaluate resource schema', 500, undefined, {
173
174
  code: 'RESOURCE_SCHEMA_INVALID',
@@ -1,5 +1,4 @@
1
- import { Hint, Kind, OptionalKind, ReadonlyKind, type TSchema } from '@sinclair/typebox';
2
- import { TypeGuard } from '@sinclair/typebox/type';
1
+ import { Type, type TSchema } from 'typebox';
3
2
  import { HttpError } from './types';
4
3
  import type { DataProvider } from './types';
5
4
  import type { ResourceSchemas } from './resource-schemas';
@@ -15,6 +14,9 @@ function invalidProvider(): never {
15
14
  });
16
15
  }
17
16
 
17
+ /** Scalar TypeBox 1.x markers that may be carried through a configuration snapshot. */
18
+ const TYPEBOX_MARKERS = new Set(['~kind', '~optional', '~readonly', '~immutable']);
19
+
18
20
  /** Configuration can carry TypeBox markers, but no getters, functions or foreign symbols. */
19
21
  function copySchemaData(value: unknown, ancestors = new Set<object>(), depth = 0): unknown {
20
22
  if (depth > 128) return invalidSchema();
@@ -41,11 +43,18 @@ function copySchemaData(value: unknown, ancestors = new Set<object>(), depth = 0
41
43
  }
42
44
  const copy: Record<PropertyKey, unknown> = {};
43
45
  for (const key of Reflect.ownKeys(descriptors)) {
44
- if (typeof key === 'symbol' && key !== Kind && key !== OptionalKind && key !== ReadonlyKind && key !== Hint) {
45
- return invalidSchema();
46
+ // TypeBox 1.x marks schema metadata with non-enumerable `~`-prefixed string keys.
47
+ if (typeof key === 'symbol') return invalidSchema();
48
+ if (key.startsWith('~')) {
49
+ if (!TYPEBOX_MARKERS.has(key)) return invalidSchema();
50
+ const marker: PropertyDescriptor | undefined = Reflect.get(descriptors, key);
51
+ if (!marker || !('value' in marker) || marker.get || marker.set) return invalidSchema();
52
+ const markerValue: unknown = marker.value;
53
+ if (typeof markerValue !== 'string' && typeof markerValue !== 'boolean') return invalidSchema();
54
+ Object.defineProperty(copy, key, { value: markerValue, enumerable: false, configurable: true, writable: false });
55
+ continue;
46
56
  }
47
57
  const field = read(key);
48
- if (typeof key === 'symbol' && typeof field !== 'string') return invalidSchema();
49
58
  Object.defineProperty(copy, key, { value: field, enumerable: true, configurable: true, writable: true });
50
59
  }
51
60
  return copy;
@@ -56,7 +65,7 @@ function copySchemaData(value: unknown, ancestors = new Set<object>(), depth = 0
56
65
 
57
66
  function schema(value: unknown): TSchema {
58
67
  const copy = copySchemaData(value);
59
- if (!TypeGuard.IsSchema(copy)) return invalidSchema();
68
+ if (!Type.IsSchema(copy) || typeof (copy as { '~kind'?: unknown })['~kind'] !== 'string') return invalidSchema();
60
69
  return copy;
61
70
  }
62
71
 
@@ -88,7 +97,7 @@ export function captureResourceSchemas(value: unknown): Map<string, ResourceSche
88
97
  if (!Array.isArray(copied)) return invalidSchema();
89
98
  const candidates: unknown[] = copied;
90
99
  for (const candidate of candidates) {
91
- if (!TypeGuard.IsSchema(candidate)) return invalidSchema();
100
+ if (!Type.IsSchema(candidate)) return invalidSchema();
92
101
  references.push(candidate);
93
102
  }
94
103
  }
@@ -1,5 +1,5 @@
1
- import { Type, type TSchema } from '@sinclair/typebox';
2
- import { Value } from '@sinclair/typebox/value';
1
+ import { Type, type TSchema } from 'typebox';
2
+ import { Value } from 'typebox/value';
3
3
 
4
4
  export type SchemaFormScalar = string | number | boolean | null;
5
5
 
@@ -1,17 +1,39 @@
1
- import { CloneType, TransformKind, Type, type Static, type TSchema } from '@sinclair/typebox';
2
- import { TypeGuard } from '@sinclair/typebox/type';
3
- import { Value } from '@sinclair/typebox/value';
1
+ import { Type, type Static, type TSchema } from 'typebox';
2
+ import { Value } from 'typebox/value';
3
+
4
+ const CODEC_KEY = '~codec';
5
+
6
+ /** JSON Schema keywords that TypeBox 1.x stores but does not expose on every typed interface. */
7
+ interface SchemaMeta {
8
+ $id?: string;
9
+ $defs?: Record<string, TSchema>;
10
+ properties?: Record<string, TSchema>;
11
+ required?: readonly string[];
12
+ additionalProperties?: TSchema | boolean;
13
+ patternProperties?: Record<string, TSchema>;
14
+ contains?: TSchema;
15
+ unevaluatedProperties?: TSchema | boolean;
16
+ anyOf?: TSchema[];
17
+ allOf?: TSchema[];
18
+ items?: TSchema | TSchema[];
19
+ not?: TSchema;
20
+ }
4
21
 
5
22
  /**
6
23
  * Strengthen optional-property presence with ordinary schemas, without changing
7
24
  * TypeBox's process-wide policy or maintaining a second value validator.
8
25
  */
9
- function exactSchema(schema: TSchema): TSchema {
10
- if (TypeGuard.IsObject(schema)) {
26
+ function exactSchema(input: TSchema): TSchema {
27
+ const schema = input as TSchema & SchemaMeta;
28
+ if (typeof schema.not === 'object' && Type.IsSchema(schema.not)) {
29
+ schema.not = exactSchema(schema.not);
30
+ }
31
+ if (Type.IsObject(input)) {
32
+ const properties = schema.properties ?? {};
11
33
  schema.properties = Object.fromEntries(
12
- Object.entries(schema.properties).map(([key, property]) => [key, exactSchema(property)]),
34
+ Object.entries(properties).map(([key, property]) => [key, exactSchema(property)]),
13
35
  );
14
- if (TypeGuard.IsSchema(schema.additionalProperties)) {
36
+ if (typeof schema.additionalProperties === 'object' && Type.IsSchema(schema.additionalProperties)) {
15
37
  schema.additionalProperties = exactSchema(schema.additionalProperties);
16
38
  }
17
39
  const optional = Object.entries(schema.properties)
@@ -20,7 +42,8 @@ function exactSchema(schema: TSchema): TSchema {
20
42
 
21
43
  const constraints = optional.map(([key, property]) => Type.Union([
22
44
  Type.Required(Type.Object({ [key]: property })),
23
- Type.Not(Type.Object({ [key]: Type.Unknown() })),
45
+ // TypeBox 1.x removed Type.Not; the JSON Schema `not` keyword is expressed through Unsafe.
46
+ Type.Unsafe({ not: Type.Object({ [key]: Type.Unknown() }) }),
24
47
  ]));
25
48
  // The presence constraint owns each optional value's validation. Leaving it
26
49
  // in both places would validate deep optional objects exponentially often.
@@ -28,57 +51,117 @@ function exactSchema(schema: TSchema): TSchema {
28
51
  // References must resolve to the strengthened object, not its inner copy.
29
52
  const id = schema.$id;
30
53
  delete schema.$id;
31
- const transform = Object.getOwnPropertyDescriptor(schema, TransformKind);
32
- Reflect.deleteProperty(schema, TransformKind);
54
+ const codec = Object.getOwnPropertyDescriptor(schema, CODEC_KEY);
55
+ Reflect.deleteProperty(schema, CODEC_KEY);
33
56
  const result = Type.Intersect([schema, ...constraints], id === undefined ? {} : { $id: id });
34
- if (transform !== undefined) Object.defineProperty(result, TransformKind, transform);
57
+ if (codec !== undefined) Object.defineProperty(result, CODEC_KEY, codec);
35
58
  return result;
36
59
  }
37
- if (TypeGuard.IsArray(schema)) {
38
- schema.items = exactSchema(schema.items);
39
- if (TypeGuard.IsSchema(schema.contains)) schema.contains = exactSchema(schema.contains);
40
- } else if (TypeGuard.IsTuple(schema)) {
41
- if (schema.items) schema.items = schema.items.map(exactSchema);
42
- } else if (TypeGuard.IsUnion(schema)) {
43
- schema.anyOf = schema.anyOf.map(exactSchema);
44
- } else if (TypeGuard.IsIntersect(schema)) {
45
- schema.allOf = schema.allOf.map(exactSchema);
46
- if (TypeGuard.IsSchema(schema.unevaluatedProperties)) {
60
+ if (Type.IsArray(input)) {
61
+ schema.items = exactSchema(schema.items as TSchema);
62
+ if (typeof schema.contains === 'object' && Type.IsSchema(schema.contains)) {
63
+ schema.contains = exactSchema(schema.contains);
64
+ }
65
+ } else if (Type.IsTuple(input)) {
66
+ if (Array.isArray(schema.items)) schema.items = schema.items.map(exactSchema);
67
+ } else if (Type.IsUnion(input)) {
68
+ schema.anyOf = (schema.anyOf ?? []).map(exactSchema);
69
+ } else if (Type.IsIntersect(input)) {
70
+ schema.allOf = (schema.allOf ?? []).map(exactSchema);
71
+ if (typeof schema.unevaluatedProperties === 'object' && Type.IsSchema(schema.unevaluatedProperties)) {
47
72
  schema.unevaluatedProperties = exactSchema(schema.unevaluatedProperties);
48
73
  }
49
- } else if (TypeGuard.IsNot(schema)) {
50
- schema.not = exactSchema(schema.not);
51
- } else if (TypeGuard.IsRecord(schema)) {
74
+ } else if (Type.IsRecord(input)) {
52
75
  schema.patternProperties = Object.fromEntries(
53
- Object.entries(schema.patternProperties).map(([key, property]) => [key, exactSchema(property)]),
76
+ Object.entries(schema.patternProperties ?? {}).map(([key, property]) => [key, exactSchema(property)]),
54
77
  );
55
- if (TypeGuard.IsSchema(schema.additionalProperties)) {
78
+ if (typeof schema.additionalProperties === 'object' && Type.IsSchema(schema.additionalProperties)) {
56
79
  schema.additionalProperties = exactSchema(schema.additionalProperties);
57
80
  }
58
- } else if (TypeGuard.IsImport(schema)) {
81
+ } else if (Type.IsCyclic(input)) {
59
82
  schema.$defs = Object.fromEntries(
60
- Object.entries(schema.$defs).map(([key, definition]) => {
61
- if (!TypeGuard.IsSchema(definition)) throw new TypeError('Invalid imported schema definition');
62
- return [key, exactSchema(definition)];
63
- }),
83
+ Object.entries(schema.$defs ?? {}).map(([key, definition]) => [key, exactSchema(definition)]),
64
84
  );
65
85
  }
66
86
  return schema;
67
87
  }
68
88
 
89
+ /** TypeBox 1.x resolves references through a Context keyed by Ref name instead of a `references` array. */
90
+ function referenceContext(references: readonly TSchema[]): Record<string, TSchema> {
91
+ const context: Record<string, TSchema> = {};
92
+ for (const reference of references) {
93
+ const id = (reference as SchemaMeta).$id;
94
+ if (typeof id === 'string' && id.length > 0) context[id] = reference;
95
+ }
96
+ return context;
97
+ }
98
+
69
99
  export function createExactSchemaValidator<S extends TSchema>(schema: S, references: readonly TSchema[] = []) {
70
- const checked = exactSchema(CloneType(schema));
71
- const checkedReferences = references.map(reference => exactSchema(CloneType(reference)));
100
+ const checked = exactSchema(Value.Clone(schema));
101
+ const checkedReferences = references.map(reference => exactSchema(Value.Clone(reference)));
102
+ const context = referenceContext(checkedReferences);
103
+ const hasReferences = Object.keys(context).length > 0;
72
104
  return {
73
105
  Check(value: unknown): value is Static<S> {
74
- return Value.Check(checked, checkedReferences, value);
106
+ return hasReferences
107
+ ? Value.Check(context, checked, value)
108
+ : Value.Check(checked, value);
75
109
  },
76
110
  Errors(value: unknown) {
77
- return Value.Errors(checked, checkedReferences, value);
111
+ return hasReferences
112
+ ? Value.Errors(context, checked, value)
113
+ : Value.Errors(checked, value);
78
114
  },
79
115
  };
80
116
  }
81
117
 
118
+ /** One normalized validation issue with a field-scoped JSON Pointer. */
119
+ export interface SchemaIssue {
120
+ path: string;
121
+ message: string;
122
+ }
123
+
124
+ interface SchemaErrorLike {
125
+ instancePath?: string;
126
+ path?: string;
127
+ keyword?: string;
128
+ params?: Record<string, unknown>;
129
+ message: string;
130
+ }
131
+
132
+ function escapePointerSegment(segment: string): string {
133
+ return segment.replaceAll('~', '~0').replaceAll('/', '~1');
134
+ }
135
+
136
+ /**
137
+ * TypeBox 1.x reports Ajv-style errors that group missing properties into a single
138
+ * `required` issue. Normalize every error back into one field-scoped issue so callers
139
+ * can keep mapping messages onto individual form fields.
140
+ */
141
+ export function schemaIssues(errors: Iterable<SchemaErrorLike>): SchemaIssue[] {
142
+ const issues: SchemaIssue[] = [];
143
+ for (const error of errors) {
144
+ const base = error.instancePath ?? error.path ?? '';
145
+ const required = error.params?.['requiredProperties'];
146
+ if (error.keyword === 'required' && Array.isArray(required)) {
147
+ for (const property of required) {
148
+ if (typeof property !== 'string') continue;
149
+ issues.push({
150
+ path: `${base}/${escapePointerSegment(property)}`,
151
+ message: `must have required property '${property}'`,
152
+ });
153
+ }
154
+ continue;
155
+ }
156
+ const additional = error.params?.['additionalProperty'];
157
+ issues.push({
158
+ path: typeof additional === 'string' ? `${base}/${escapePointerSegment(additional)}` : base,
159
+ message: error.message,
160
+ });
161
+ }
162
+ return issues;
163
+ }
164
+
82
165
  const validators = new WeakMap<TSchema, ReturnType<typeof createExactSchemaValidator>>();
83
166
 
84
167
  /** Schemas are snapshotted on first use; later edits cannot weaken a live boundary. */
@@ -89,4 +172,4 @@ export function checkExact<S extends TSchema>(schema: S, value: unknown): value
89
172
  validators.set(schema, validator);
90
173
  }
91
174
  return validator.Check(value);
92
- }
175
+ }
@@ -1,4 +1,4 @@
1
- import { Type, type Static } from '@sinclair/typebox';
1
+ import { Type, type Static } from 'typebox';
2
2
  import { snapshotPlainData } from './plain-data';
3
3
  import { checkExact } from './schema-validation';
4
4
 
@@ -1,4 +1,4 @@
1
- import type { TObject, TSchema } from '@sinclair/typebox';
1
+ import type { TObject, TSchema } from 'typebox';
2
2
  import { hashKey, useQueryClient } from '@tanstack/svelte-query';
3
3
  import { captureAdminContext, type AdminContextAccessor } from './context.svelte';
4
4
  import { commandDefinition, prepareCommand, parseCommandInput, parseCommandResponse,
@@ -1,4 +1,4 @@
1
- import { Type, type Static } from '@sinclair/typebox';
1
+ import { Type, type Static } from 'typebox';
2
2
  import { checkExact } from './schema-validation';
3
3
  import { snapshotPlainData } from './plain-data';
4
4
 
@@ -1,4 +1,4 @@
1
- import { Type } from '@sinclair/typebox';
1
+ import { Type } from 'typebox';
2
2
  import { checkExact } from './schema-validation';
3
3
  import { snapshotPlainData } from './plain-data';
4
4
  import { TaskError, decodeTaskList, decodeTaskRecord, decodeTaskSubmitOptions, type SubmitTaskOptions } from './task-contract';
@@ -1,4 +1,4 @@
1
- import { Type } from '@sinclair/typebox';
1
+ import { Type } from 'typebox';
2
2
  import { snapshotPlainData } from './plain-data';
3
3
  import { checkExact } from './schema-validation';
4
4
  import { HttpError } from './types';
@@ -1,4 +1,4 @@
1
- import { Type } from '@sinclair/typebox';
1
+ import { Type } from 'typebox';
2
2
  import { snapshotPlainData } from './plain-data';
3
3
  import { checkExact } from './schema-validation';
4
4
  import { parseContractId, parseContractRecord, parseContractUpdateInput,
@@ -1,4 +1,4 @@
1
- import { Type } from '@sinclair/typebox';
1
+ import { Type } from 'typebox';
2
2
  import { useContractForm, type ContractFormOptions } from './contract-form.svelte';
3
3
  import { getContractFormFields, type ContractSchemas, type ContractFormValues } from './resource-contract';
4
4
  import { snapshotPlainData } from './plain-data';