@svadmin/surface 0.8.19 → 0.10.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 (102) hide show
  1. package/README.md +76 -38
  2. package/STYLING.md +33 -0
  3. package/compatibility.json +40 -8
  4. package/dist/agent-contract.d.ts +37 -0
  5. package/dist/agent-contract.js +206 -0
  6. package/dist/agent.d.ts +38 -7
  7. package/dist/agent.js +98 -59
  8. package/dist/binding.d.ts +2 -0
  9. package/dist/binding.js +7 -1
  10. package/dist/builtin-definitions.d.ts +6 -0
  11. package/dist/builtin-definitions.js +40 -0
  12. package/dist/builtin-schemas.d.ts +25 -0
  13. package/dist/builtin-schemas.js +13 -0
  14. package/dist/business-data.d.ts +16 -0
  15. package/dist/business-data.js +42 -0
  16. package/dist/business-definitions.d.ts +28 -0
  17. package/dist/business-definitions.js +53 -0
  18. package/dist/business.d.ts +3 -0
  19. package/dist/business.js +17 -0
  20. package/dist/catalog.d.ts +4 -61
  21. package/dist/catalog.js +20 -44
  22. package/dist/components/ActivityFeedWidget.svelte +34 -0
  23. package/dist/components/ActivityFeedWidget.svelte.d.ts +4 -0
  24. package/dist/components/MetricWidget.svelte +13 -8
  25. package/dist/components/MetricWidget.svelte.d.ts +1 -0
  26. package/dist/components/ResourceDetailWidget.svelte +56 -0
  27. package/dist/components/ResourceDetailWidget.svelte.d.ts +4 -0
  28. package/dist/components/ResourceFormBody.svelte +97 -0
  29. package/dist/components/ResourceFormBody.svelte.d.ts +13 -0
  30. package/dist/components/ResourceFormWidget.svelte +19 -0
  31. package/dist/components/ResourceFormWidget.svelte.d.ts +4 -0
  32. package/dist/components/ResourceTableWidget.svelte +15 -12
  33. package/dist/components/ResourceTableWidget.svelte.d.ts +1 -0
  34. package/dist/components/SurfaceEditPreview.svelte +107 -0
  35. package/dist/components/SurfaceEditPreview.svelte.d.ts +20 -0
  36. package/dist/components/SurfaceRenderer.svelte +121 -163
  37. package/dist/components/SurfaceRenderer.svelte.d.ts +5 -0
  38. package/dist/components/SurfaceWorkflowProvider.svelte +22 -0
  39. package/dist/components/SurfaceWorkflowProvider.svelte.d.ts +17 -0
  40. package/dist/components/SvarGridWidget.svelte +47 -0
  41. package/dist/components/SvarGridWidget.svelte.d.ts +4 -0
  42. package/dist/components/SvarSurfaceProvider.svelte +12 -0
  43. package/dist/components/SvarSurfaceProvider.svelte.d.ts +11 -0
  44. package/dist/design-contract.d.ts +2 -0
  45. package/dist/design-contract.js +2 -0
  46. package/dist/edits.d.ts +57 -0
  47. package/dist/edits.js +166 -0
  48. package/dist/index.d.ts +8 -2
  49. package/dist/index.js +5 -1
  50. package/dist/interactive.d.ts +6 -0
  51. package/dist/interactive.js +15 -0
  52. package/dist/localization.d.ts +11 -1
  53. package/dist/localization.js +20 -0
  54. package/dist/openui.d.ts +64 -0
  55. package/dist/openui.js +168 -0
  56. package/dist/recipes.d.ts +20 -0
  57. package/dist/recipes.js +20 -0
  58. package/dist/schema.d.ts +108 -0
  59. package/dist/schema.js +73 -0
  60. package/dist/server-sqlite.d.ts +1 -0
  61. package/dist/server-sqlite.js +2 -0
  62. package/dist/server.d.ts +4 -0
  63. package/dist/server.js +4 -0
  64. package/dist/source-cache.d.ts +30 -0
  65. package/dist/source-cache.js +135 -0
  66. package/dist/styles/editor.css +124 -0
  67. package/dist/styles/editor.d.ts +8 -0
  68. package/dist/styles/editor.js +9 -0
  69. package/dist/styles.css +43 -0
  70. package/dist/svar-catalog.d.ts +3 -0
  71. package/dist/svar-catalog.js +7 -0
  72. package/dist/svar-context.d.ts +9 -0
  73. package/dist/svar-context.js +1 -0
  74. package/dist/svar-schema.d.ts +21 -0
  75. package/dist/svar-schema.js +36 -0
  76. package/dist/svar.d.ts +4 -0
  77. package/dist/svar.js +3 -0
  78. package/dist/svelte.d.ts +4 -2
  79. package/dist/svelte.js +2 -1
  80. package/dist/types.d.ts +5 -1
  81. package/dist/validation.js +14 -108
  82. package/dist/wire.d.ts +13 -0
  83. package/dist/wire.js +30 -0
  84. package/dist/workflows/action-contracts.d.ts +7 -0
  85. package/dist/workflows/action-contracts.js +54 -0
  86. package/dist/workflows/catalog.d.ts +20 -0
  87. package/dist/workflows/catalog.js +50 -0
  88. package/dist/workflows/client.d.ts +45 -0
  89. package/dist/workflows/client.js +125 -0
  90. package/dist/workflows/context.d.ts +9 -0
  91. package/dist/workflows/context.js +5 -0
  92. package/dist/workflows/openui-guard.d.ts +19 -0
  93. package/dist/workflows/openui-guard.js +209 -0
  94. package/dist/workflows/service.d.ts +58 -0
  95. package/dist/workflows/service.js +296 -0
  96. package/dist/workflows/sqlite-store.d.ts +20 -0
  97. package/dist/workflows/sqlite-store.js +91 -0
  98. package/dist/workflows/types.d.ts +103 -0
  99. package/dist/workflows/types.js +8 -0
  100. package/dist/workflows.d.ts +5 -0
  101. package/dist/workflows.js +4 -0
  102. package/package.json +59 -8
package/dist/svar.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ export { default as SvarSurfaceProvider } from './components/SvarSurfaceProvider.svelte';
2
+ export { createSvarSurfaceCatalog } from './svar-catalog.js';
3
+ export { svarGridDefinition, svarGridPropsSchema } from './svar-schema.js';
4
+ export type { SvarGridProps } from './svar-schema.js';
package/dist/svar.js ADDED
@@ -0,0 +1,3 @@
1
+ export { default as SvarSurfaceProvider } from './components/SvarSurfaceProvider.svelte';
2
+ export { createSvarSurfaceCatalog } from './svar-catalog.js';
3
+ export { svarGridDefinition, svarGridPropsSchema } from './svar-schema.js';
package/dist/svelte.d.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  export { default as SurfaceRenderer } from './components/SurfaceRenderer.svelte';
2
- export type { SurfaceRendererError, SurfaceRendererProps, } from './components/SurfaceRenderer.svelte';
2
+ export type { SurfaceRendererError, SurfaceRendererProps } from './components/SurfaceRenderer.svelte';
3
+ export { default as SurfaceEditPreview } from './components/SurfaceEditPreview.svelte';
4
+ export type { SurfaceEditPreviewProps } from './components/SurfaceEditPreview.svelte';
3
5
  export type { SurfaceMessages } from './localization.js';
4
- export { DEFAULT_SURFACE_CATALOG_VERSION, defaultSurfaceCatalog, defineSurfaceCatalog, } from './catalog.js';
6
+ export { DEFAULT_SURFACE_CATALOG_VERSION, STYLED_SURFACE_CATALOG_VERSION, styledSurfaceCatalog, defaultSurfaceCatalog, defineSurfaceCatalog, } from './catalog.js';
5
7
  export type { SurfaceRenderCatalog, SurfaceWidgetRegistration, SurfaceWidgetRendererProps, } from './catalog.js';
package/dist/svelte.js CHANGED
@@ -1,2 +1,3 @@
1
1
  export { default as SurfaceRenderer } from './components/SurfaceRenderer.svelte';
2
- export { DEFAULT_SURFACE_CATALOG_VERSION, defaultSurfaceCatalog, defineSurfaceCatalog, } from './catalog.js';
2
+ export { default as SurfaceEditPreview } from './components/SurfaceEditPreview.svelte';
3
+ export { DEFAULT_SURFACE_CATALOG_VERSION, STYLED_SURFACE_CATALOG_VERSION, styledSurfaceCatalog, defaultSurfaceCatalog, defineSurfaceCatalog, } from './catalog.js';
package/dist/types.d.ts CHANGED
@@ -88,11 +88,15 @@ export interface SurfaceResourcePolicy {
88
88
  export interface SurfacePolicy {
89
89
  readonly resources: Readonly<Record<string, SurfaceResourcePolicy>>;
90
90
  }
91
- export type SurfaceCatalogDataKind = 'none' | 'scalar' | 'items';
91
+ export type SurfaceCatalogDataKind = 'none' | 'scalar' | 'items' | 'record';
92
92
  export interface SurfaceWidgetDefinition {
93
93
  readonly type: string;
94
94
  readonly dataKind: SurfaceCatalogDataKind;
95
95
  readonly propsSchema: TSchema;
96
+ /** 由可信目录提供,用于生成 AI 使用说明;不来自模型输出。 */
97
+ readonly description?: string;
98
+ /** 参数示例必须通过同一 propsSchema 校验。不要放入真实用户数据。 */
99
+ readonly examples?: readonly JsonObject[];
96
100
  /** Fields read from the bound record, supplied by a trusted Catalog. */
97
101
  readonly getReferencedFields?: (props: JsonObject) => readonly string[];
98
102
  }
@@ -1,111 +1,11 @@
1
- import { Type } from "@sinclair/typebox";
2
1
  import { TypeCompiler } from "@sinclair/typebox/compiler";
3
2
  import { Value } from "@sinclair/typebox/value";
4
3
  import { decodedJsonPointerToken, jsonPointer, jsonValueIssue } from "./json.js";
4
+ import { surfaceSpecSchema } from './schema.js';
5
5
  import { SURFACE_LIMITS, SURFACE_SCHEMA_VERSION, } from "./types.js";
6
- const idSchema = Type.String({
7
- minLength: 1,
8
- maxLength: SURFACE_LIMITS.maxIdLength,
9
- pattern: "^[A-Za-z][A-Za-z0-9_-]*$",
10
- });
11
- const fieldSchema = Type.String({
12
- minLength: 1,
13
- maxLength: SURFACE_LIMITS.maxIdLength,
14
- });
15
- const jsonPrimitiveSchema = Type.Union([
16
- Type.String(),
17
- Type.Number(),
18
- Type.Boolean(),
19
- Type.Null(),
20
- ]);
21
- const surfaceFilterSchema = Type.Union([
22
- Type.Object({
23
- field: fieldSchema,
24
- operator: Type.Union([
25
- Type.Literal("eq"),
26
- Type.Literal("ne"),
27
- Type.Literal("lt"),
28
- Type.Literal("lte"),
29
- Type.Literal("gt"),
30
- Type.Literal("gte"),
31
- Type.Literal("contains"),
32
- Type.Literal("startswith"),
33
- Type.Literal("endswith"),
34
- ]),
35
- value: jsonPrimitiveSchema,
36
- }, { additionalProperties: false }),
37
- Type.Object({
38
- field: fieldSchema,
39
- operator: Type.Union([
40
- Type.Literal("in"),
41
- Type.Literal("nin"),
42
- ]),
43
- value: Type.Array(jsonPrimitiveSchema),
44
- }, { additionalProperties: false }),
45
- Type.Object({
46
- field: fieldSchema,
47
- operator: Type.Union([
48
- Type.Literal("null"),
49
- Type.Literal("nnull"),
50
- ]),
51
- }, { additionalProperties: false }),
52
- ]);
53
- const resourceListSchema = Type.Object({
54
- id: idSchema,
55
- type: Type.Literal("resource-list"),
56
- resource: idSchema,
57
- pageSize: Type.Optional(Type.Integer({ minimum: 1 })),
58
- sorters: Type.Optional(Type.Array(Type.Object({
59
- field: fieldSchema,
60
- order: Type.Union([Type.Literal("asc"), Type.Literal("desc")]),
61
- }, { additionalProperties: false }))),
62
- filters: Type.Optional(Type.Array(surfaceFilterSchema)),
63
- }, { additionalProperties: false });
64
- const resourceOneSchema = Type.Object({
65
- id: idSchema,
66
- type: Type.Literal("resource-one"),
67
- resource: idSchema,
68
- recordId: Type.Union([Type.String(), Type.Number()]),
69
- }, { additionalProperties: false });
70
- const surfaceSpecSchema = Type.Object({
71
- schemaVersion: Type.String(),
72
- catalogVersion: Type.String({ minLength: 1 }),
73
- surfaceId: idSchema,
74
- title: Type.String({ minLength: 1, maxLength: SURFACE_LIMITS.maxTitleLength }),
75
- layout: Type.Object({
76
- type: Type.Literal("grid"),
77
- columns: Type.Literal(12),
78
- gap: Type.Optional(Type.Union([
79
- Type.Literal("sm"),
80
- Type.Literal("md"),
81
- Type.Literal("lg"),
82
- ])),
83
- }, { additionalProperties: false }),
84
- dataSources: Type.Array(Type.Union([resourceListSchema, resourceOneSchema])),
85
- widgets: Type.Array(Type.Object({
86
- id: idSchema,
87
- type: idSchema,
88
- props: Type.Record(Type.String(), Type.Unknown()),
89
- binding: Type.Optional(Type.Object({
90
- sourceId: idSchema,
91
- pointer: Type.Optional(Type.String()),
92
- }, { additionalProperties: false })),
93
- placement: Type.Optional(Type.Object({
94
- columnSpan: Type.Optional(Type.Integer({ minimum: 1, maximum: 12 })),
95
- }, { additionalProperties: false })),
96
- }, { additionalProperties: false })),
97
- }, { additionalProperties: false });
98
6
  const compiledSurfaceSpec = TypeCompiler.Compile(surfaceSpecSchema);
99
7
  const forbiddenPropertyNames = new Set([
100
- "class",
101
- "className",
102
- "color",
103
- "href",
104
- "html",
105
- "innerHTML",
106
- "src",
107
- "style",
108
- "url",
8
+ "class", "className", "color", "href", "html", "innerHTML", "src", "style", "url",
109
9
  ]);
110
10
  const strictSchemaProbeKey = "__surface_unknown_property_probe__";
111
11
  function invalidJsonIssue(pathSegments, message) {
@@ -197,10 +97,15 @@ function propsContainForbiddenKey(props) {
197
97
  }
198
98
  function referencedFieldIssue(widget, widgetIndex, definition, source, policy) {
199
99
  if (!definition.getReferencedFields)
200
- return null;
100
+ return definition.dataKind === 'record'
101
+ ? { code: 'invalid_widget_props', path: `/widgets/${widgetIndex}/props`, message: 'Record widgets require an explicit field selector', widgetId: widget.id }
102
+ : null;
201
103
  let referencedFields;
202
104
  try {
203
105
  referencedFields = definition.getReferencedFields(widget.props);
106
+ if (!Array.isArray(referencedFields) || (definition.dataKind === 'record' && referencedFields.length === 0)) {
107
+ throw new Error('Record fields must be explicit');
108
+ }
204
109
  }
205
110
  catch {
206
111
  return {
@@ -211,13 +116,13 @@ function referencedFieldIssue(widget, widgetIndex, definition, source, policy) {
211
116
  };
212
117
  }
213
118
  const readableFields = resourcePolicyFor(policy, source.resource)?.readFields ?? [];
214
- const deniedField = referencedFields.find((field) => (typeof field !== "string" || !readableFields.includes(field)));
215
- return deniedField === undefined
119
+ const deniedIndex = referencedFields.findIndex((field) => (typeof field !== "string" || !readableFields.includes(field)));
120
+ return deniedIndex === -1
216
121
  ? null
217
122
  : {
218
123
  code: "field_denied",
219
124
  path: `/widgets/${widgetIndex}/props`,
220
- message: `Field "${String(deniedField)}" is not readable`,
125
+ message: `Field "${String(referencedFields[deniedIndex])}" is not readable`,
221
126
  widgetId: widget.id,
222
127
  sourceId: source.id,
223
128
  };
@@ -286,8 +191,9 @@ function widgetIssue(widget, widgetIndex, definition, sources, policy) {
286
191
  const resourceOneField = source.type === "resource-one" ? resourceOnePointerField(pointer) : null;
287
192
  const pointerIsValid = definition.dataKind === "items"
288
193
  ? source.type === "resource-list" && pointer === "/items"
289
- : (source.type === "resource-list" && pointer === "/total")
290
- || resourceOneField !== null;
194
+ : definition.dataKind === 'record'
195
+ ? source.type === 'resource-one' && pointer === ''
196
+ : definition.dataKind === 'scalar' && ((source.type === "resource-list" && pointer === "/total") || resourceOneField !== null);
291
197
  if (!pointerIsValid) {
292
198
  return {
293
199
  code: "invalid_binding_pointer",
package/dist/wire.d.ts ADDED
@@ -0,0 +1,13 @@
1
+ import type { SurfaceValidationIssue } from './types.js';
2
+ export type SurfaceWireResult = {
3
+ readonly ok: true;
4
+ readonly value: unknown;
5
+ } | {
6
+ readonly ok: false;
7
+ readonly issues: readonly SurfaceValidationIssue[];
8
+ };
9
+ export declare function surfaceWireError(message: string, code?: 'invalid_json' | 'limit_exceeded'): Extract<SurfaceWireResult, {
10
+ ok: false;
11
+ }>;
12
+ /** 先检查大小和 JSON 安全性,再进入 TypeBox;返回独立副本,避免校验后被调用方改写。 */
13
+ export declare function parseSurfaceJson(input: unknown, legacyFences?: boolean): SurfaceWireResult;
package/dist/wire.js ADDED
@@ -0,0 +1,30 @@
1
+ import { jsonPointer, jsonValueIssue } from './json.js';
2
+ import { SURFACE_AGENT_LIMITS } from './agent-contract.js';
3
+ export function surfaceWireError(message, code = 'invalid_json') {
4
+ return { ok: false, issues: [{ code, path: '', message }] };
5
+ }
6
+ /** 先检查大小和 JSON 安全性,再进入 TypeBox;返回独立副本,避免校验后被调用方改写。 */
7
+ export function parseSurfaceJson(input, legacyFences = false) {
8
+ try {
9
+ let candidate = input;
10
+ if (typeof input === 'string') {
11
+ if (input.length > SURFACE_AGENT_LIMITS.maxInputCharacters)
12
+ return surfaceWireError('Surface input is too large', 'limit_exceeded');
13
+ const source = input.trim();
14
+ const fenced = legacyFences
15
+ ? /```(?:json)?\s*([\s\S]*?)```/iu.exec(source)
16
+ : /^```(?:json)?\s*([\s\S]*?)```$/iu.exec(source);
17
+ candidate = JSON.parse(fenced?.[1] ?? source);
18
+ }
19
+ const issue = jsonValueIssue(candidate);
20
+ if (issue)
21
+ return { ok: false, issues: [{ code: 'invalid_json', path: jsonPointer(issue.path), message: issue.message }] };
22
+ const serialized = JSON.stringify(candidate);
23
+ if (serialized.length > SURFACE_AGENT_LIMITS.maxInputCharacters)
24
+ return surfaceWireError('Surface input is too large', 'limit_exceeded');
25
+ return { ok: true, value: JSON.parse(serialized) };
26
+ }
27
+ catch {
28
+ return surfaceWireError('Surface input must be valid, safely inspectable JSON');
29
+ }
30
+ }
@@ -0,0 +1,7 @@
1
+ import type { SurfaceActionDescriptor, SurfaceRegisteredAction } from './types.js';
2
+ /** Only the form subset actually rendered by JsonSchemaForm is advertised.
3
+ * No refs, transforms, secret inputs, arbitrary unions or computed fields.
4
+ */
5
+ export declare function validateSurfaceActionDescriptor(action: SurfaceActionDescriptor): void;
6
+ /** One immutable schema drives the form and the server validation. */
7
+ export declare function defineSurfaceAction(action: Omit<SurfaceRegisteredAction, 'validateInput'>): SurfaceRegisteredAction;
@@ -0,0 +1,54 @@
1
+ import { Value } from '@sinclair/typebox/value';
2
+ import { surfaceSchemaToJson } from '../agent-contract.js';
3
+ /** Only the form subset actually rendered by JsonSchemaForm is advertised.
4
+ * No refs, transforms, secret inputs, arbitrary unions or computed fields.
5
+ */
6
+ export function validateSurfaceActionDescriptor(action) {
7
+ if (!/^[A-Za-z][A-Za-z0-9_.-]{0,63}$/u.test(action.id) || !action.version || action.version.length > 64
8
+ || !action.label.trim() || action.label.length > 120 || !['confirm', 'four-eyes'].includes(action.approval)) {
9
+ throw new Error('Invalid Surface action contract');
10
+ }
11
+ surfaceSchemaToJson(action.inputSchema);
12
+ let fields = 0;
13
+ function visit(schema, depth) {
14
+ if (++fields > 128 || depth > 8)
15
+ throw new Error('Surface form schema exceeds supported limits');
16
+ if (schema['anyOf'] || schema['oneOf'] || schema['allOf'] || schema['not'] || schema['if'] || schema.writeOnly
17
+ || !['object', 'array', 'string', 'number', 'integer', 'boolean'].includes(schema['type'])) {
18
+ throw new Error('Unsupported Surface form schema; use a registered custom widget instead');
19
+ }
20
+ if (schema['type'] === 'object') {
21
+ if (schema['additionalProperties'] !== false || !schema['properties'])
22
+ throw new Error('Surface actions require closed object schemas');
23
+ for (const [name, child] of Object.entries(schema['properties'])) {
24
+ if (!/^[A-Za-z][A-Za-z0-9_-]{0,63}$/u.test(name))
25
+ throw new Error('Invalid Surface form field');
26
+ visit(child, depth + 1);
27
+ }
28
+ }
29
+ else if (schema['type'] === 'array') {
30
+ if (!schema['items'] || Array.isArray(schema['items']))
31
+ throw new Error('Surface forms require homogeneous array items');
32
+ visit(schema['items'], depth + 1);
33
+ }
34
+ }
35
+ if (action.inputSchema['type'] !== 'object')
36
+ throw new Error('Surface actions require an object input schema');
37
+ visit(action.inputSchema, 0);
38
+ }
39
+ function snapshot(value) {
40
+ if (Array.isArray(value))
41
+ return Object.freeze(value.map(snapshot));
42
+ if (value && typeof value === 'object') {
43
+ return Object.freeze(Object.fromEntries(Reflect.ownKeys(value).map((key) => [key, snapshot(Reflect.get(value, key))])));
44
+ }
45
+ return value;
46
+ }
47
+ /** One immutable schema drives the form and the server validation. */
48
+ export function defineSurfaceAction(action) {
49
+ validateSurfaceActionDescriptor(action);
50
+ if (typeof action.authorize !== 'function' || typeof action.execute !== 'function')
51
+ throw new Error('Surface actions require authorization and execution handlers');
52
+ const inputSchema = snapshot(action.inputSchema);
53
+ return Object.freeze({ ...action, inputSchema, validateInput: (input) => Value.Check(inputSchema, input) });
54
+ }
@@ -0,0 +1,20 @@
1
+ import type { SurfaceCatalog, SurfaceWidgetDefinition, JsonObject } from '../types.js';
2
+ import type { SurfaceActionDescriptor } from './types.js';
3
+ export declare const surfaceAppearanceSchema: import("@sinclair/typebox").TObject<{
4
+ tone: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<import("@sinclair/typebox").TLiteral<"info" | "warning" | "success" | "neutral" | "danger">[]>>;
5
+ density: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<import("@sinclair/typebox").TLiteral<"compact" | "comfortable">[]>>;
6
+ }>;
7
+ export declare function withoutSurfaceAppearance(props: JsonObject): JsonObject;
8
+ /** Adds frame-level semantic appearance to every registered widget without
9
+ * leaking presentation props into its existing component implementation.
10
+ * Internal table cell density remains the table's own semantic prop.
11
+ */
12
+ export declare function withSurfaceAppearance<T extends SurfaceWidgetDefinition>(catalog: {
13
+ version: string;
14
+ widgets: readonly T[];
15
+ }): {
16
+ version: string;
17
+ widgets: T[];
18
+ };
19
+ /** DOM-free definitions reused by server validation, prompts and Svelte catalog. */
20
+ export declare function createInteractiveSurfaceDefinitions(actions: readonly SurfaceActionDescriptor[], base: SurfaceCatalog): SurfaceCatalog;
@@ -0,0 +1,50 @@
1
+ import { Type } from '@sinclair/typebox';
2
+ import { surfaceDesignContract } from '../design-contract.js';
3
+ import { validateSurfaceActionDescriptor } from './action-contracts.js';
4
+ export const surfaceAppearanceSchema = Type.Object({
5
+ tone: Type.Optional(Type.Union(surfaceDesignContract.metric.tone.map((tone) => Type.Literal(tone)))),
6
+ density: Type.Optional(Type.Union(surfaceDesignContract.metric.density.map((density) => Type.Literal(density)))),
7
+ }, { additionalProperties: false });
8
+ export function withoutSurfaceAppearance(props) {
9
+ return Object.fromEntries(Object.entries(props).filter(([key]) => key !== 'appearance'));
10
+ }
11
+ function withAppearance(schema) {
12
+ if (Array.isArray(schema['anyOf']))
13
+ return { ...schema, anyOf: schema['anyOf'].map((part) => withAppearance(part)) };
14
+ if (schema['type'] !== 'object' || schema['additionalProperties'] !== false || !schema['properties'] || 'appearance' in schema['properties']) {
15
+ throw new Error('Surface appearance requires closed object schemas without a reserved appearance property');
16
+ }
17
+ return { ...schema, properties: { ...schema['properties'], appearance: Type.Optional(surfaceAppearanceSchema) } };
18
+ }
19
+ /** Adds frame-level semantic appearance to every registered widget without
20
+ * leaking presentation props into its existing component implementation.
21
+ * Internal table cell density remains the table's own semantic prop.
22
+ */
23
+ export function withSurfaceAppearance(catalog) {
24
+ return { version: `${catalog.version}+appearance/v1`, widgets: catalog.widgets.map((widget) => ({
25
+ ...widget, presentation: 'surface-appearance/v1', propsSchema: withAppearance(widget.propsSchema),
26
+ ...(widget.getReferencedFields ? { getReferencedFields: (props) => widget.getReferencedFields?.(withoutSurfaceAppearance(props)) ?? [] } : {}),
27
+ })) };
28
+ }
29
+ /** DOM-free definitions reused by server validation, prompts and Svelte catalog. */
30
+ export function createInteractiveSurfaceDefinitions(actions, base) {
31
+ const firstAction = actions[0];
32
+ if (!firstAction || actions.length > 32)
33
+ throw new Error('Register between one and 32 Surface actions');
34
+ if (new Set(actions.map((action) => action.id)).size !== actions.length)
35
+ throw new Error('Duplicate Surface action');
36
+ if (base.widgets.some((widget) => widget.type === 'resource-form'))
37
+ throw new Error('resource-form is reserved by the interactive catalog');
38
+ for (const action of actions)
39
+ validateSurfaceActionDescriptor(action);
40
+ const actionIds = actions.map((action) => Type.Literal(action.id, { description: `${action.label}; version ${action.version}; approval ${action.approval}` }));
41
+ return withSurfaceAppearance({ version: `${base.version}+forms/v1`, widgets: [...base.widgets, {
42
+ type: 'resource-form', dataKind: 'none',
43
+ description: 'Nested resource form backed by a trusted action input schema. Only a user submission can create a proposal. Approval and execution are separate server-authorized steps.',
44
+ propsSchema: Type.Object({
45
+ actionId: Type.Union(actionIds),
46
+ title: Type.Optional(Type.String({ minLength: 1, maxLength: 120 })),
47
+ }, { additionalProperties: false }),
48
+ examples: [{ actionId: firstAction.id }],
49
+ }] });
50
+ }
@@ -0,0 +1,45 @@
1
+ import type { JsonObject } from '../types.js';
2
+ import type { SurfaceActionProposal, SurfaceActionDescriptor } from './types.js';
3
+ /** HTTP bindings are trusted host code. Authenticate and apply CSRF/origin checks
4
+ * on every server route. Never expose service identity as a model-editable field.
5
+ */
6
+ export interface SurfaceWorkflowTransport {
7
+ propose(input: {
8
+ actionId: string;
9
+ args: JsonObject;
10
+ surfaceId: string;
11
+ surfaceRevision: number;
12
+ requestKey: string;
13
+ }, signal: AbortSignal): Promise<SurfaceActionProposal>;
14
+ inspect(id: string, signal: AbortSignal): Promise<SurfaceActionProposal>;
15
+ approve(id: string, digest: string, signal: AbortSignal): Promise<SurfaceActionProposal>;
16
+ reject(id: string, digest: string, signal: AbortSignal): Promise<SurfaceActionProposal>;
17
+ execute(id: string, digest: string, signal: AbortSignal): Promise<SurfaceActionProposal>;
18
+ }
19
+ export interface SurfaceWorkflowClientScope {
20
+ readonly scopeKey: string;
21
+ readonly surfaceId: string;
22
+ readonly revision: number;
23
+ readonly enabled: boolean;
24
+ readonly transport: SurfaceWorkflowTransport;
25
+ }
26
+ export type SurfaceWorkflowClientState = {
27
+ readonly busy: boolean;
28
+ readonly proposal?: SurfaceActionProposal;
29
+ readonly error?: string;
30
+ };
31
+ /** Independent of Svelte so races and late responses can be tested directly. */
32
+ export declare function createSurfaceFormController(options: {
33
+ action: SurfaceActionDescriptor;
34
+ getScope: () => SurfaceWorkflowClientScope;
35
+ onState: (state: SurfaceWorkflowClientState) => void;
36
+ onCommitted?: (proposal: SurfaceActionProposal) => void;
37
+ }): {
38
+ submit(args: JsonObject): Promise<void>;
39
+ confirm(): Promise<void>;
40
+ execute(): Promise<void>;
41
+ refresh(): Promise<void>;
42
+ reject(): Promise<void>;
43
+ reset: () => void;
44
+ dispose(): void;
45
+ };
@@ -0,0 +1,125 @@
1
+ /** Independent of Svelte so races and late responses can be tested directly. */
2
+ export function createSurfaceFormController(options) {
3
+ let state = { busy: false };
4
+ let generation = 0;
5
+ let abort = new AbortController();
6
+ let disposed = false;
7
+ let notified;
8
+ let draft;
9
+ const action = options.action;
10
+ let activeScope = { ...options.getScope() };
11
+ function publish(next) { state = next; options.onState(next); }
12
+ function same(a, b) {
13
+ return a.scopeKey === b.scopeKey && a.surfaceId === b.surfaceId && a.revision === b.revision && a.transport === b.transport && a.enabled === b.enabled;
14
+ }
15
+ function reset() {
16
+ if (disposed)
17
+ return;
18
+ generation += 1;
19
+ draft = undefined;
20
+ abort.abort();
21
+ abort = new AbortController();
22
+ activeScope = { ...options.getScope() };
23
+ publish({ busy: false });
24
+ }
25
+ function syncScope() {
26
+ const next = { ...options.getScope() };
27
+ if (!same(activeScope, next))
28
+ reset();
29
+ return next;
30
+ }
31
+ async function run(work) {
32
+ const scope = syncScope();
33
+ if (disposed || state.busy || !scope.enabled)
34
+ return;
35
+ const ticket = ++generation;
36
+ publish({ busy: true, ...(state.proposal ? { proposal: state.proposal } : {}) });
37
+ try {
38
+ // 宿主状态回调可能同步 reset/dispose;不得继续派发已失效请求。
39
+ if (disposed || ticket !== generation)
40
+ return;
41
+ if (!same(scope, options.getScope())) {
42
+ reset();
43
+ return;
44
+ }
45
+ const proposal = await work(scope, abort.signal);
46
+ if (disposed || ticket !== generation)
47
+ return;
48
+ if (!same(scope, options.getScope())) {
49
+ reset();
50
+ return;
51
+ }
52
+ if (proposal.actionId !== action.id || proposal.actionVersion !== action.version || proposal.surfaceId !== scope.surfaceId || proposal.surfaceRevision !== scope.revision) {
53
+ throw new Error('Proposal does not belong to the active form');
54
+ }
55
+ publish({ busy: false, proposal });
56
+ // 成功状态发布也可能切换会话或启动新请求,旧完成回调必须失效。
57
+ if (disposed || ticket !== generation)
58
+ return;
59
+ if (!same(scope, options.getScope())) {
60
+ reset();
61
+ return;
62
+ }
63
+ if (proposal.status === 'succeeded' && proposal.id !== notified) {
64
+ notified = proposal.id;
65
+ try {
66
+ options.onCommitted?.(proposal);
67
+ }
68
+ catch { /* A host refresh failure must not misreport a successful write. */ }
69
+ }
70
+ }
71
+ catch {
72
+ if (disposed || ticket !== generation)
73
+ return;
74
+ if (!same(scope, options.getScope())) {
75
+ reset();
76
+ return;
77
+ }
78
+ publish({ ...state, busy: false, error: 'Operation failed. Check status before retrying.' });
79
+ }
80
+ }
81
+ return {
82
+ submit(args) {
83
+ syncScope();
84
+ if (disposed || state.busy || state.proposal || !options.getScope().enabled)
85
+ return Promise.resolve();
86
+ const captured = structuredClone(args);
87
+ const serialized = JSON.stringify(captured);
88
+ if (draft?.serialized !== serialized)
89
+ draft = { serialized, requestKey: crypto.randomUUID() };
90
+ const requestKey = draft.requestKey;
91
+ return run((scope, signal) => scope.transport.propose({ actionId: action.id, args: captured,
92
+ surfaceId: scope.surfaceId, surfaceRevision: scope.revision, requestKey }, signal));
93
+ },
94
+ confirm() {
95
+ syncScope();
96
+ const p = state.proposal;
97
+ if (!p || p.status !== 'pending' || p.approval !== 'confirm')
98
+ return Promise.resolve();
99
+ return run((scope, signal) => scope.transport.approve(p.id, p.digest, signal));
100
+ },
101
+ execute() {
102
+ syncScope();
103
+ const p = state.proposal;
104
+ if (!p || p.status !== 'approved')
105
+ return Promise.resolve();
106
+ return run((scope, signal) => scope.transport.execute(p.id, p.digest, signal));
107
+ },
108
+ refresh() {
109
+ syncScope();
110
+ const p = state.proposal;
111
+ if (!p)
112
+ return Promise.resolve();
113
+ return run((scope, signal) => scope.transport.inspect(p.id, signal));
114
+ },
115
+ reject() {
116
+ syncScope();
117
+ const p = state.proposal;
118
+ if (!p || !['pending', 'approved'].includes(p.status))
119
+ return Promise.resolve();
120
+ return run((scope, signal) => scope.transport.reject(p.id, p.digest, signal));
121
+ },
122
+ reset,
123
+ dispose() { disposed = true; generation += 1; abort.abort(); },
124
+ };
125
+ }
@@ -0,0 +1,9 @@
1
+ import type { SurfaceActionDescriptor, SurfaceActionProposal } from './types.js';
2
+ import type { SurfaceWorkflowClientScope } from './client.js';
3
+ export declare const SURFACE_WORKFLOW_CONTEXT: unique symbol;
4
+ export interface SurfaceWorkflowHost {
5
+ getScope(): SurfaceWorkflowClientScope;
6
+ getAction(id: string): SurfaceActionDescriptor | undefined;
7
+ committed(proposal: SurfaceActionProposal): void;
8
+ }
9
+ export declare function getSurfaceWorkflowHost(): SurfaceWorkflowHost | undefined;
@@ -0,0 +1,5 @@
1
+ import { getContext } from 'svelte';
2
+ export const SURFACE_WORKFLOW_CONTEXT = Symbol('svadmin.surface.workflow');
3
+ export function getSurfaceWorkflowHost() {
4
+ return getContext(SURFACE_WORKFLOW_CONTEXT);
5
+ }
@@ -0,0 +1,19 @@
1
+ /** Strict, bounded framing for the static OpenUI subset consumed by Surface.
2
+ * This is NOT a replacement for the upstream parser. It prevents unsupported
3
+ * expressions from reaching that parser and only releases complete statements.
4
+ */
5
+ export declare const OPENUI_LIMITS: Readonly<{
6
+ characters: 131072;
7
+ statementCharacters: 32768;
8
+ statements: 128;
9
+ depth: 32;
10
+ tokens: 12000;
11
+ }>;
12
+ export declare class SurfaceOpenUIError extends Error {
13
+ readonly code: 'syntax' | 'limit' | 'unsupported' | 'invalid_surface';
14
+ constructor(code: 'syntax' | 'limit' | 'unsupported' | 'invalid_surface', message: string);
15
+ }
16
+ export declare function createOpenUIStatementGuard(callNames: readonly string[]): {
17
+ push(chunk: string): readonly string[];
18
+ finish(): readonly string[];
19
+ };