@happyvertical/smrt-svelte 0.42.6 → 0.42.7

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 (54) hide show
  1. package/README.md +20 -0
  2. package/dist/Provider.svelte +49 -0
  3. package/dist/Provider.svelte.d.ts +15 -0
  4. package/dist/Provider.svelte.d.ts.map +1 -1
  5. package/dist/__tests__/data-surface-bridge.test.js +351 -0
  6. package/dist/components/forms/AddressInput.svelte +11 -0
  7. package/dist/components/forms/AddressInput.svelte.d.ts.map +1 -1
  8. package/dist/components/forms/CheckboxInput.svelte +3 -1
  9. package/dist/components/forms/CheckboxInput.svelte.d.ts.map +1 -1
  10. package/dist/components/forms/DateRangeInput.svelte +3 -0
  11. package/dist/components/forms/DateRangeInput.svelte.d.ts.map +1 -1
  12. package/dist/components/forms/DateTimeInput.svelte +2 -0
  13. package/dist/components/forms/DateTimeInput.svelte.d.ts.map +1 -1
  14. package/dist/components/forms/Form.svelte +172 -6
  15. package/dist/components/forms/Form.svelte.d.ts +8 -1
  16. package/dist/components/forms/Form.svelte.d.ts.map +1 -1
  17. package/dist/components/forms/MeasurementInput.svelte +3 -0
  18. package/dist/components/forms/MeasurementInput.svelte.d.ts.map +1 -1
  19. package/dist/components/forms/MoneyInput.svelte +3 -0
  20. package/dist/components/forms/MoneyInput.svelte.d.ts.map +1 -1
  21. package/dist/components/forms/NumberInput.svelte +3 -0
  22. package/dist/components/forms/NumberInput.svelte.d.ts.map +1 -1
  23. package/dist/components/forms/PhoneInput.svelte +2 -0
  24. package/dist/components/forms/PhoneInput.svelte.d.ts.map +1 -1
  25. package/dist/components/forms/SelectInput.svelte +7 -1
  26. package/dist/components/forms/SelectInput.svelte.d.ts.map +1 -1
  27. package/dist/components/forms/TextInput.svelte +5 -1
  28. package/dist/components/forms/TextInput.svelte.d.ts.map +1 -1
  29. package/dist/components/forms/TextareaInput.svelte +3 -1
  30. package/dist/components/forms/TextareaInput.svelte.d.ts.map +1 -1
  31. package/dist/components/forms/__tests__/Form.webmcp.test.js +216 -0
  32. package/dist/components/forms/__tests__/form-with-fields.fixture.svelte +25 -3
  33. package/dist/components/forms/__tests__/form-with-fields.fixture.svelte.d.ts +5 -0
  34. package/dist/components/forms/__tests__/form-with-fields.fixture.svelte.d.ts.map +1 -1
  35. package/dist/components/forms/__tests__/form-with-structured-fields.fixture.svelte +40 -0
  36. package/dist/components/forms/__tests__/form-with-structured-fields.fixture.svelte.d.ts +10 -0
  37. package/dist/components/forms/__tests__/form-with-structured-fields.fixture.svelte.d.ts.map +1 -0
  38. package/dist/data-surface.d.ts +104 -0
  39. package/dist/data-surface.d.ts.map +1 -0
  40. package/dist/data-surface.js +348 -0
  41. package/dist/index.d.ts +2 -0
  42. package/dist/index.d.ts.map +1 -1
  43. package/dist/index.js +5 -0
  44. package/dist/state/form-context.d.ts +2 -0
  45. package/dist/state/form-context.d.ts.map +1 -1
  46. package/dist/web/__tests__/webmcp-harness.svelte +14 -0
  47. package/dist/web/__tests__/webmcp-harness.svelte.d.ts +7 -0
  48. package/dist/web/__tests__/webmcp-harness.svelte.d.ts.map +1 -0
  49. package/dist/web/__tests__/webmcp.test.js +39 -0
  50. package/dist/web/webmcp.d.ts +21 -0
  51. package/dist/web/webmcp.svelte.d.ts +28 -0
  52. package/dist/web/webmcp.svelte.d.ts.map +1 -0
  53. package/dist/web/webmcp.svelte.js +27 -0
  54. package/package.json +17 -5
@@ -0,0 +1,40 @@
1
+ <script lang="ts">
2
+ import AddressInput from '../AddressInput.svelte';
3
+ import DateRangeInput from '../DateRangeInput.svelte';
4
+ import Form from '../Form.svelte';
5
+ import MeasurementInput from '../MeasurementInput.svelte';
6
+
7
+ let {
8
+ onsubmit = undefined,
9
+ webmcp = false,
10
+ addressFields = ['street', 'city', 'province', 'postalCode', 'country'],
11
+ structuredRequired = true,
12
+ }: {
13
+ onsubmit?: (data: Record<string, unknown>) => void;
14
+ webmcp?: boolean;
15
+ addressFields?: Array<
16
+ 'street' | 'city' | 'province' | 'postalCode' | 'country'
17
+ >;
18
+ structuredRequired?: boolean;
19
+ } = $props();
20
+ </script>
21
+
22
+ <Form {onsubmit} {webmcp}>
23
+ <MeasurementInput
24
+ name="measurement"
25
+ label="Measurement"
26
+ required={structuredRequired}
27
+ />
28
+ <DateRangeInput
29
+ name="dates"
30
+ label="Dates"
31
+ required={structuredRequired}
32
+ />
33
+ <AddressInput
34
+ name="address"
35
+ label="Address"
36
+ fields={addressFields}
37
+ required={structuredRequired}
38
+ />
39
+ <button type="submit">Submit</button>
40
+ </Form>
@@ -0,0 +1,10 @@
1
+ type $$ComponentProps = {
2
+ onsubmit?: (data: Record<string, unknown>) => void;
3
+ webmcp?: boolean;
4
+ addressFields?: Array<'street' | 'city' | 'province' | 'postalCode' | 'country'>;
5
+ structuredRequired?: boolean;
6
+ };
7
+ declare const FormWithStructuredFields: import("svelte").Component<$$ComponentProps, {}, "">;
8
+ type FormWithStructuredFields = ReturnType<typeof FormWithStructuredFields>;
9
+ export default FormWithStructuredFields;
10
+ //# sourceMappingURL=form-with-structured-fields.fixture.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"form-with-structured-fields.fixture.svelte.d.ts","sourceRoot":"","sources":["../../../../src/components/forms/__tests__/form-with-structured-fields.fixture.svelte.ts"],"names":[],"mappings":"AAQC,KAAK,gBAAgB,GAAI;IACxB,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;IACnD,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,aAAa,CAAC,EAAE,KAAK,CACnB,QAAQ,GAAG,MAAM,GAAG,UAAU,GAAG,YAAY,GAAG,SAAS,CAC1D,CAAC;IACF,kBAAkB,CAAC,EAAE,OAAO,CAAC;CAC9B,CAAC;AAwBF,QAAA,MAAM,wBAAwB,sDAAwC,CAAC;AACvE,KAAK,wBAAwB,GAAG,UAAU,CAAC,OAAO,wBAAwB,CAAC,CAAC;AAC5E,eAAe,wBAAwB,CAAC"}
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Authenticated browser bridge for mounted data surfaces.
3
+ *
4
+ * `smrt-ui` owns the local registry and deliberately knows nothing about
5
+ * transports or authentication. This adapter is the browser-side trust
6
+ * boundary: only a request carrying the configured session and peer source is
7
+ * delivered to the registry. A command is successful only after its ack has
8
+ * travelled back through the transport.
9
+ */
10
+ import { type DataSurfaceCommandResult, type DataSurfaceIdentity, type DataSurfaceRegistry, type DataSurfaceRegistryEvent, type DataSurfaceSnapshot, type DataSurfaceVisibleCommand } from '@happyvertical/smrt-ui/data';
11
+ export declare const DATA_SURFACE_BRIDGE_VERSION: 1;
12
+ export declare const DEFAULT_DATA_SURFACE_BRIDGE_TTL_MS = 30000;
13
+ export declare const DEFAULT_DATA_SURFACE_BRIDGE_REPLAY_ENTRIES = 100;
14
+ export { DATA_SURFACE_IDENTIFIER_MAX_LENGTH } from '@happyvertical/smrt-ui/data-surface';
15
+ export type DataSurfaceBridgeFailureReason = 'not_found' | 'unsupported' | 'stale_revision' | 'idempotency_conflict' | 'denied' | 'execution_failed' | 'non_monotonic_revision' | 'expired' | 'timeout' | 'disconnected' | 'invalid_request' | 'source_mismatch' | 'session_mismatch' | 'replay_capacity_exceeded';
16
+ export interface DataSurfaceCommandRequest {
17
+ type: 'data-surface.command';
18
+ version: typeof DATA_SURFACE_BRIDGE_VERSION;
19
+ commandId: string;
20
+ sessionId: string;
21
+ /** Authenticated peer/source id, never a profile or tenant authority. */
22
+ source: string;
23
+ expiresAt: number;
24
+ identity: DataSurfaceIdentity;
25
+ expectedRevision: number;
26
+ controlId: string;
27
+ payload?: DataSurfaceVisibleCommand['payload'];
28
+ }
29
+ export interface DataSurfaceCommandAck {
30
+ type: 'data-surface.ack';
31
+ version: typeof DATA_SURFACE_BRIDGE_VERSION;
32
+ commandId: string;
33
+ sessionId: string;
34
+ source: string;
35
+ expiresAt: number;
36
+ identity: DataSurfaceIdentity;
37
+ expectedRevision: number;
38
+ ok: boolean;
39
+ revision?: number;
40
+ snapshot?: DataSurfaceSnapshot;
41
+ reason?: DataSurfaceBridgeFailureReason;
42
+ }
43
+ export interface DataSurfaceBridgeEvent {
44
+ type: 'data-surface.event';
45
+ version: typeof DATA_SURFACE_BRIDGE_VERSION;
46
+ sessionId: string;
47
+ source: string;
48
+ sequence: number;
49
+ identity: DataSurfaceIdentity;
50
+ revision: number;
51
+ event: DataSurfaceRegistryEvent['type'];
52
+ command?: DataSurfaceVisibleCommand;
53
+ result?: DataSurfaceCommandResult;
54
+ }
55
+ export type DataSurfaceBridgeMessage = DataSurfaceCommandRequest | DataSurfaceCommandAck | DataSurfaceBridgeEvent;
56
+ /**
57
+ * Identity verified by the transport adapter, outside the wire message.
58
+ * Adapters must derive this from authenticated connection state (for example,
59
+ * a bound WebSocket session or an origin-checked postMessage peer), never
60
+ * from fields in `message`. `send` must route only to that bound peer.
61
+ */
62
+ export interface DataSurfaceBridgePeer {
63
+ sessionId: string;
64
+ source: string;
65
+ }
66
+ export type DataSurfaceBridgeConnectionState = 'connected' | 'disconnected' | 'reconnecting';
67
+ /** A deliberately tiny adapter for WebSocket, SSE, postMessage, or a test. */
68
+ export interface DataSurfaceBridgeTransport {
69
+ send(message: DataSurfaceBridgeMessage): void | Promise<void>;
70
+ subscribe(listener: (message: unknown, peer: DataSurfaceBridgePeer) => void): () => void;
71
+ subscribeStatus?: (listener: (state: DataSurfaceBridgeConnectionState) => void) => () => void;
72
+ }
73
+ export interface DataSurfaceBrowserBridgeOptions {
74
+ registry: DataSurfaceRegistry;
75
+ transport: DataSurfaceBridgeTransport;
76
+ sessionId: string;
77
+ /** This browser's source id, placed on acknowledgements/events. */
78
+ source: string;
79
+ /** The only server source accepted for commands. */
80
+ peerSource: string;
81
+ now?: () => number;
82
+ maxTtlMs?: number;
83
+ maxReplayEntries?: number;
84
+ }
85
+ export interface DataSurfaceBrowserBridge {
86
+ readonly sessionId: string;
87
+ readonly source: string;
88
+ /** Stop receiving transport messages and registry events. */
89
+ dispose(): void;
90
+ /** Handle a message directly; useful for transports that batch delivery. */
91
+ receive(message: unknown, peer: DataSurfaceBridgePeer): Promise<void>;
92
+ }
93
+ /**
94
+ * Attach a mounted registry to an authenticated browser transport.
95
+ * Malformed requests and messages from unauthenticated peers are ignored before
96
+ * acknowledgement. Valid requests from the bound peer that are expired,
97
+ * cross-session, or cross-source are acknowledged without exposing a registry
98
+ * snapshot. Replays return the original ack and do not invoke the mounted
99
+ * surface a second time.
100
+ */
101
+ export declare function createDataSurfaceBrowserBridge(options: DataSurfaceBrowserBridgeOptions): DataSurfaceBrowserBridge;
102
+ /** Compatibility alias that makes the browser role explicit at call sites. */
103
+ export declare const createDataSurfaceBridge: typeof createDataSurfaceBrowserBridge;
104
+ //# sourceMappingURL=data-surface.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"data-surface.d.ts","sourceRoot":"","sources":["../src/data-surface.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,348 @@
1
+ /**
2
+ * Authenticated browser bridge for mounted data surfaces.
3
+ *
4
+ * `smrt-ui` owns the local registry and deliberately knows nothing about
5
+ * transports or authentication. This adapter is the browser-side trust
6
+ * boundary: only a request carrying the configured session and peer source is
7
+ * delivered to the registry. A command is successful only after its ack has
8
+ * travelled back through the transport.
9
+ */
10
+ import { normalizeDataSurfaceVisibleCommand, } from '@happyvertical/smrt-ui/data';
11
+ import { DATA_SURFACE_IDENTIFIER_MAX_LENGTH } from '@happyvertical/smrt-ui/data-surface';
12
+ export const DATA_SURFACE_BRIDGE_VERSION = 1;
13
+ export const DEFAULT_DATA_SURFACE_BRIDGE_TTL_MS = 30_000;
14
+ export const DEFAULT_DATA_SURFACE_BRIDGE_REPLAY_ENTRIES = 100;
15
+ export { DATA_SURFACE_IDENTIFIER_MAX_LENGTH } from '@happyvertical/smrt-ui/data-surface';
16
+ function isRecord(value) {
17
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
18
+ }
19
+ function isString(value) {
20
+ return (typeof value === 'string' &&
21
+ value.length > 0 &&
22
+ value.length <= DATA_SURFACE_IDENTIFIER_MAX_LENGTH);
23
+ }
24
+ function isDisplayString(value) {
25
+ return typeof value === 'string' && value.length > 0;
26
+ }
27
+ function isPeer(value) {
28
+ return isRecord(value) && isString(value.sessionId) && isString(value.source);
29
+ }
30
+ function isFiniteInteger(value) {
31
+ return typeof value === 'number' && Number.isSafeInteger(value);
32
+ }
33
+ function identityOf(value) {
34
+ if (!isRecord(value) || !isString(value.surfaceId) || !isString(value.kind)) {
35
+ return undefined;
36
+ }
37
+ if (value.kind !== 'table' &&
38
+ value.kind !== 'list' &&
39
+ value.kind !== 'report' &&
40
+ value.kind !== 'custom') {
41
+ return undefined;
42
+ }
43
+ if (value.subject !== undefined) {
44
+ if (!isRecord(value.subject) ||
45
+ !isString(value.subject.type) ||
46
+ !isString(value.subject.id) ||
47
+ (value.subject.label !== undefined &&
48
+ !isDisplayString(value.subject.label))) {
49
+ return undefined;
50
+ }
51
+ }
52
+ return value;
53
+ }
54
+ function isRequest(value) {
55
+ if (!isRecord(value) ||
56
+ value.type !== 'data-surface.command' ||
57
+ value.version !== 1) {
58
+ return false;
59
+ }
60
+ return (isString(value.commandId) &&
61
+ isString(value.sessionId) &&
62
+ isString(value.source) &&
63
+ typeof value.expiresAt === 'number' &&
64
+ Number.isFinite(value.expiresAt) &&
65
+ identityOf(value.identity) !== undefined &&
66
+ isFiniteInteger(value.expectedRevision) &&
67
+ value.expectedRevision >= 0 &&
68
+ isString(value.controlId));
69
+ }
70
+ function commandSignature(request) {
71
+ return JSON.stringify({
72
+ sessionId: request.sessionId,
73
+ source: request.source,
74
+ identity: request.identity,
75
+ expectedRevision: request.expectedRevision,
76
+ controlId: request.controlId,
77
+ payload: request.payload,
78
+ });
79
+ }
80
+ function cloneMessage(message) {
81
+ return JSON.parse(JSON.stringify(message));
82
+ }
83
+ /**
84
+ * Attach a mounted registry to an authenticated browser transport.
85
+ * Malformed requests and messages from unauthenticated peers are ignored before
86
+ * acknowledgement. Valid requests from the bound peer that are expired,
87
+ * cross-session, or cross-source are acknowledged without exposing a registry
88
+ * snapshot. Replays return the original ack and do not invoke the mounted
89
+ * surface a second time.
90
+ */
91
+ export function createDataSurfaceBrowserBridge(options) {
92
+ if (!isString(options.sessionId) ||
93
+ !isString(options.source) ||
94
+ !isString(options.peerSource)) {
95
+ throw new TypeError('DataSurface bridge session/source ids are required');
96
+ }
97
+ const now = options.now ?? (() => Date.now());
98
+ const maxTtlMs = options.maxTtlMs ?? DEFAULT_DATA_SURFACE_BRIDGE_TTL_MS;
99
+ const maxReplayEntries = options.maxReplayEntries ?? DEFAULT_DATA_SURFACE_BRIDGE_REPLAY_ENTRIES;
100
+ if (!Number.isFinite(maxTtlMs) ||
101
+ maxTtlMs <= 0 ||
102
+ !Number.isSafeInteger(maxReplayEntries) ||
103
+ maxReplayEntries <= 0) {
104
+ throw new RangeError('Invalid DataSurface bridge bounds');
105
+ }
106
+ const replay = new Map();
107
+ const reservations = new Set();
108
+ const inflight = new Map();
109
+ let disposed = false;
110
+ let lastSequence = 0;
111
+ const pruneReplay = () => {
112
+ const current = now();
113
+ for (const [commandId, entry] of replay) {
114
+ if (entry.expiresAt <= current)
115
+ replay.delete(commandId);
116
+ }
117
+ };
118
+ const remember = (request, ack) => {
119
+ replay.set(request.commandId, {
120
+ signature: commandSignature(request),
121
+ ack: cloneMessage(ack),
122
+ expiresAt: request.expiresAt,
123
+ });
124
+ };
125
+ const replayAck = (request, ack) => ({
126
+ ...cloneMessage(ack),
127
+ expiresAt: request.expiresAt,
128
+ });
129
+ const sendAck = async (ack) => {
130
+ if (disposed)
131
+ return;
132
+ try {
133
+ await options.transport.send(cloneMessage(ack));
134
+ }
135
+ catch {
136
+ // A disconnected browser cannot repair delivery. The server-side
137
+ // bridge reports the bounded disconnect/timeout outcome to its caller.
138
+ }
139
+ };
140
+ const rejected = (request, reason) => ({
141
+ type: 'data-surface.ack',
142
+ version: 1,
143
+ commandId: typeof request.commandId === 'string' ? request.commandId : 'invalid',
144
+ sessionId: options.sessionId,
145
+ source: options.source,
146
+ expiresAt: typeof request.expiresAt === 'number' ? request.expiresAt : now(),
147
+ identity: identityOf(request.identity) ?? {
148
+ surfaceId: 'unknown',
149
+ kind: 'custom',
150
+ },
151
+ expectedRevision: isFiniteInteger(request.expectedRevision) && request.expectedRevision >= 0
152
+ ? request.expectedRevision
153
+ : 0,
154
+ ok: false,
155
+ reason,
156
+ });
157
+ const receive = async (value, peer) => {
158
+ if (disposed ||
159
+ !isRequest(value) ||
160
+ !isPeer(peer) ||
161
+ peer.sessionId !== options.sessionId ||
162
+ peer.source !== options.peerSource)
163
+ return;
164
+ const request = value;
165
+ pruneReplay();
166
+ const current = now();
167
+ if (request.expiresAt <= current ||
168
+ request.expiresAt - current > maxTtlMs) {
169
+ await sendAck(rejected(request, 'expired'));
170
+ return;
171
+ }
172
+ const existing = replay.get(request.commandId);
173
+ const signature = commandSignature(request);
174
+ if (existing) {
175
+ await sendAck(existing.signature === signature
176
+ ? replayAck(request, existing.ack)
177
+ : rejected(request, 'idempotency_conflict'));
178
+ return;
179
+ }
180
+ const active = inflight.get(request.commandId);
181
+ if (active) {
182
+ if (active.signature !== signature) {
183
+ await sendAck(rejected(request, 'idempotency_conflict'));
184
+ return;
185
+ }
186
+ await Promise.race([active.promise, active.expiry]);
187
+ const completed = replay.get(request.commandId);
188
+ if (completed?.signature === signature) {
189
+ await sendAck(replayAck(request, completed.ack));
190
+ }
191
+ else if (active.expired || request.expiresAt <= now()) {
192
+ await sendAck(rejected(request, 'expired'));
193
+ }
194
+ return;
195
+ }
196
+ let ack;
197
+ if (request.sessionId !== options.sessionId) {
198
+ ack = rejected(request, 'session_mismatch');
199
+ }
200
+ else if (request.source !== options.peerSource) {
201
+ ack = rejected(request, 'source_mismatch');
202
+ }
203
+ else if (replay.size + reservations.size >= maxReplayEntries) {
204
+ ack = rejected(request, 'replay_capacity_exceeded');
205
+ }
206
+ else {
207
+ reservations.add(request.commandId);
208
+ let resolveExpiry;
209
+ const expiry = new Promise((resolve) => {
210
+ resolveExpiry = resolve;
211
+ });
212
+ const active = {
213
+ signature,
214
+ promise: Promise.resolve(),
215
+ expiry,
216
+ expired: false,
217
+ expiryAckSent: false,
218
+ expiryTimer: undefined,
219
+ };
220
+ let promise;
221
+ promise = (async () => {
222
+ try {
223
+ let resultAck;
224
+ try {
225
+ const command = normalizeDataSurfaceVisibleCommand({
226
+ version: 1,
227
+ commandId: request.commandId,
228
+ identity: request.identity,
229
+ expectedRevision: request.expectedRevision,
230
+ controlId: request.controlId,
231
+ ...(request.payload === undefined
232
+ ? {}
233
+ : { payload: request.payload }),
234
+ });
235
+ const result = await options.registry.execute(command);
236
+ resultAck = {
237
+ type: 'data-surface.ack',
238
+ version: 1,
239
+ commandId: request.commandId,
240
+ sessionId: options.sessionId,
241
+ source: options.source,
242
+ expiresAt: request.expiresAt,
243
+ identity: result.identity,
244
+ expectedRevision: request.expectedRevision,
245
+ ok: result.ok,
246
+ revision: result.revision,
247
+ snapshot: result.snapshot,
248
+ reason: result.reason,
249
+ };
250
+ }
251
+ catch {
252
+ resultAck = rejected(request, 'invalid_request');
253
+ }
254
+ if (active.expired || request.expiresAt <= now()) {
255
+ active.expired = true;
256
+ reservations.delete(request.commandId);
257
+ if (inflight.get(request.commandId)?.promise === promise) {
258
+ inflight.delete(request.commandId);
259
+ }
260
+ clearTimeout(active.expiryTimer);
261
+ resolveExpiry();
262
+ if (!active.expiryAckSent) {
263
+ active.expiryAckSent = true;
264
+ await sendAck(rejected(request, 'expired'));
265
+ }
266
+ return;
267
+ }
268
+ remember(request, resultAck);
269
+ reservations.delete(request.commandId);
270
+ if (inflight.get(request.commandId)?.promise === promise) {
271
+ inflight.delete(request.commandId);
272
+ }
273
+ clearTimeout(active.expiryTimer);
274
+ resolveExpiry();
275
+ await sendAck(resultAck);
276
+ }
277
+ finally {
278
+ reservations.delete(request.commandId);
279
+ if (inflight.get(request.commandId)?.promise === promise) {
280
+ inflight.delete(request.commandId);
281
+ }
282
+ clearTimeout(active.expiryTimer);
283
+ resolveExpiry();
284
+ }
285
+ })();
286
+ active.promise = promise;
287
+ active.expiryTimer = setTimeout(() => {
288
+ if (inflight.get(request.commandId)?.promise !== promise)
289
+ return;
290
+ active.expired = true;
291
+ reservations.delete(request.commandId);
292
+ inflight.delete(request.commandId);
293
+ resolveExpiry();
294
+ active.expiryAckSent = true;
295
+ void sendAck(rejected(request, 'expired'));
296
+ }, Math.max(0, request.expiresAt - current));
297
+ inflight.set(request.commandId, active);
298
+ try {
299
+ await Promise.race([promise, expiry]);
300
+ }
301
+ finally {
302
+ if (inflight.get(request.commandId)?.promise === promise) {
303
+ inflight.delete(request.commandId);
304
+ }
305
+ }
306
+ return;
307
+ }
308
+ if (ack !== undefined)
309
+ await sendAck(ack);
310
+ };
311
+ const emit = (event) => {
312
+ if (disposed || event.sequence <= lastSequence)
313
+ return;
314
+ lastSequence = event.sequence;
315
+ const message = {
316
+ type: 'data-surface.event',
317
+ version: 1,
318
+ sessionId: options.sessionId,
319
+ source: options.source,
320
+ sequence: event.sequence,
321
+ identity: event.identity,
322
+ revision: event.revision,
323
+ event: event.type,
324
+ ...(event.command ? { command: event.command } : {}),
325
+ ...(event.result ? { result: event.result } : {}),
326
+ };
327
+ void Promise.resolve(options.transport.send(cloneMessage(message))).catch(() => undefined);
328
+ };
329
+ const unsubscribeTransport = options.transport.subscribe((message, peer) => {
330
+ void receive(message, peer);
331
+ });
332
+ const unsubscribeRegistry = options.registry.subscribe(emit);
333
+ return {
334
+ sessionId: options.sessionId,
335
+ source: options.source,
336
+ dispose() {
337
+ if (disposed)
338
+ return;
339
+ disposed = true;
340
+ unsubscribeTransport();
341
+ unsubscribeRegistry();
342
+ replay.clear();
343
+ },
344
+ receive,
345
+ };
346
+ }
347
+ /** Compatibility alias that makes the browser role explicit at call sites. */
348
+ export const createDataSurfaceBridge = createDataSurfaceBrowserBridge;
package/dist/index.d.ts CHANGED
@@ -13,7 +13,9 @@
13
13
  */
14
14
  export * from './components/forms/index.js';
15
15
  export * from './components/module/index.js';
16
+ export * from './data-surface.js';
16
17
  export * from './hooks/index.js';
17
18
  export { default as Provider } from './Provider.svelte';
18
19
  export * from './state/index.js';
20
+ export { useWebMcpTool, type WebMcpToolSpec } from './web/webmcp.svelte.js';
19
21
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAGH,cAAc,6BAA6B,CAAC;AAE5C,cAAc,8BAA8B,CAAC;AAE7C,cAAc,kBAAkB,CAAC;AAEjC,OAAO,EAAE,OAAO,IAAI,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAExD,cAAc,kBAAkB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;GAYG;AAGH,cAAc,6BAA6B,CAAC;AAE5C,cAAc,8BAA8B,CAAC;AAE7C,cAAc,mBAAmB,CAAC;AAElC,cAAc,kBAAkB,CAAC;AAEjC,OAAO,EAAE,OAAO,IAAI,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAExD,cAAc,kBAAkB,CAAC;AAEjC,OAAO,EAAE,aAAa,EAAE,KAAK,cAAc,EAAE,MAAM,wBAAwB,CAAC"}
package/dist/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ /// <reference path="./web/webmcp.d.ts" />
1
2
  /**
2
3
  * @happyvertical/smrt-svelte
3
4
  *
@@ -15,9 +16,13 @@
15
16
  export * from './components/forms/index.js';
16
17
  // Module components (for dynamic module UI rendering)
17
18
  export * from './components/module/index.js';
19
+ // Authenticated browser-side data-surface command/ack/event bridge
20
+ export * from './data-surface.js';
18
21
  // Hooks
19
22
  export * from './hooks/index.js';
20
23
  // Core - App wrapper/provider
21
24
  export { default as Provider } from './Provider.svelte';
22
25
  // State management
23
26
  export * from './state/index.js';
27
+ // Opt-in browser WebMCP lifecycle primitive.
28
+ export { useWebMcpTool } from './web/webmcp.svelte.js';
@@ -27,6 +27,8 @@ export interface FieldDefinition {
27
27
  readable?: boolean;
28
28
  writable?: boolean;
29
29
  constraints?: ControlConstraints;
30
+ /** Optional WebMCP schema override for structured field values. */
31
+ webMcpSchema?: Record<string, unknown>;
30
32
  options?: ControlOption[];
31
33
  unit?: string;
32
34
  clear?: () => void;
@@ -1 +1 @@
1
- {"version":3,"file":"form-context.d.ts","sourceRoot":"","sources":["../../src/state/form-context.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EACV,kBAAkB,EAClB,0BAA0B,EAC1B,WAAW,EACX,aAAa,EACb,mBAAmB,EACnB,kBAAkB,EACnB,MAAM,8BAA8B,CAAC;AAGtC;;GAEG;AACH,qCAAqC;AACrC,MAAM,MAAM,aAAa,GACrB,MAAM,GACN,OAAO,GACP,UAAU,GACV,QAAQ,GACR,SAAS,GACT,UAAU,GACV,WAAW,GACX,aAAa,GACb,OAAO,GACP,OAAO,GACP,QAAQ,GACR,UAAU,CAAC;AAEf,MAAM,WAAW,eAAe;IAC9B,kDAAkD;IAClD,IAAI,EAAE,MAAM,CAAC;IACb,iBAAiB;IACjB,IAAI,EAAE,aAAa,CAAC;IACpB,2BAA2B;IAC3B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,uDAAuD;IACvD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,0BAA0B;IAC1B,QAAQ,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;IACnC,8BAA8B;IAC9B,QAAQ,EAAE,MAAM,OAAO,CAAC;IACxB,6DAA6D;IAC7D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,eAAe,CAAC,EAAE,WAAW,CAAC;IAC9B,WAAW,CAAC,EAAE,kBAAkB,CAAC;IACjC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,WAAW,CAAC,EAAE,kBAAkB,CAAC;IACjC,OAAO,CAAC,EAAE,aAAa,EAAE,CAAC;IAC1B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,IAAI,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,IAAI,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IACpB,SAAS,CAAC,EAAE,CAAC,UAAU,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;IAC1C,QAAQ,CAAC,EAAE,MAAM,OAAO,CAAC;IACzB,QAAQ,CAAC,EAAE,MAAM,mBAAmB,CAAC;CACtC;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,mBAAmB;IACnB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,CAAC;IAClC,qCAAqC;IACrC,aAAa,EAAE,CAAC,KAAK,EAAE,eAAe,KAAK,IAAI,CAAC;IAChD,yBAAyB;IACzB,eAAe,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,wDAAwD;IACxD,cAAc,EAAE,MAAM,eAAe,EAAE,CAAC;IACxC,6CAA6C;IAC7C,QAAQ,CAAC,eAAe,EAAE,OAAO,CAAC;IAClC,+CAA+C;IAC/C,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC;IAC/B,yCAAyC;IACzC,eAAe,EAAE,MAAM,IAAI,CAAC;IAC5B,8EAA8E;IAC9E,QAAQ,CAAC,mBAAmB,EAAE,0BAA0B,CAAC;IACzD,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED;;GAEG;AACH,eAAO,MAAM,aAAa,eAAsB,CAAC;AAEjD;;GAEG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,eAAe,GAAG,IAAI,CAEzD;AAED;;;GAGG;AACH,wBAAgB,cAAc,IAAI,eAAe,CAUhD;AAED;;GAEG;AACH,wBAAgB,iBAAiB,IAAI,eAAe,GAAG,IAAI,CAE1D"}
1
+ {"version":3,"file":"form-context.d.ts","sourceRoot":"","sources":["../../src/state/form-context.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EACV,kBAAkB,EAClB,0BAA0B,EAC1B,WAAW,EACX,aAAa,EACb,mBAAmB,EACnB,kBAAkB,EACnB,MAAM,8BAA8B,CAAC;AAGtC;;GAEG;AACH,qCAAqC;AACrC,MAAM,MAAM,aAAa,GACrB,MAAM,GACN,OAAO,GACP,UAAU,GACV,QAAQ,GACR,SAAS,GACT,UAAU,GACV,WAAW,GACX,aAAa,GACb,OAAO,GACP,OAAO,GACP,QAAQ,GACR,UAAU,CAAC;AAEf,MAAM,WAAW,eAAe;IAC9B,kDAAkD;IAClD,IAAI,EAAE,MAAM,CAAC;IACb,iBAAiB;IACjB,IAAI,EAAE,aAAa,CAAC;IACpB,2BAA2B;IAC3B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,uDAAuD;IACvD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,0BAA0B;IAC1B,QAAQ,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;IACnC,8BAA8B;IAC9B,QAAQ,EAAE,MAAM,OAAO,CAAC;IACxB,6DAA6D;IAC7D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,eAAe,CAAC,EAAE,WAAW,CAAC;IAC9B,WAAW,CAAC,EAAE,kBAAkB,CAAC;IACjC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,WAAW,CAAC,EAAE,kBAAkB,CAAC;IACjC,mEAAmE;IACnE,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACvC,OAAO,CAAC,EAAE,aAAa,EAAE,CAAC;IAC1B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,IAAI,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,IAAI,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IACpB,SAAS,CAAC,EAAE,CAAC,UAAU,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;IAC1C,QAAQ,CAAC,EAAE,MAAM,OAAO,CAAC;IACzB,QAAQ,CAAC,EAAE,MAAM,mBAAmB,CAAC;CACtC;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,mBAAmB;IACnB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,CAAC;IAClC,qCAAqC;IACrC,aAAa,EAAE,CAAC,KAAK,EAAE,eAAe,KAAK,IAAI,CAAC;IAChD,yBAAyB;IACzB,eAAe,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,wDAAwD;IACxD,cAAc,EAAE,MAAM,eAAe,EAAE,CAAC;IACxC,6CAA6C;IAC7C,QAAQ,CAAC,eAAe,EAAE,OAAO,CAAC;IAClC,+CAA+C;IAC/C,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC;IAC/B,yCAAyC;IACzC,eAAe,EAAE,MAAM,IAAI,CAAC;IAC5B,8EAA8E;IAC9E,QAAQ,CAAC,mBAAmB,EAAE,0BAA0B,CAAC;IACzD,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED;;GAEG;AACH,eAAO,MAAM,aAAa,eAAsB,CAAC;AAEjD;;GAEG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,eAAe,GAAG,IAAI,CAEzD;AAED;;;GAGG;AACH,wBAAgB,cAAc,IAAI,eAAe,CAUhD;AAED;;GAEG;AACH,wBAAgB,iBAAiB,IAAI,eAAe,GAAG,IAAI,CAE1D"}
@@ -0,0 +1,14 @@
1
+ <script lang="ts">
2
+ import { useWebMcpTool } from '../webmcp.svelte.js';
3
+
4
+ let { version = 1 }: { version?: number } = $props();
5
+
6
+ useWebMcpTool(() => ({
7
+ name: `harness_tool_${version}`,
8
+ description: 'A lifecycle test tool',
9
+ inputSchema: { type: 'object' },
10
+ execute: () => String(version),
11
+ }));
12
+ </script>
13
+
14
+ <span>{version}</span>
@@ -0,0 +1,7 @@
1
+ type $$ComponentProps = {
2
+ version?: number;
3
+ };
4
+ declare const WebmcpHarness: import("svelte").Component<$$ComponentProps, {}, "">;
5
+ type WebmcpHarness = ReturnType<typeof WebmcpHarness>;
6
+ export default WebmcpHarness;
7
+ //# sourceMappingURL=webmcp-harness.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webmcp-harness.svelte.d.ts","sourceRoot":"","sources":["../../../src/web/__tests__/webmcp-harness.svelte.ts"],"names":[],"mappings":"AAKC,KAAK,gBAAgB,GAAI;IAAE,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAkB/C,QAAA,MAAM,aAAa,sDAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}
@@ -0,0 +1,39 @@
1
+ import { render } from '@testing-library/svelte';
2
+ import { tick } from 'svelte';
3
+ import { afterEach, describe, expect, it } from 'vitest';
4
+ import Harness from './webmcp-harness.svelte';
5
+ function installModelContext() {
6
+ const registered = [];
7
+ document.modelContext = {
8
+ registerTool(tool, options) {
9
+ registered.push({ name: tool.name, signal: options?.signal });
10
+ },
11
+ };
12
+ return registered;
13
+ }
14
+ afterEach(() => {
15
+ delete document.modelContext;
16
+ });
17
+ describe('useWebMcpTool', () => {
18
+ it('registers on mount, aborts on spec changes, and aborts on unmount', async () => {
19
+ const registered = installModelContext();
20
+ const view = render(Harness, { props: { version: 1 } });
21
+ await tick();
22
+ expect(registered).toHaveLength(1);
23
+ expect(registered[0].name).toBe('harness_tool_1');
24
+ expect(registered[0].signal?.aborted).toBe(false);
25
+ await view.rerender({ version: 2 });
26
+ await tick();
27
+ expect(registered).toHaveLength(2);
28
+ expect(registered[0].signal?.aborted).toBe(true);
29
+ expect(registered[1].name).toBe('harness_tool_2');
30
+ view.unmount();
31
+ expect(registered[1].signal?.aborted).toBe(true);
32
+ });
33
+ it('is a no-op when WebMCP is unavailable', async () => {
34
+ const view = render(Harness);
35
+ await tick();
36
+ expect(view.container).toHaveTextContent('1');
37
+ view.unmount();
38
+ });
39
+ });
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Ambient WebMCP types. Chrome currently exposes this origin-trial API without
3
+ * shipping it in TypeScript's DOM library.
4
+ */
5
+
6
+ import type { WebMcpToolSpec } from './webmcp.svelte.js';
7
+
8
+ declare global {
9
+ interface Document {
10
+ modelContext?: WebMcpModelContext;
11
+ }
12
+
13
+ interface WebMcpModelContext {
14
+ registerTool(
15
+ tool: WebMcpToolSpec,
16
+ options?: { signal?: AbortSignal },
17
+ ): void;
18
+ }
19
+ }
20
+
21
+ export {};