@svadmin/surface 0.8.19 → 0.9.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 (191) hide show
  1. package/README.md +70 -38
  2. package/STYLING.md +31 -0
  3. package/compatibility.json +23 -2
  4. package/dist/agent-contract.d.ts +37 -0
  5. package/dist/agent-contract.js +203 -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/catalog.d.ts +4 -61
  15. package/dist/catalog.js +20 -44
  16. package/dist/components/MetricWidget.svelte +12 -8
  17. package/dist/components/ResourceFormBody.svelte +97 -0
  18. package/dist/components/ResourceFormBody.svelte.d.ts +13 -0
  19. package/dist/components/ResourceFormWidget.svelte +19 -0
  20. package/dist/components/ResourceFormWidget.svelte.d.ts +4 -0
  21. package/dist/components/ResourceTableWidget.svelte +14 -12
  22. package/dist/components/SurfaceEditPreview.svelte +107 -0
  23. package/dist/components/SurfaceEditPreview.svelte.d.ts +20 -0
  24. package/dist/components/SurfaceRenderer.svelte +120 -163
  25. package/dist/components/SurfaceRenderer.svelte.d.ts +4 -0
  26. package/dist/components/SurfaceWorkflowProvider.svelte +22 -0
  27. package/dist/components/SurfaceWorkflowProvider.svelte.d.ts +17 -0
  28. package/dist/components/SvarGridWidget.svelte +47 -0
  29. package/dist/components/SvarGridWidget.svelte.d.ts +4 -0
  30. package/dist/components/SvarSurfaceProvider.svelte +12 -0
  31. package/dist/components/SvarSurfaceProvider.svelte.d.ts +11 -0
  32. package/dist/edits.d.ts +57 -0
  33. package/dist/edits.js +166 -0
  34. package/dist/index.d.ts +8 -2
  35. package/dist/index.js +5 -1
  36. package/dist/interactive.d.ts +6 -0
  37. package/dist/interactive.js +15 -0
  38. package/dist/openui.d.ts +64 -0
  39. package/dist/openui.js +168 -0
  40. package/dist/schema.d.ts +108 -0
  41. package/dist/schema.js +73 -0
  42. package/dist/server-sqlite.d.ts +1 -0
  43. package/dist/server-sqlite.js +2 -0
  44. package/dist/server.d.ts +4 -0
  45. package/dist/server.js +4 -0
  46. package/dist/source-cache.d.ts +30 -0
  47. package/dist/source-cache.js +135 -0
  48. package/dist/styled-system/css/conditions.js +36 -0
  49. package/dist/styled-system/css/css.d.ts +22 -0
  50. package/dist/styled-system/css/css.js +45 -0
  51. package/dist/styled-system/css/cva.d.ts +6 -0
  52. package/dist/styled-system/css/cva.js +87 -0
  53. package/dist/styled-system/css/cx.d.ts +5 -0
  54. package/dist/styled-system/css/cx.js +15 -0
  55. package/dist/styled-system/css/index.d.ts +5 -0
  56. package/dist/styled-system/css/index.js +4 -0
  57. package/dist/styled-system/css/sva.d.ts +4 -0
  58. package/dist/styled-system/css/sva.js +46 -0
  59. package/dist/styled-system/design-contract.d.ts +13 -0
  60. package/dist/styled-system/design-contract.js +11 -0
  61. package/dist/styled-system/helpers.js +316 -0
  62. package/dist/styled-system/patterns/aspect-ratio.d.ts +20 -0
  63. package/dist/styled-system/patterns/aspect-ratio.js +38 -0
  64. package/dist/styled-system/patterns/bleed.d.ts +21 -0
  65. package/dist/styled-system/patterns/bleed.js +24 -0
  66. package/dist/styled-system/patterns/box.d.ts +20 -0
  67. package/dist/styled-system/patterns/box.js +15 -0
  68. package/dist/styled-system/patterns/center.d.ts +20 -0
  69. package/dist/styled-system/patterns/center.js +21 -0
  70. package/dist/styled-system/patterns/circle.d.ts +20 -0
  71. package/dist/styled-system/patterns/circle.js +25 -0
  72. package/dist/styled-system/patterns/container.d.ts +20 -0
  73. package/dist/styled-system/patterns/container.js +21 -0
  74. package/dist/styled-system/patterns/cq.d.ts +21 -0
  75. package/dist/styled-system/patterns/cq.js +21 -0
  76. package/dist/styled-system/patterns/divider.d.ts +22 -0
  77. package/dist/styled-system/patterns/divider.js +25 -0
  78. package/dist/styled-system/patterns/flex.d.ts +26 -0
  79. package/dist/styled-system/patterns/flex.js +26 -0
  80. package/dist/styled-system/patterns/float.d.ts +23 -0
  81. package/dist/styled-system/patterns/float.js +52 -0
  82. package/dist/styled-system/patterns/grid-item.d.ts +25 -0
  83. package/dist/styled-system/patterns/grid-item.js +25 -0
  84. package/dist/styled-system/patterns/grid.d.ts +24 -0
  85. package/dist/styled-system/patterns/grid.js +27 -0
  86. package/dist/styled-system/patterns/hstack.d.ts +21 -0
  87. package/dist/styled-system/patterns/hstack.js +24 -0
  88. package/dist/styled-system/patterns/index.d.ts +21 -0
  89. package/dist/styled-system/patterns/index.js +20 -0
  90. package/dist/styled-system/patterns/link-overlay.d.ts +20 -0
  91. package/dist/styled-system/patterns/link-overlay.js +24 -0
  92. package/dist/styled-system/patterns/spacer.d.ts +20 -0
  93. package/dist/styled-system/patterns/spacer.js +25 -0
  94. package/dist/styled-system/patterns/square.d.ts +20 -0
  95. package/dist/styled-system/patterns/square.js +24 -0
  96. package/dist/styled-system/patterns/stack.d.ts +23 -0
  97. package/dist/styled-system/patterns/stack.js +24 -0
  98. package/dist/styled-system/patterns/visually-hidden.d.ts +20 -0
  99. package/dist/styled-system/patterns/visually-hidden.js +18 -0
  100. package/dist/styled-system/patterns/vstack.d.ts +21 -0
  101. package/dist/styled-system/patterns/vstack.js +24 -0
  102. package/dist/styled-system/patterns/wrap.d.ts +24 -0
  103. package/dist/styled-system/patterns/wrap.js +25 -0
  104. package/dist/styled-system/recipes/content-header.d.ts +31 -0
  105. package/dist/styled-system/recipes/content-header.js +65 -0
  106. package/dist/styled-system/recipes/content-page.d.ts +34 -0
  107. package/dist/styled-system/recipes/content-page.js +43 -0
  108. package/dist/styled-system/recipes/create-recipe.js +82 -0
  109. package/dist/styled-system/recipes/index.d.ts +17 -0
  110. package/dist/styled-system/recipes/index.js +16 -0
  111. package/dist/styled-system/recipes/metric-block.d.ts +34 -0
  112. package/dist/styled-system/recipes/metric-block.js +76 -0
  113. package/dist/styled-system/recipes/product-list.d.ts +31 -0
  114. package/dist/styled-system/recipes/product-list.js +73 -0
  115. package/dist/styled-system/recipes/product-section.d.ts +31 -0
  116. package/dist/styled-system/recipes/product-section.js +49 -0
  117. package/dist/styled-system/recipes/product-settings-row.d.ts +34 -0
  118. package/dist/styled-system/recipes/product-settings-row.js +58 -0
  119. package/dist/styled-system/recipes/product-settings.d.ts +31 -0
  120. package/dist/styled-system/recipes/product-settings.js +57 -0
  121. package/dist/styled-system/recipes/product-status.d.ts +34 -0
  122. package/dist/styled-system/recipes/product-status.js +45 -0
  123. package/dist/styled-system/recipes/product-toolbar.d.ts +31 -0
  124. package/dist/styled-system/recipes/product-toolbar.js +41 -0
  125. package/dist/styled-system/recipes/product-workspace.d.ts +34 -0
  126. package/dist/styled-system/recipes/product-workspace.js +58 -0
  127. package/dist/styled-system/recipes/surface-metric.d.ts +38 -0
  128. package/dist/styled-system/recipes/surface-metric.js +63 -0
  129. package/dist/styled-system/recipes/surface-table.d.ts +34 -0
  130. package/dist/styled-system/recipes/surface-table.js +62 -0
  131. package/dist/styled-system/recipes/ui-badge.d.ts +31 -0
  132. package/dist/styled-system/recipes/ui-badge.js +38 -0
  133. package/dist/styled-system/recipes/ui-button.d.ts +32 -0
  134. package/dist/styled-system/recipes/ui-button.js +43 -0
  135. package/dist/styled-system/recipes/ui-input.d.ts +31 -0
  136. package/dist/styled-system/recipes/ui-input.js +49 -0
  137. package/dist/styled-system/recipes/ui-textarea.d.ts +31 -0
  138. package/dist/styled-system/recipes/ui-textarea.js +24 -0
  139. package/dist/styled-system/styles.css +1257 -0
  140. package/dist/styled-system/tokens/index.d.ts +9 -0
  141. package/dist/styled-system/tokens/index.js +268 -0
  142. package/dist/styled-system/tokens/tokens.d.ts +36 -0
  143. package/dist/styled-system/types/composition.d.ts +227 -0
  144. package/dist/styled-system/types/conditions.d.ts +236 -0
  145. package/dist/styled-system/types/csstype.d.ts +22570 -0
  146. package/dist/styled-system/types/index.d.ts +6 -0
  147. package/dist/styled-system/types/parts.d.ts +8 -0
  148. package/dist/styled-system/types/pattern.d.ts +78 -0
  149. package/dist/styled-system/types/prop-type.d.ts +223 -0
  150. package/dist/styled-system/types/recipe.d.ts +181 -0
  151. package/dist/styled-system/types/selectors.d.ts +59 -0
  152. package/dist/styled-system/types/static-css.d.ts +56 -0
  153. package/dist/styled-system/types/style-props.d.ts +8088 -0
  154. package/dist/styled-system/types/system-types.d.ts +151 -0
  155. package/dist/styles/editor.css +124 -0
  156. package/dist/styles/editor.generated.d.ts +8 -0
  157. package/dist/styles/editor.generated.js +9 -0
  158. package/dist/styles.css +1262 -0
  159. package/dist/svar-catalog.d.ts +3 -0
  160. package/dist/svar-catalog.js +7 -0
  161. package/dist/svar-context.d.ts +9 -0
  162. package/dist/svar-context.js +1 -0
  163. package/dist/svar-schema.d.ts +21 -0
  164. package/dist/svar-schema.js +36 -0
  165. package/dist/svar.d.ts +4 -0
  166. package/dist/svar.js +3 -0
  167. package/dist/svelte.d.ts +4 -2
  168. package/dist/svelte.js +2 -1
  169. package/dist/types.d.ts +4 -0
  170. package/dist/validation.js +2 -102
  171. package/dist/wire.d.ts +13 -0
  172. package/dist/wire.js +30 -0
  173. package/dist/workflows/action-contracts.d.ts +7 -0
  174. package/dist/workflows/action-contracts.js +54 -0
  175. package/dist/workflows/catalog.d.ts +20 -0
  176. package/dist/workflows/catalog.js +50 -0
  177. package/dist/workflows/client.d.ts +45 -0
  178. package/dist/workflows/client.js +125 -0
  179. package/dist/workflows/context.d.ts +9 -0
  180. package/dist/workflows/context.js +5 -0
  181. package/dist/workflows/openui-guard.d.ts +19 -0
  182. package/dist/workflows/openui-guard.js +209 -0
  183. package/dist/workflows/service.d.ts +58 -0
  184. package/dist/workflows/service.js +296 -0
  185. package/dist/workflows/sqlite-store.d.ts +20 -0
  186. package/dist/workflows/sqlite-store.js +91 -0
  187. package/dist/workflows/types.d.ts +103 -0
  188. package/dist/workflows/types.js +8 -0
  189. package/dist/workflows.d.ts +5 -0
  190. package/dist/workflows.js +4 -0
  191. package/package.json +52 -8
@@ -0,0 +1,209 @@
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 const OPENUI_LIMITS = Object.freeze({ characters: 131_072, statementCharacters: 32_768, statements: 128, depth: 32, tokens: 12_000 });
6
+ export class SurfaceOpenUIError extends Error {
7
+ code;
8
+ constructor(code, message) {
9
+ super(message);
10
+ this.code = code;
11
+ this.name = 'SurfaceOpenUIError';
12
+ }
13
+ }
14
+ const identifier = /^[A-Za-z_][A-Za-z0-9_]*$/u;
15
+ const forbidden = new Set(['__proto__', 'prototype', 'constructor']);
16
+ function checkStatement(text, calls) {
17
+ const tokens = [];
18
+ // JSON.parse rejects unescaped control characters in string tokens below.
19
+ const lexer = /\s+|"(?:[^"\\]|\\(?:["\\/bfnrt]|u[0-9a-fA-F]{4}))*"|-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?|[A-Za-z_][A-Za-z0-9_]*|[=(),[\]{}:]/uy;
20
+ let offset = 0;
21
+ while (offset < text.length) {
22
+ lexer.lastIndex = offset;
23
+ const token = lexer.exec(text);
24
+ if (!token)
25
+ throw new SurfaceOpenUIError('syntax', 'Only static OpenUI declarations and JSON literals are supported');
26
+ offset = lexer.lastIndex;
27
+ if (token[0].trim())
28
+ tokens.push(token[0]);
29
+ if (tokens.length > OPENUI_LIMITS.tokens)
30
+ throw new SurfaceOpenUIError('limit', 'Too many tokens in a declaration');
31
+ }
32
+ const topCall = tokens[2];
33
+ if (!topCall || !calls.has(topCall) || tokens[3] !== '(')
34
+ throw new SurfaceOpenUIError('unsupported', 'Declarations must be registered component calls');
35
+ if ((tokens[0] === 'root') !== (topCall === 'Surface'))
36
+ throw new SurfaceOpenUIError('unsupported', 'Only root may declare Surface');
37
+ const references = new Set();
38
+ let at = 0;
39
+ function take(value) {
40
+ if (tokens[at++] !== value)
41
+ throw new SurfaceOpenUIError('syntax', `Expected ${value}`);
42
+ }
43
+ function value(depth) {
44
+ if (depth > OPENUI_LIMITS.depth)
45
+ throw new SurfaceOpenUIError('limit', 'OpenUI nesting limit exceeded');
46
+ const token = tokens[at++];
47
+ if (token === undefined)
48
+ throw new SurfaceOpenUIError('syntax', 'Missing expression');
49
+ if (token.startsWith('"') || /^(?:-?[0-9]|true$|false$|null$)/u.test(token)) {
50
+ const literal = JSON.parse(token);
51
+ if (typeof literal === 'number' && !Number.isFinite(literal))
52
+ throw new SurfaceOpenUIError('syntax', 'Numbers must be finite');
53
+ return;
54
+ }
55
+ if (token === '[' || token === '{') {
56
+ const end = token === '[' ? ']' : '}';
57
+ const keys = new Set();
58
+ if (tokens[at] !== end) {
59
+ for (;;) {
60
+ if (token === '{') {
61
+ const keyToken = tokens[at++];
62
+ if (!keyToken?.startsWith('"'))
63
+ throw new SurfaceOpenUIError('syntax', 'Object keys must be quoted JSON strings');
64
+ const key = JSON.parse(keyToken);
65
+ if (forbidden.has(key) || keys.has(key))
66
+ throw new SurfaceOpenUIError('syntax', 'Duplicate or unsafe object key');
67
+ keys.add(key);
68
+ take(':');
69
+ }
70
+ value(depth + 1);
71
+ if (tokens[at] !== ',')
72
+ break;
73
+ at += 1;
74
+ }
75
+ }
76
+ take(end);
77
+ return;
78
+ }
79
+ if (!identifier.test(token) || forbidden.has(token))
80
+ throw new SurfaceOpenUIError('syntax', 'Invalid reference');
81
+ if (tokens[at] === '(') {
82
+ if (depth !== 0)
83
+ throw new SurfaceOpenUIError('unsupported', 'Use named declarations, not nested calls');
84
+ if (!calls.has(token))
85
+ throw new SurfaceOpenUIError('unsupported', `Unregistered OpenUI call: ${token}`);
86
+ at += 1;
87
+ if (tokens[at] !== ')') {
88
+ for (;;) {
89
+ value(depth + 1);
90
+ if (tokens[at] !== ',')
91
+ break;
92
+ at += 1;
93
+ }
94
+ }
95
+ take(')');
96
+ }
97
+ else {
98
+ if (topCall !== 'Surface' || token === 'root' || references.has(token))
99
+ throw new SurfaceOpenUIError('unsupported', 'References are only allowed once in the Surface root');
100
+ references.add(token);
101
+ if (references.size > 32)
102
+ throw new SurfaceOpenUIError('limit', 'Too many root references');
103
+ }
104
+ }
105
+ const name = tokens[at++];
106
+ if (!name || !identifier.test(name) || forbidden.has(name) || name.length > 64)
107
+ throw new SurfaceOpenUIError('syntax', 'Invalid declaration name');
108
+ take('=');
109
+ value(0);
110
+ if (at !== tokens.length)
111
+ throw new SurfaceOpenUIError('syntax', 'Unexpected tokens after declaration');
112
+ return name;
113
+ }
114
+ export function createOpenUIStatementGuard(callNames) {
115
+ const calls = new Set(callNames);
116
+ const names = new Set();
117
+ let pending = '';
118
+ let characters = 0;
119
+ const stack = [];
120
+ let inString = false;
121
+ let escaped = false;
122
+ let closed = false;
123
+ let failure;
124
+ function release() {
125
+ const statement = pending.trim();
126
+ pending = '';
127
+ if (!statement)
128
+ return undefined;
129
+ const name = checkStatement(statement, calls);
130
+ if (names.has(name))
131
+ throw new SurfaceOpenUIError('syntax', `Duplicate declaration: ${name}`);
132
+ names.add(name);
133
+ if (names.size > OPENUI_LIMITS.statements)
134
+ throw new SurfaceOpenUIError('limit', 'Too many declarations');
135
+ return statement + '\n';
136
+ }
137
+ return {
138
+ push(chunk) {
139
+ if (failure)
140
+ throw failure;
141
+ if (closed)
142
+ throw new SurfaceOpenUIError('syntax', 'Stream is closed');
143
+ try {
144
+ if (typeof chunk !== 'string')
145
+ throw new SurfaceOpenUIError('syntax', 'Chunks must be text');
146
+ characters += chunk.length;
147
+ if (characters > OPENUI_LIMITS.characters)
148
+ throw new SurfaceOpenUIError('limit', 'OpenUI input is too large');
149
+ const result = [];
150
+ for (const char of chunk) {
151
+ if (!inString && stack.length === 0 && (char === '\n' || char === ';')) {
152
+ const statement = release();
153
+ if (statement)
154
+ result.push(statement);
155
+ continue;
156
+ }
157
+ pending += char;
158
+ if (pending.length > OPENUI_LIMITS.statementCharacters)
159
+ throw new SurfaceOpenUIError('limit', 'Declaration is too large');
160
+ if (inString) {
161
+ if (escaped)
162
+ escaped = false;
163
+ else if (char === '\\')
164
+ escaped = true;
165
+ else if (char === '"')
166
+ inString = false;
167
+ }
168
+ else if (char === '"')
169
+ inString = true;
170
+ else if ('([{'.includes(char)) {
171
+ stack.push(char);
172
+ if (stack.length > OPENUI_LIMITS.depth)
173
+ throw new SurfaceOpenUIError('limit', 'OpenUI nesting limit exceeded');
174
+ }
175
+ else if (')]}'.includes(char)) {
176
+ const expected = { ')': '(', ']': '[', '}': '{' }[char];
177
+ if (stack.pop() !== expected)
178
+ throw new SurfaceOpenUIError('syntax', 'Unbalanced delimiter');
179
+ }
180
+ }
181
+ return result;
182
+ }
183
+ catch (error) {
184
+ failure = error instanceof SurfaceOpenUIError ? error : new SurfaceOpenUIError('syntax', 'Invalid OpenUI input');
185
+ pending = '';
186
+ throw failure;
187
+ }
188
+ },
189
+ finish() {
190
+ if (failure)
191
+ throw failure;
192
+ if (closed)
193
+ return [];
194
+ closed = true;
195
+ if (inString || stack.length) {
196
+ failure = new SurfaceOpenUIError('syntax', 'Truncated OpenUI declaration');
197
+ throw failure;
198
+ }
199
+ try {
200
+ const statement = release();
201
+ return statement ? [statement] : [];
202
+ }
203
+ catch (error) {
204
+ failure = error instanceof SurfaceOpenUIError ? error : new SurfaceOpenUIError('syntax', 'Invalid OpenUI input');
205
+ throw failure;
206
+ }
207
+ },
208
+ };
209
+ }
@@ -0,0 +1,58 @@
1
+ import type { JsonObject, JsonValue } from '../types.js';
2
+ import type { SurfaceWorkflowContext, SurfaceWorkflowOptions, SurfaceActionProposal, SurfaceStoredRevision } from './types.js';
3
+ /** Server-only orchestration; no functions from this file belong in a browser bundle. */
4
+ export declare function createSurfaceWorkflowService(options: SurfaceWorkflowOptions): {
5
+ saveSurface(identity: SurfaceWorkflowContext, input: {
6
+ spec: unknown;
7
+ expectedRevision: number;
8
+ }): Promise<SurfaceStoredRevision>;
9
+ readSurface(identity: SurfaceWorkflowContext, surfaceId: string, revision?: number): Promise<SurfaceStoredRevision>;
10
+ propose(identity: SurfaceWorkflowContext, input: {
11
+ actionId: string;
12
+ args: unknown;
13
+ surfaceId: string;
14
+ surfaceRevision: number;
15
+ requestKey: string;
16
+ }): Promise<SurfaceActionProposal>;
17
+ inspect(identity: SurfaceWorkflowContext, id: string): Promise<SurfaceActionProposal>;
18
+ approve(identity: SurfaceWorkflowContext, id: string, expectedDigest: string): Promise<{
19
+ status: "approved";
20
+ approvedBy: string;
21
+ id: string;
22
+ tenantId: string;
23
+ requesterId: string;
24
+ requestKey: string;
25
+ surfaceId: string;
26
+ surfaceRevision: number;
27
+ actionId: string;
28
+ actionVersion: string;
29
+ actionLabel: string;
30
+ args: JsonObject;
31
+ digest: string;
32
+ approval: import("./types.js").SurfaceApprovalPolicy;
33
+ createdAt: number;
34
+ expiresAt: number;
35
+ result?: JsonValue;
36
+ }>;
37
+ reject(identity: SurfaceWorkflowContext, id: string, expectedDigest: string): Promise<{
38
+ status: "rejected";
39
+ id: string;
40
+ tenantId: string;
41
+ requesterId: string;
42
+ requestKey: string;
43
+ surfaceId: string;
44
+ surfaceRevision: number;
45
+ actionId: string;
46
+ actionVersion: string;
47
+ actionLabel: string;
48
+ args: JsonObject;
49
+ digest: string;
50
+ approval: import("./types.js").SurfaceApprovalPolicy;
51
+ createdAt: number;
52
+ expiresAt: number;
53
+ approvedBy?: string;
54
+ result?: JsonValue;
55
+ }>;
56
+ execute(identity: SurfaceWorkflowContext, id: string, expectedDigest: string): Promise<SurfaceActionProposal>;
57
+ audit(identity: SurfaceWorkflowContext, surfaceId: string, after?: number, limit?: number): Promise<readonly import("./types.js").SurfaceWorkflowAuditEvent[]>;
58
+ };
@@ -0,0 +1,296 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { jsonValueIssue } from '../json.js';
3
+ import { validateSurfaceSpec } from '../validation.js';
4
+ import { SurfaceWorkflowError } from './types.js';
5
+ import { defineSurfaceAction } from './action-contracts.js';
6
+ const clone = (value) => structuredClone(value);
7
+ function frozen(value) {
8
+ if (value && typeof value === 'object') {
9
+ for (const child of Object.values(value))
10
+ frozen(child);
11
+ Object.freeze(value);
12
+ }
13
+ return value;
14
+ }
15
+ function canonical(value) {
16
+ if (value === null || typeof value !== 'object')
17
+ return JSON.stringify(value);
18
+ if (Array.isArray(value))
19
+ return '[' + value.map(canonical).join(',') + ']';
20
+ const object = value;
21
+ return '{' + Object.entries(object).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0).map(([key, child]) => JSON.stringify(key) + ':' + canonical(child)).join(',') + '}';
22
+ }
23
+ function json(input) {
24
+ if (jsonValueIssue(input) || JSON.stringify(input).length > 131_072)
25
+ throw new SurfaceWorkflowError('invalid_input', 'Expected bounded, plain JSON');
26
+ }
27
+ function key(value) {
28
+ if (typeof value !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9_.:-]{0,127}$/u.test(value))
29
+ throw new SurfaceWorkflowError('invalid_input', 'Invalid identifier');
30
+ }
31
+ function context(input) {
32
+ key(input.tenantId);
33
+ key(input.actorId);
34
+ return Object.freeze({ tenantId: input.tenantId, actorId: input.actorId });
35
+ }
36
+ function digest(value) { return createHash('sha256').update(canonical(value)).digest('hex'); }
37
+ function argumentDigest(p) {
38
+ return digest({ tenantId: p.tenantId, requesterId: p.requesterId, surfaceId: p.surfaceId,
39
+ surfaceRevision: p.surfaceRevision, actionId: p.actionId, actionVersion: p.actionVersion, args: p.args, approval: p.approval });
40
+ }
41
+ /** Server-only orchestration; no functions from this file belong in a browser bundle. */
42
+ export function createSurfaceWorkflowService(options) {
43
+ const { store } = options;
44
+ const now = options.now ?? Date.now;
45
+ const lifetime = options.proposalLifetimeMs ?? 900_000;
46
+ if (!Number.isFinite(lifetime) || lifetime < 1000 || lifetime > 86_400_000)
47
+ throw new Error('Invalid proposal lifetime');
48
+ const actions = new Map();
49
+ for (const action of options.actions) {
50
+ if (actions.has(action.id))
51
+ throw new Error(`Duplicate Surface action: ${action.id}`);
52
+ actions.set(action.id, defineSurfaceAction(action));
53
+ }
54
+ function registered(id) {
55
+ const action = actions.get(id);
56
+ if (!action)
57
+ throw new SurfaceWorkflowError('denied');
58
+ return action;
59
+ }
60
+ async function access(ctx, surfaceId, operation) {
61
+ key(surfaceId);
62
+ const result = await options.authorizeSurface({ context: ctx, operation, surfaceId });
63
+ if (!result)
64
+ throw new SurfaceWorkflowError('denied');
65
+ return result;
66
+ }
67
+ async function authorize(ctx, action, phase, args, requesterId) {
68
+ if (!await action.authorize({ context: ctx, phase, args: frozen(clone(args)), requesterId }))
69
+ throw new SurfaceWorkflowError('denied');
70
+ }
71
+ function event(tx, ctx, name, p) {
72
+ tx.audit({ tenantId: ctx.tenantId, surfaceId: p.surfaceId, proposalId: p.id, actorId: ctx.actorId,
73
+ event: name, at: now(), metadata: { actionId: p.actionId, revision: p.surfaceRevision, digest: p.digest } });
74
+ }
75
+ function latest(tx, ctx, surfaceId, revision) {
76
+ const saved = tx.getRevision(ctx.tenantId, surfaceId);
77
+ if (!saved || saved.revision !== revision)
78
+ throw new SurfaceWorkflowError('conflict', 'Surface revision changed');
79
+ return saved;
80
+ }
81
+ function proposal(tx, ctx, id) {
82
+ key(id);
83
+ const p = tx.getProposal(ctx.tenantId, id);
84
+ if (!p)
85
+ throw new SurfaceWorkflowError('not_found');
86
+ return p;
87
+ }
88
+ function current(tx, ctx, p, action) {
89
+ if (p.actionVersion !== action.version || p.approval !== action.approval || p.digest !== argumentDigest(p))
90
+ throw new SurfaceWorkflowError('conflict', 'Action contract changed');
91
+ if (p.expiresAt <= now())
92
+ throw new SurfaceWorkflowError('expired');
93
+ if (!action.validateInput(p.args))
94
+ throw new SurfaceWorkflowError('invalid_input');
95
+ const saved = latest(tx, ctx, p.surfaceId, p.surfaceRevision);
96
+ if (!saved.spec.widgets.some((w) => w.type === 'resource-form' && w.props['actionId'] === action.id))
97
+ throw new SurfaceWorkflowError('denied');
98
+ }
99
+ return {
100
+ async saveSurface(identity, input) {
101
+ const ctx = context(identity);
102
+ input = { spec: input.spec, expectedRevision: input.expectedRevision };
103
+ json(input.spec);
104
+ const spec = clone(input.spec);
105
+ const surfaceId = spec?.['surfaceId'];
106
+ key(surfaceId);
107
+ if (!Number.isSafeInteger(input.expectedRevision) || input.expectedRevision < 0)
108
+ throw new SurfaceWorkflowError('invalid_input');
109
+ const { catalog, policy } = await access(ctx, surfaceId, 'save');
110
+ const checked = validateSurfaceSpec(spec, catalog, policy);
111
+ if (!checked.ok)
112
+ throw new SurfaceWorkflowError('invalid_input', 'Surface failed policy validation');
113
+ return store.transaction((tx) => {
114
+ const previous = tx.getRevision(ctx.tenantId, surfaceId);
115
+ if ((previous?.revision ?? 0) !== input.expectedRevision)
116
+ throw new SurfaceWorkflowError('conflict');
117
+ const revision = { tenantId: ctx.tenantId, surfaceId, revision: input.expectedRevision + 1,
118
+ spec: clone(checked.value), actorId: ctx.actorId, createdAt: now() };
119
+ tx.putRevision(revision);
120
+ tx.audit({ tenantId: ctx.tenantId, surfaceId, actorId: ctx.actorId, event: 'surface.saved', at: now(), metadata: { revision: revision.revision } });
121
+ return clone(revision);
122
+ });
123
+ },
124
+ async readSurface(identity, surfaceId, revision) {
125
+ const ctx = context(identity);
126
+ const { catalog, policy } = await access(ctx, surfaceId, 'read');
127
+ if (revision !== undefined && (!Number.isSafeInteger(revision) || revision < 1))
128
+ throw new SurfaceWorkflowError('invalid_input');
129
+ const saved = store.getRevision(ctx.tenantId, surfaceId, revision);
130
+ if (!saved)
131
+ throw new SurfaceWorkflowError('not_found');
132
+ if (!validateSurfaceSpec(saved.spec, catalog, policy).ok)
133
+ throw new SurfaceWorkflowError('denied');
134
+ return clone(saved);
135
+ },
136
+ async propose(identity, input) {
137
+ const ctx = context(identity);
138
+ input = { ...input };
139
+ key(input.requestKey);
140
+ key(input.actionId);
141
+ if (!Number.isSafeInteger(input.surfaceRevision) || input.surfaceRevision < 1)
142
+ throw new SurfaceWorkflowError('invalid_input');
143
+ json(input.args);
144
+ const args = clone(input.args);
145
+ const action = registered(input.actionId);
146
+ if (!action.validateInput(args))
147
+ throw new SurfaceWorkflowError('invalid_input');
148
+ const { catalog, policy } = await access(ctx, input.surfaceId, 'read');
149
+ await authorize(ctx, action, 'propose', args, ctx.actorId);
150
+ const candidate = {
151
+ id: randomUUID(), tenantId: ctx.tenantId, requesterId: ctx.actorId, requestKey: input.requestKey,
152
+ surfaceId: input.surfaceId, surfaceRevision: input.surfaceRevision,
153
+ actionId: action.id, actionVersion: action.version, actionLabel: action.label,
154
+ args: args, approval: action.approval, status: 'pending',
155
+ createdAt: now(), expiresAt: now() + lifetime, digest: '',
156
+ };
157
+ const next = { ...candidate, digest: argumentDigest(candidate) };
158
+ return store.transaction((tx) => {
159
+ const saved = latest(tx, ctx, input.surfaceId, input.surfaceRevision);
160
+ if (!validateSurfaceSpec(saved.spec, catalog, policy).ok)
161
+ throw new SurfaceWorkflowError('denied');
162
+ current(tx, ctx, next, action);
163
+ const existing = tx.findRequest(ctx.tenantId, ctx.actorId, input.requestKey);
164
+ if (existing) {
165
+ if (existing.digest !== next.digest)
166
+ throw new SurfaceWorkflowError('conflict', 'Idempotency key reused with different input');
167
+ return clone(existing);
168
+ }
169
+ tx.putProposal(next);
170
+ event(tx, ctx, 'action.proposed', next);
171
+ return clone(next);
172
+ });
173
+ },
174
+ async inspect(identity, id) {
175
+ const ctx = context(identity);
176
+ const p = proposal(store, ctx, id);
177
+ await access(ctx, p.surfaceId, 'read');
178
+ await authorize(ctx, registered(p.actionId), 'read', p.args, p.requesterId);
179
+ return clone(p);
180
+ },
181
+ async approve(identity, id, expectedDigest) {
182
+ const ctx = context(identity);
183
+ const before = proposal(store, ctx, id);
184
+ const action = registered(before.actionId);
185
+ const { catalog, policy } = await access(ctx, before.surfaceId, 'read');
186
+ await authorize(ctx, action, 'approve', before.args, before.requesterId);
187
+ if (action.approval === 'confirm' ? ctx.actorId !== before.requesterId : ctx.actorId === before.requesterId)
188
+ throw new SurfaceWorkflowError('denied');
189
+ return store.transaction((tx) => {
190
+ const p = proposal(tx, ctx, id);
191
+ current(tx, ctx, p, action);
192
+ if (!validateSurfaceSpec(latest(tx, ctx, p.surfaceId, p.surfaceRevision).spec, catalog, policy).ok)
193
+ throw new SurfaceWorkflowError('denied');
194
+ if (expectedDigest !== p.digest)
195
+ throw new SurfaceWorkflowError('conflict');
196
+ if (p.status !== 'pending')
197
+ throw new SurfaceWorkflowError('invalid_state');
198
+ const next = { ...p, status: 'approved', approvedBy: ctx.actorId };
199
+ tx.putProposal(next);
200
+ event(tx, ctx, 'action.approved', next);
201
+ return clone(next);
202
+ });
203
+ },
204
+ async reject(identity, id, expectedDigest) {
205
+ const ctx = context(identity);
206
+ const before = proposal(store, ctx, id);
207
+ await access(ctx, before.surfaceId, 'read');
208
+ await authorize(ctx, registered(before.actionId), 'reject', before.args, before.requesterId);
209
+ return store.transaction((tx) => {
210
+ const p = proposal(tx, ctx, id);
211
+ if (p.digest !== expectedDigest)
212
+ throw new SurfaceWorkflowError('conflict');
213
+ if (p.status !== 'pending' && p.status !== 'approved')
214
+ throw new SurfaceWorkflowError('invalid_state');
215
+ const next = { ...p, status: 'rejected' };
216
+ tx.putProposal(next);
217
+ event(tx, ctx, 'action.rejected', next);
218
+ return clone(next);
219
+ });
220
+ },
221
+ async execute(identity, id, expectedDigest) {
222
+ const ctx = context(identity);
223
+ const before = proposal(store, ctx, id);
224
+ const action = registered(before.actionId);
225
+ if (before.requesterId !== ctx.actorId)
226
+ throw new SurfaceWorkflowError('denied');
227
+ if (before.status !== 'approved' && before.status !== 'succeeded')
228
+ throw new SurfaceWorkflowError('invalid_state');
229
+ const { catalog, policy } = await access(ctx, before.surfaceId, 'read');
230
+ await authorize(ctx, action, 'execute', before.args, before.requesterId);
231
+ if (before.status === 'approved' && before.approvedBy) {
232
+ await authorize(context({ ...ctx, actorId: before.approvedBy }), action, 'approve', before.args, before.requesterId);
233
+ }
234
+ const claimed = store.transaction((tx) => {
235
+ const p = proposal(tx, ctx, id);
236
+ if (p.digest !== expectedDigest)
237
+ throw new SurfaceWorkflowError('conflict');
238
+ // Repeated successful requests return the recorded result, never rerun.
239
+ if (p.status === 'succeeded')
240
+ return p;
241
+ current(tx, ctx, p, action);
242
+ const saved = latest(tx, ctx, p.surfaceId, p.surfaceRevision);
243
+ if (!validateSurfaceSpec(saved.spec, catalog, policy).ok)
244
+ throw new SurfaceWorkflowError('denied');
245
+ if (p.status !== 'approved' || !p.approvedBy)
246
+ throw new SurfaceWorkflowError('invalid_state');
247
+ if (p.approval === 'confirm' ? p.approvedBy !== p.requesterId : p.approvedBy === p.requesterId)
248
+ throw new SurfaceWorkflowError('denied');
249
+ const next = { ...p, status: 'executing' };
250
+ tx.putProposal(next);
251
+ event(tx, ctx, 'action.executing', next);
252
+ return next;
253
+ });
254
+ if (claimed.status === 'succeeded')
255
+ return clone(claimed);
256
+ let result;
257
+ try {
258
+ const value = await action.execute({ context: ctx, args: frozen(clone(claimed.args)), idempotencyKey: claimed.id });
259
+ json(value);
260
+ result = clone(value);
261
+ }
262
+ catch {
263
+ try {
264
+ store.transaction((tx) => {
265
+ const p = proposal(tx, ctx, id);
266
+ tx.putProposal({ ...p, status: 'indeterminate' });
267
+ event(tx, ctx, 'action.indeterminate', p);
268
+ });
269
+ }
270
+ catch { /* The durable executing claim still prevents a duplicate effect. */ }
271
+ throw new SurfaceWorkflowError('indeterminate', 'Business effect may have occurred; reconciliation is required before any retry');
272
+ }
273
+ try {
274
+ return store.transaction((tx) => {
275
+ const p = proposal(tx, ctx, id);
276
+ if (p.status !== 'executing')
277
+ throw new SurfaceWorkflowError('invalid_state');
278
+ const next = { ...p, status: 'succeeded', result };
279
+ tx.putProposal(next);
280
+ event(tx, ctx, 'action.succeeded', next);
281
+ return clone(next);
282
+ });
283
+ }
284
+ catch {
285
+ throw new SurfaceWorkflowError('indeterminate', 'Business effect completed but recording failed; do not retry the business effect');
286
+ }
287
+ },
288
+ async audit(identity, surfaceId, after = 0, limit = 50) {
289
+ const ctx = context(identity);
290
+ await access(ctx, surfaceId, 'audit');
291
+ if (!Number.isSafeInteger(after) || after < 0 || !Number.isSafeInteger(limit) || limit < 1 || limit > 100)
292
+ throw new SurfaceWorkflowError('invalid_input');
293
+ return clone(store.listAudit(ctx.tenantId, surfaceId, after, limit));
294
+ },
295
+ };
296
+ }
@@ -0,0 +1,20 @@
1
+ import type { SurfaceWorkflowStore, SurfaceWorkflowTransaction, SurfaceStoredRevision, SurfaceActionProposal, SurfaceWorkflowAuditEvent } from './types.js';
2
+ /** Node-only durable adapter. Use a local persistent volume, not an ephemeral
3
+ * serverless filesystem or a network filesystem shared between server replicas.
4
+ * DB files/backups contain form data and require application retention/access controls.
5
+ */
6
+ export declare class SqliteSurfaceWorkflowStore implements SurfaceWorkflowStore {
7
+ private readonly db;
8
+ private active;
9
+ constructor(filename: string);
10
+ transaction<T>(work: (tx: SurfaceWorkflowTransaction) => T): T;
11
+ private writing;
12
+ getRevision(tenantId: string, surfaceId: string, revision?: number): SurfaceStoredRevision | undefined;
13
+ putRevision(revision: SurfaceStoredRevision): void;
14
+ getProposal(tenantId: string, id: string): SurfaceActionProposal | undefined;
15
+ findRequest(tenantId: string, requesterId: string, requestKey: string): SurfaceActionProposal | undefined;
16
+ putProposal(proposal: SurfaceActionProposal): void;
17
+ audit(event: SurfaceWorkflowAuditEvent): void;
18
+ listAudit(tenantId: string, surfaceId: string, after: number, limit: number): readonly SurfaceWorkflowAuditEvent[];
19
+ close(): void;
20
+ }
@@ -0,0 +1,91 @@
1
+ import { DatabaseSync } from 'node:sqlite';
2
+ /** Node-only durable adapter. Use a local persistent volume, not an ephemeral
3
+ * serverless filesystem or a network filesystem shared between server replicas.
4
+ * DB files/backups contain form data and require application retention/access controls.
5
+ */
6
+ export class SqliteSurfaceWorkflowStore {
7
+ db;
8
+ active = false;
9
+ constructor(filename) {
10
+ this.db = new DatabaseSync(filename);
11
+ this.db.exec(`
12
+ PRAGMA busy_timeout = 5000;
13
+ PRAGMA journal_mode = WAL;
14
+ PRAGMA synchronous = FULL;
15
+ CREATE TABLE IF NOT EXISTS svadmin_surface_revisions (
16
+ tenant TEXT NOT NULL, surface TEXT NOT NULL, revision INTEGER NOT NULL CHECK(revision > 0),
17
+ document TEXT NOT NULL, PRIMARY KEY(tenant, surface, revision)
18
+ );
19
+ CREATE TABLE IF NOT EXISTS svadmin_surface_proposals (
20
+ tenant TEXT NOT NULL, id TEXT NOT NULL, requester TEXT NOT NULL, request_key TEXT NOT NULL,
21
+ document TEXT NOT NULL, PRIMARY KEY(tenant, id), UNIQUE(tenant, requester, request_key)
22
+ );
23
+ CREATE TABLE IF NOT EXISTS svadmin_surface_audit (
24
+ sequence INTEGER PRIMARY KEY AUTOINCREMENT, tenant TEXT NOT NULL, surface TEXT NOT NULL,
25
+ document TEXT NOT NULL
26
+ );
27
+ CREATE INDEX IF NOT EXISTS svadmin_surface_audit_scope ON svadmin_surface_audit(tenant, surface, sequence);
28
+ `);
29
+ }
30
+ transaction(work) {
31
+ if (this.active)
32
+ throw new Error('Nested workflow transactions are not supported');
33
+ this.db.exec('BEGIN IMMEDIATE');
34
+ this.active = true;
35
+ try {
36
+ const result = work(this);
37
+ if (result && typeof result === 'object' && 'then' in result)
38
+ throw new Error('Workflow transactions must be synchronous');
39
+ this.db.exec('COMMIT');
40
+ return result;
41
+ }
42
+ catch (error) {
43
+ this.db.exec('ROLLBACK');
44
+ throw error;
45
+ }
46
+ finally {
47
+ this.active = false;
48
+ }
49
+ }
50
+ writing() {
51
+ if (!this.active)
52
+ throw new Error('Workflow writes must occur in an atomic transaction');
53
+ }
54
+ getRevision(tenantId, surfaceId, revision) {
55
+ const row = revision === undefined
56
+ ? this.db.prepare('SELECT document FROM svadmin_surface_revisions WHERE tenant = ? AND surface = ? ORDER BY revision DESC LIMIT 1').get(tenantId, surfaceId)
57
+ : this.db.prepare('SELECT document FROM svadmin_surface_revisions WHERE tenant = ? AND surface = ? AND revision = ?').get(tenantId, surfaceId, revision);
58
+ return row ? JSON.parse(String(row['document'])) : undefined;
59
+ }
60
+ putRevision(revision) {
61
+ this.writing();
62
+ this.db.prepare('INSERT INTO svadmin_surface_revisions(tenant, surface, revision, document) VALUES (?, ?, ?, ?)')
63
+ .run(revision.tenantId, revision.surfaceId, revision.revision, JSON.stringify(revision));
64
+ }
65
+ getProposal(tenantId, id) {
66
+ const row = this.db.prepare('SELECT document FROM svadmin_surface_proposals WHERE tenant = ? AND id = ?').get(tenantId, id);
67
+ return row ? JSON.parse(String(row['document'])) : undefined;
68
+ }
69
+ findRequest(tenantId, requesterId, requestKey) {
70
+ const row = this.db.prepare('SELECT document FROM svadmin_surface_proposals WHERE tenant = ? AND requester = ? AND request_key = ?')
71
+ .get(tenantId, requesterId, requestKey);
72
+ return row ? JSON.parse(String(row['document'])) : undefined;
73
+ }
74
+ putProposal(proposal) {
75
+ this.writing();
76
+ this.db.prepare(`INSERT INTO svadmin_surface_proposals(tenant, id, requester, request_key, document)
77
+ VALUES (?, ?, ?, ?, ?) ON CONFLICT(tenant, id) DO UPDATE SET document = excluded.document`)
78
+ .run(proposal.tenantId, proposal.id, proposal.requesterId, proposal.requestKey, JSON.stringify(proposal));
79
+ }
80
+ audit(event) {
81
+ this.writing();
82
+ this.db.prepare('INSERT INTO svadmin_surface_audit(tenant, surface, document) VALUES (?, ?, ?)')
83
+ .run(event.tenantId, event.surfaceId, JSON.stringify(event));
84
+ }
85
+ listAudit(tenantId, surfaceId, after, limit) {
86
+ return this.db.prepare('SELECT sequence, document FROM svadmin_surface_audit WHERE tenant = ? AND surface = ? AND sequence > ? ORDER BY sequence LIMIT ?')
87
+ .all(tenantId, surfaceId, after, limit)
88
+ .map((row) => ({ ...JSON.parse(String(row['document'])), sequence: Number(row['sequence']) }));
89
+ }
90
+ close() { this.db.close(); }
91
+ }