@happyvertical/smrt-web 0.43.3 → 0.43.4
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.
- package/AGENTS.md +29 -1
- package/README.md +77 -0
- package/dist/chunks/{src-D9RvoJSQ.js → src-n14q6RHC.js} +335 -43
- package/dist/chunks/src-n14q6RHC.js.map +1 -0
- package/dist/index.d.ts +77 -3
- package/dist/index.js +2 -2
- package/dist/webmcp.d.ts +59 -2
- package/dist/webmcp.js +1 -1
- package/package.json +1 -1
- package/dist/chunks/src-D9RvoJSQ.js.map +0 -1
package/AGENTS.md
CHANGED
|
@@ -38,7 +38,7 @@ module you are editing. This file keeps what holds in every module.
|
|
|
38
38
|
| `index.ts` hooks + `durable-store.ts` | the six capability hook points, hook error isolation, the no-op guarantee, and the shared durable-store namespacing/wipe registry | [agents/capability-seam.md](agents/capability-seam.md) |
|
|
39
39
|
| `offline/` | durable offline writes — config, sync-apply-only replay, idempotency, the shared namespace-keyed engine, and Web Locks leader election | [agents/offline-outbox.md](agents/offline-outbox.md) |
|
|
40
40
|
| `sse-client.ts` | the client half of live cache invalidation — the app-wide subscriber, the wire contract it consumes, and SSE-vs-polling behaviour | [agents/live-invalidation.md](agents/live-invalidation.md) |
|
|
41
|
-
| `webmcp.ts` | framework-agnostic WebMCP registrar;
|
|
41
|
+
| `webmcp.ts` | framework-agnostic WebMCP registrar; validates an optionally bounded/namespaced, effect-filtered prospective set atomically, keeps legacy list-backed tools on collection state, and executes canonical tool-only definitions directly through REST fetchers | — |
|
|
42
42
|
| `persistence/` + `update-state.ts` | the read-cache rehydrate capability and the framework-free `updateAvailable` primitive (bundle + contract signals) | [agents/version-persistence.md](agents/version-persistence.md) |
|
|
43
43
|
| `data-query.ts` | dependency-free browser mirror and defensive response normalizer for the canonical bounded data-query envelope (#2444) | — |
|
|
44
44
|
| `remote-query.ts` | query-shaped remote pages over a `SmrtWebCollection`, with keyed stale cache, execution modes, cancellation/latest-query-wins, and optional query-scoped live subscriptions (#2445) | — |
|
|
@@ -61,6 +61,34 @@ module you are editing. This file keeps what holds in every module.
|
|
|
61
61
|
|
|
62
62
|
## Conventions
|
|
63
63
|
|
|
64
|
+
- **WebMCP definition mirror** — `WebMcpToolDefinition` textually mirrors the
|
|
65
|
+
generated `@happyvertical/smrt-virt-web` / physical `@smrt/web` declaration.
|
|
66
|
+
It is transport-complete and does not imply that a list-materialized client
|
|
67
|
+
collection exists. Keep the mirror dependency-free. Register canonical tools
|
|
68
|
+
or legacy collection definitions for overlapping collections; when composing
|
|
69
|
+
the two forms, keep their names and collection/action identities disjoint.
|
|
70
|
+
Duplicates fail atomically before registration.
|
|
71
|
+
With no exposure policy, only `read` effects are selected; broader effects
|
|
72
|
+
require explicit opt-in, and undeclared custom actions are destructive.
|
|
73
|
+
Direct mutations invalidate their
|
|
74
|
+
own and relationship-derived collection names through the public
|
|
75
|
+
`invalidateSmrtWebCollections()` seam when the host supplies its shared
|
|
76
|
+
`SmrtWebClient`. Legacy `filter` callbacks receive complete collection
|
|
77
|
+
metadata; canonical definitions use `filterTool`. Supplying either filter for
|
|
78
|
+
definitions of the other kind fails closed rather than ignoring the predicate
|
|
79
|
+
or fabricating incomplete metadata for a policy decision. Filters and fetcher
|
|
80
|
+
resolvers receive isolated value snapshots, so integrations must key external
|
|
81
|
+
state by stable values such as collection/action rather than definition object
|
|
82
|
+
identity.
|
|
83
|
+
Canonical writes validate that shared client handle before registration, and
|
|
84
|
+
string or structured `{ error }` REST envelopes fail before cache
|
|
85
|
+
invalidation. The private `__smrt_options` GET sentinel is reserved only for
|
|
86
|
+
no-path single-options-bag actions; positional actions preserve a legitimate
|
|
87
|
+
parameter with that name.
|
|
88
|
+
This capability policy is not authorization:
|
|
89
|
+
the authenticated REST surface remains the auth, tenant, field-write, and
|
|
90
|
+
sensitive-data boundary.
|
|
91
|
+
|
|
64
92
|
- **No inter-smrt dependencies** — depends only on TanStack packages
|
|
65
93
|
(dependency-DAG guardrails). Definitions and fetchers arrive as arguments.
|
|
66
94
|
- **Data-query mirror** — `data-query.ts` mirrors the portable
|
package/README.md
CHANGED
|
@@ -133,6 +133,83 @@ core.
|
|
|
133
133
|
| Version awareness | `createUpdateState` |
|
|
134
134
|
| WebMCP | `registerWebMcpTools` |
|
|
135
135
|
|
|
136
|
+
## WebMCP capability exposure
|
|
137
|
+
|
|
138
|
+
`registerWebMcpTools()` is secure by default: omitting an exposure policy
|
|
139
|
+
registers only `read` tools. CRUD effects are fixed (`list`/`get` are `read`,
|
|
140
|
+
`create`/`update` are `write`, and `delete` is `destructive`). A custom action
|
|
141
|
+
without declared metadata is treated as destructive, non-idempotent, and open
|
|
142
|
+
world. Declare safer custom-action semantics in the route metadata only when
|
|
143
|
+
they are true:
|
|
144
|
+
|
|
145
|
+
```ts
|
|
146
|
+
@smrt({
|
|
147
|
+
api: {
|
|
148
|
+
routes: {
|
|
149
|
+
preview: {
|
|
150
|
+
method: 'GET',
|
|
151
|
+
effect: 'read',
|
|
152
|
+
idempotent: true,
|
|
153
|
+
openWorld: false,
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
})
|
|
158
|
+
class Report extends SmrtObject {}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Opt into broader capabilities explicitly. `namespace` prevents cross-surface
|
|
162
|
+
name collisions, an explicit `maxTools` bounds the selected set, and duplicate
|
|
163
|
+
names or stable collection/action identities reject the entire call before the
|
|
164
|
+
first browser registration. No implicit budget is applied to whole-manifest
|
|
165
|
+
read registration:
|
|
166
|
+
|
|
167
|
+
```ts
|
|
168
|
+
registerWebMcpTools(definitions, {
|
|
169
|
+
effects: ['read', 'write', 'destructive'],
|
|
170
|
+
namespace: 'admin',
|
|
171
|
+
maxTools: 32,
|
|
172
|
+
filter: (collection, tool) => collection.fields.tenantId !== undefined,
|
|
173
|
+
filterTool: (tool) => tool.collection === 'reports',
|
|
174
|
+
});
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
The returned disposer also exposes a `ready` promise. Await it when the host
|
|
178
|
+
must report browser-side registration rejection; any rejected tool aborts all
|
|
179
|
+
sibling registrations from that call before `ready` rejects.
|
|
180
|
+
|
|
181
|
+
`filter` receives legacy collection metadata; `filterTool` receives canonical
|
|
182
|
+
per-tool definitions. Configuring only one filter while registering definitions
|
|
183
|
+
for the other filter kind fails closed; canonical tools do not carry complete
|
|
184
|
+
collection field metadata, and legacy descriptors do not satisfy the canonical
|
|
185
|
+
filter contract. Policy callbacks and fetcher resolvers receive isolated value
|
|
186
|
+
snapshots; key host-side maps by stable values such as collection/action or tool
|
|
187
|
+
name, not definition object identity.
|
|
188
|
+
|
|
189
|
+
Do not concatenate complete legacy and canonical definition sets for the same
|
|
190
|
+
collections. Their duplicate tool names or collection/action identities reject
|
|
191
|
+
the registration atomically. Prefer the canonical set for complete generated
|
|
192
|
+
coverage, or compose only disjoint legacy and canonical subsets.
|
|
193
|
+
|
|
194
|
+
WebMCP policy controls which capabilities a page advertises; it is not an
|
|
195
|
+
authorization boundary. Execution still uses the page's authenticated REST
|
|
196
|
+
transport, whose auth, tenancy, writable-field, and sensitive-field guards must
|
|
197
|
+
remain enabled. All application-derived tool results are annotated as untrusted
|
|
198
|
+
content, including mutation responses.
|
|
199
|
+
|
|
200
|
+
Policy only narrows the actions already exposed by the generated API metadata.
|
|
201
|
+
Legacy descriptors outside their collection's `actions` set reject the whole
|
|
202
|
+
registration, and intrinsic CRUD effects cannot be relabeled by caller data.
|
|
203
|
+
|
|
204
|
+
Migration note: registrations that previously relied on every descriptor being
|
|
205
|
+
exposed must now pass `effects: ['read', 'write', 'destructive']`. Prefer a
|
|
206
|
+
narrower allowlist for each browser surface. Integrations that previously keyed
|
|
207
|
+
filter or resolver state by definition object identity must migrate to stable
|
|
208
|
+
name or collection/action keys. If both legacy and canonical definition arrays
|
|
209
|
+
are available, select one complete source or remove overlaps before combining
|
|
210
|
+
them. Set `maxTools` explicitly on surfaces that need a hard capability budget;
|
|
211
|
+
overflow rejects the complete registration rather than truncating it.
|
|
212
|
+
|
|
136
213
|
## Development
|
|
137
214
|
|
|
138
215
|
```bash
|
|
@@ -2116,38 +2116,266 @@ function getModelContext() {
|
|
|
2116
2116
|
if (mc && typeof mc.registerTool === "function") return mc;
|
|
2117
2117
|
}
|
|
2118
2118
|
function registerWebMcpTools(definitions, options = {}) {
|
|
2119
|
+
const exposure = validateExposurePolicy(options);
|
|
2119
2120
|
const ctx = getModelContext();
|
|
2120
|
-
if (!ctx) return () => {};
|
|
2121
|
+
if (!ctx) return registrationDisposer(() => {}, Promise.resolve());
|
|
2121
2122
|
const basePath = options.basePath ?? "/api/v1";
|
|
2123
|
+
const client = options.client;
|
|
2124
|
+
const { tools, allowedEffects } = selectProspectiveTools(definitions, options, exposure);
|
|
2125
|
+
if (client && tools.some((tool) => tool.kind === "canonical" && tool.effect !== "read")) validateSmrtWebClient(client);
|
|
2122
2126
|
const controller = new AbortController();
|
|
2123
2127
|
const collections = /* @__PURE__ */ new Map();
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
fetchers,
|
|
2130
|
-
basePath,
|
|
2131
|
-
fetchFn: options.fetchFn,
|
|
2132
|
-
...options.client ? { client: options.client } : {},
|
|
2133
|
-
...options.scope ? { scope: options.scope } : {}
|
|
2134
|
-
});
|
|
2135
|
-
collections.set(definition, collection);
|
|
2136
|
-
for (const descriptor of descriptors) {
|
|
2137
|
-
if (options.filter && !options.filter(definition, descriptor)) continue;
|
|
2138
|
-
ctx.registerTool({
|
|
2139
|
-
name: descriptor.name,
|
|
2140
|
-
description: descriptor.description,
|
|
2141
|
-
inputSchema: descriptor.inputSchema,
|
|
2142
|
-
annotations: { readOnlyHint: descriptor.readOnly },
|
|
2143
|
-
execute: (args) => dispatch(fetchers, collection, definition, descriptor.action, descriptor.route, args ?? {})
|
|
2144
|
-
}, { signal: controller.signal });
|
|
2145
|
-
}
|
|
2146
|
-
}
|
|
2147
|
-
return () => {
|
|
2128
|
+
const collectionFetchers = /* @__PURE__ */ new Map();
|
|
2129
|
+
let disposed = false;
|
|
2130
|
+
const dispose = () => {
|
|
2131
|
+
if (disposed) return;
|
|
2132
|
+
disposed = true;
|
|
2148
2133
|
controller.abort();
|
|
2149
2134
|
for (const collection of collections.values()) collection.cleanup().catch(() => void 0);
|
|
2150
2135
|
};
|
|
2136
|
+
const registrations = [];
|
|
2137
|
+
try {
|
|
2138
|
+
for (const tool of tools) {
|
|
2139
|
+
if (tool.kind === "legacy") {
|
|
2140
|
+
const { definition: definition2, descriptor } = tool;
|
|
2141
|
+
let fetchers2 = collectionFetchers.get(definition2);
|
|
2142
|
+
let collection = collections.get(definition2);
|
|
2143
|
+
if (!fetchers2 || !collection) {
|
|
2144
|
+
fetchers2 = options.resolveFetchers ? options.resolveFetchers(snapshotLegacyDefinition(definition2)) : createDefinitionFetchers(definition2, basePath, options.fetchFn);
|
|
2145
|
+
collection = createSmrtCollection(definition2, {
|
|
2146
|
+
fetchers: fetchers2,
|
|
2147
|
+
basePath,
|
|
2148
|
+
fetchFn: options.fetchFn,
|
|
2149
|
+
...client ? { client } : {},
|
|
2150
|
+
...options.scope ? { scope: options.scope } : {}
|
|
2151
|
+
});
|
|
2152
|
+
collectionFetchers.set(definition2, fetchers2);
|
|
2153
|
+
collections.set(definition2, collection);
|
|
2154
|
+
}
|
|
2155
|
+
registrations.push(Promise.resolve(ctx.registerTool({
|
|
2156
|
+
name: tool.name,
|
|
2157
|
+
description: descriptor.description,
|
|
2158
|
+
inputSchema: descriptor.inputSchema,
|
|
2159
|
+
annotations: annotationsFor(tool),
|
|
2160
|
+
execute: guardedExecute(tool, allowedEffects, () => disposed, (args) => dispatchCollection(fetchers2, collection, definition2, descriptor.action, descriptor.route, args))
|
|
2161
|
+
}, { signal: controller.signal })));
|
|
2162
|
+
continue;
|
|
2163
|
+
}
|
|
2164
|
+
const { definition } = tool;
|
|
2165
|
+
const fetchers = options.resolveToolFetchers ? options.resolveToolFetchers(snapshotCanonicalDefinition(definition, {
|
|
2166
|
+
effect: tool.effect,
|
|
2167
|
+
destructive: tool.destructive,
|
|
2168
|
+
idempotent: tool.idempotent,
|
|
2169
|
+
openWorld: tool.openWorld
|
|
2170
|
+
})) : createDefinitionFetchers({
|
|
2171
|
+
name: definition.collection,
|
|
2172
|
+
endpoint: definition.endpoint
|
|
2173
|
+
}, basePath, options.fetchFn);
|
|
2174
|
+
registrations.push(Promise.resolve(ctx.registerTool({
|
|
2175
|
+
name: tool.name,
|
|
2176
|
+
description: definition.description,
|
|
2177
|
+
inputSchema: definition.inputSchema,
|
|
2178
|
+
annotations: annotationsFor(tool),
|
|
2179
|
+
execute: guardedExecute(tool, allowedEffects, () => disposed, (args) => dispatchDirect(fetchers, definition, args, client))
|
|
2180
|
+
}, { signal: controller.signal })));
|
|
2181
|
+
}
|
|
2182
|
+
} catch (error) {
|
|
2183
|
+
Promise.all(registrations).catch(() => void 0);
|
|
2184
|
+
dispose();
|
|
2185
|
+
throw error;
|
|
2186
|
+
}
|
|
2187
|
+
const ready = Promise.all(registrations).then(() => void 0).catch((error) => {
|
|
2188
|
+
dispose();
|
|
2189
|
+
throw error;
|
|
2190
|
+
});
|
|
2191
|
+
ready.catch(() => void 0);
|
|
2192
|
+
return registrationDisposer(dispose, ready);
|
|
2193
|
+
}
|
|
2194
|
+
function registrationDisposer(dispose, ready) {
|
|
2195
|
+
return Object.assign(dispose, { ready });
|
|
2196
|
+
}
|
|
2197
|
+
var VALID_EFFECTS = [
|
|
2198
|
+
"read",
|
|
2199
|
+
"write",
|
|
2200
|
+
"destructive"
|
|
2201
|
+
];
|
|
2202
|
+
var NAMESPACE_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_-]*$/;
|
|
2203
|
+
function validateExposurePolicy(options) {
|
|
2204
|
+
const effects = options.effects ?? ["read"];
|
|
2205
|
+
for (const effect of effects) if (!VALID_EFFECTS.includes(effect)) throw new Error(`Invalid WebMCP effect: ${String(effect)}`);
|
|
2206
|
+
const maxTools = options.maxTools;
|
|
2207
|
+
if (maxTools !== void 0 && (!Number.isSafeInteger(maxTools) || maxTools < 0)) throw new Error("WebMCP maxTools must be a non-negative safe integer");
|
|
2208
|
+
const namespace = options.namespace;
|
|
2209
|
+
if (namespace !== void 0 && !NAMESPACE_PATTERN.test(namespace)) throw new Error("WebMCP namespace must start with an alphanumeric character and contain only letters, numbers, underscores, or hyphens");
|
|
2210
|
+
return {
|
|
2211
|
+
allowedEffects: new Set(effects),
|
|
2212
|
+
...maxTools !== void 0 ? { maxTools } : {},
|
|
2213
|
+
...namespace !== void 0 ? { namespace } : {}
|
|
2214
|
+
};
|
|
2215
|
+
}
|
|
2216
|
+
function selectProspectiveTools(definitions, options, exposure) {
|
|
2217
|
+
const { allowedEffects, maxTools, namespace } = exposure;
|
|
2218
|
+
const stableDefinitions = definitions.map((definition) => snapshotValue(definition));
|
|
2219
|
+
const tools = [];
|
|
2220
|
+
for (const definition of stableDefinitions) {
|
|
2221
|
+
if (isCanonicalToolDefinition(definition)) {
|
|
2222
|
+
const semantics = actionSemantics(definition.action, definition);
|
|
2223
|
+
if (!allowedEffects.has(semantics.effect)) continue;
|
|
2224
|
+
if (options.filter && !options.filterTool) throw new Error("[smrt-web] canonical WebMCP definitions require filterTool when filter is configured");
|
|
2225
|
+
const stableDefinition2 = snapshotCanonicalDefinition(definition, semantics);
|
|
2226
|
+
if (options.filterTool && !options.filterTool(snapshotCanonicalDefinition(stableDefinition2, semantics))) continue;
|
|
2227
|
+
tools.push({
|
|
2228
|
+
kind: "canonical",
|
|
2229
|
+
definition: stableDefinition2,
|
|
2230
|
+
descriptor: stableDefinition2,
|
|
2231
|
+
name: qualifiedToolName(stableDefinition2.name, namespace),
|
|
2232
|
+
identity: `${stableDefinition2.collection}#${stableDefinition2.action}`,
|
|
2233
|
+
...semantics
|
|
2234
|
+
});
|
|
2235
|
+
continue;
|
|
2236
|
+
}
|
|
2237
|
+
const stableDefinition = snapshotLegacyDefinition(definition);
|
|
2238
|
+
for (const descriptor of stableDefinition.toolDescriptors ?? []) {
|
|
2239
|
+
if (!stableDefinition.actions.includes(descriptor.action)) throw new Error(`WebMCP tool ${descriptor.name} exposes action ${descriptor.action} outside ${stableDefinition.name}'s allowed actions`);
|
|
2240
|
+
const semantics = actionSemantics(descriptor.action, descriptor);
|
|
2241
|
+
if (!allowedEffects.has(semantics.effect)) continue;
|
|
2242
|
+
if (options.filterTool && !options.filter) throw new Error("[smrt-web] legacy WebMCP definitions require filter when filterTool is configured");
|
|
2243
|
+
const stableDescriptor = snapshotLegacyDescriptor(descriptor, semantics);
|
|
2244
|
+
if (options.filter && !options.filter(snapshotLegacyDefinition(stableDefinition), snapshotLegacyDescriptor(stableDescriptor, semantics))) continue;
|
|
2245
|
+
tools.push({
|
|
2246
|
+
kind: "legacy",
|
|
2247
|
+
definition: stableDefinition,
|
|
2248
|
+
descriptor: stableDescriptor,
|
|
2249
|
+
name: qualifiedToolName(stableDescriptor.name, namespace),
|
|
2250
|
+
identity: `${stableDefinition.name}#${stableDescriptor.action}`,
|
|
2251
|
+
...semantics
|
|
2252
|
+
});
|
|
2253
|
+
}
|
|
2254
|
+
}
|
|
2255
|
+
validateProspectiveTools(tools, maxTools);
|
|
2256
|
+
return {
|
|
2257
|
+
tools,
|
|
2258
|
+
allowedEffects
|
|
2259
|
+
};
|
|
2260
|
+
}
|
|
2261
|
+
function qualifiedToolName(name, namespace) {
|
|
2262
|
+
return namespace ? `${namespace}_${name}` : name;
|
|
2263
|
+
}
|
|
2264
|
+
function actionSemantics(action, declared) {
|
|
2265
|
+
switch (action) {
|
|
2266
|
+
case "list":
|
|
2267
|
+
case "get": return {
|
|
2268
|
+
effect: "read",
|
|
2269
|
+
destructive: false,
|
|
2270
|
+
idempotent: true,
|
|
2271
|
+
openWorld: false
|
|
2272
|
+
};
|
|
2273
|
+
case "create": return {
|
|
2274
|
+
effect: "write",
|
|
2275
|
+
destructive: true,
|
|
2276
|
+
idempotent: false,
|
|
2277
|
+
openWorld: false
|
|
2278
|
+
};
|
|
2279
|
+
case "update": return {
|
|
2280
|
+
effect: "write",
|
|
2281
|
+
destructive: true,
|
|
2282
|
+
idempotent: true,
|
|
2283
|
+
openWorld: false
|
|
2284
|
+
};
|
|
2285
|
+
case "delete": return {
|
|
2286
|
+
effect: "destructive",
|
|
2287
|
+
destructive: true,
|
|
2288
|
+
idempotent: true,
|
|
2289
|
+
openWorld: false
|
|
2290
|
+
};
|
|
2291
|
+
default: {
|
|
2292
|
+
const effect = VALID_EFFECTS.includes(declared.effect) ? declared.effect : "destructive";
|
|
2293
|
+
return {
|
|
2294
|
+
effect,
|
|
2295
|
+
destructive: effect !== "read",
|
|
2296
|
+
idempotent: declared.idempotent ?? false,
|
|
2297
|
+
openWorld: declared.openWorld ?? true
|
|
2298
|
+
};
|
|
2299
|
+
}
|
|
2300
|
+
}
|
|
2301
|
+
}
|
|
2302
|
+
function snapshotRoute(route) {
|
|
2303
|
+
return route ? snapshotValue(route) : void 0;
|
|
2304
|
+
}
|
|
2305
|
+
function snapshotValue(value, active = /* @__PURE__ */ new WeakSet(), path = "$") {
|
|
2306
|
+
if (value && typeof value === "object") {
|
|
2307
|
+
if (active.has(value)) throw new Error(`[smrt-web] WebMCP definitions must be acyclic (cycle at ${path})`);
|
|
2308
|
+
active.add(value);
|
|
2309
|
+
}
|
|
2310
|
+
try {
|
|
2311
|
+
if (Array.isArray(value)) return value.map((entry, index) => snapshotValue(entry, active, `${path}[${index}]`));
|
|
2312
|
+
if (value && typeof value === "object") {
|
|
2313
|
+
const snapshot = {};
|
|
2314
|
+
for (const [key, entry] of Object.entries(value)) snapshot[key] = snapshotValue(entry, active, `${path}.${key}`);
|
|
2315
|
+
return snapshot;
|
|
2316
|
+
}
|
|
2317
|
+
return value;
|
|
2318
|
+
} finally {
|
|
2319
|
+
if (value && typeof value === "object") active.delete(value);
|
|
2320
|
+
}
|
|
2321
|
+
}
|
|
2322
|
+
function snapshotLegacyDescriptor(descriptor, semantics) {
|
|
2323
|
+
return snapshotValue({
|
|
2324
|
+
...descriptor,
|
|
2325
|
+
effect: semantics.effect,
|
|
2326
|
+
idempotent: semantics.idempotent,
|
|
2327
|
+
openWorld: semantics.openWorld,
|
|
2328
|
+
readOnly: semantics.effect === "read",
|
|
2329
|
+
route: snapshotRoute(descriptor.route)
|
|
2330
|
+
});
|
|
2331
|
+
}
|
|
2332
|
+
function snapshotLegacyDefinition(definition) {
|
|
2333
|
+
const snapshot = snapshotValue({
|
|
2334
|
+
...definition,
|
|
2335
|
+
actions: [...definition.actions]
|
|
2336
|
+
});
|
|
2337
|
+
snapshot.toolDescriptors = snapshot.toolDescriptors?.map((descriptor) => snapshotLegacyDescriptor(descriptor, actionSemantics(descriptor.action, descriptor)));
|
|
2338
|
+
return snapshot;
|
|
2339
|
+
}
|
|
2340
|
+
function snapshotCanonicalDefinition(definition, semantics) {
|
|
2341
|
+
return snapshotValue({
|
|
2342
|
+
...definition,
|
|
2343
|
+
effect: semantics.effect,
|
|
2344
|
+
idempotent: semantics.idempotent,
|
|
2345
|
+
openWorld: semantics.openWorld,
|
|
2346
|
+
readOnly: semantics.effect === "read",
|
|
2347
|
+
route: snapshotRoute(definition.route)
|
|
2348
|
+
});
|
|
2349
|
+
}
|
|
2350
|
+
function validateProspectiveTools(tools, maxTools) {
|
|
2351
|
+
if (maxTools !== void 0 && tools.length > maxTools) throw new Error(`WebMCP tool budget exceeded: ${tools.length} tools selected, maximum is ${maxTools}`);
|
|
2352
|
+
const names = /* @__PURE__ */ new Set();
|
|
2353
|
+
const identities = /* @__PURE__ */ new Set();
|
|
2354
|
+
for (const tool of tools) {
|
|
2355
|
+
if (names.has(tool.name)) throw new Error(`Duplicate WebMCP tool name: ${tool.name}`);
|
|
2356
|
+
names.add(tool.name);
|
|
2357
|
+
if (identities.has(tool.identity)) throw new Error(`Duplicate WebMCP tool identity: ${tool.identity}`);
|
|
2358
|
+
identities.add(tool.identity);
|
|
2359
|
+
}
|
|
2360
|
+
}
|
|
2361
|
+
function annotationsFor(tool) {
|
|
2362
|
+
return {
|
|
2363
|
+
readOnlyHint: tool.effect === "read",
|
|
2364
|
+
destructiveHint: tool.destructive,
|
|
2365
|
+
idempotentHint: tool.idempotent,
|
|
2366
|
+
openWorldHint: tool.openWorld,
|
|
2367
|
+
untrustedContentHint: true
|
|
2368
|
+
};
|
|
2369
|
+
}
|
|
2370
|
+
function guardedExecute(tool, allowedEffects, isDisposed, execute) {
|
|
2371
|
+
return (args) => {
|
|
2372
|
+
if (isDisposed()) throw new Error(`WebMCP tool ${tool.name} is no longer registered`);
|
|
2373
|
+
if (!allowedEffects.has(tool.effect)) throw new Error(`WebMCP policy no longer allows ${tool.effect} tool ${tool.name}`);
|
|
2374
|
+
return execute(args ?? {});
|
|
2375
|
+
};
|
|
2376
|
+
}
|
|
2377
|
+
function isCanonicalToolDefinition(definition) {
|
|
2378
|
+
return "collection" in definition && "readOnly" in definition;
|
|
2151
2379
|
}
|
|
2152
2380
|
function requireId(args, action) {
|
|
2153
2381
|
const id = args.id;
|
|
@@ -2167,7 +2395,7 @@ function listParams(args) {
|
|
|
2167
2395
|
if (args.where !== void 0) params.where = args.where;
|
|
2168
2396
|
return params;
|
|
2169
2397
|
}
|
|
2170
|
-
async function
|
|
2398
|
+
async function dispatchCollection(fetchers, collection, definition, action, route, args) {
|
|
2171
2399
|
switch (action) {
|
|
2172
2400
|
case "list": {
|
|
2173
2401
|
const rows = unwrapListResult(await fetchers.list(listParams(args)), definition.name);
|
|
@@ -2208,6 +2436,45 @@ async function dispatch(fetchers, collection, definition, action, route, args) {
|
|
|
2208
2436
|
return JSON.stringify(await collection.action(action, args, route));
|
|
2209
2437
|
}
|
|
2210
2438
|
}
|
|
2439
|
+
async function dispatchDirect(fetchers, definition, args, client) {
|
|
2440
|
+
let result;
|
|
2441
|
+
switch (definition.action) {
|
|
2442
|
+
case "list":
|
|
2443
|
+
if (!fetchers.list) throw new Error(`${definition.collection} has no list action`);
|
|
2444
|
+
result = unwrapListResult(await fetchers.list(listParams(args)), definition.collection);
|
|
2445
|
+
break;
|
|
2446
|
+
case "get":
|
|
2447
|
+
if (!fetchers.get) throw new Error(`${definition.collection} has no get action`);
|
|
2448
|
+
result = unwrapItemResult(await fetchers.get(requireIdentifier(args)), `get(${definition.collection})`);
|
|
2449
|
+
break;
|
|
2450
|
+
case "create":
|
|
2451
|
+
if (!fetchers.create) throw new Error(`${definition.collection} has no create action`);
|
|
2452
|
+
result = unwrapItemResult(await fetchers.create(args), `create(${definition.collection})`);
|
|
2453
|
+
break;
|
|
2454
|
+
case "update": {
|
|
2455
|
+
if (!fetchers.update) throw new Error(`${definition.collection} has no update action`);
|
|
2456
|
+
const id = requireId(args, "update");
|
|
2457
|
+
const { id: _id, ...body } = args;
|
|
2458
|
+
result = unwrapItemResult(await fetchers.update(id, body), `update(${definition.collection})`);
|
|
2459
|
+
break;
|
|
2460
|
+
}
|
|
2461
|
+
case "delete": {
|
|
2462
|
+
if (!fetchers.delete) throw new Error(`${definition.collection} has no delete action`);
|
|
2463
|
+
const id = requireId(args, "delete");
|
|
2464
|
+
throwIfSmrtWebError(await fetchers.delete(id), `delete(${definition.collection})`);
|
|
2465
|
+
result = {
|
|
2466
|
+
success: true,
|
|
2467
|
+
id
|
|
2468
|
+
};
|
|
2469
|
+
break;
|
|
2470
|
+
}
|
|
2471
|
+
default:
|
|
2472
|
+
if (!fetchers.custom) throw new Error(`${definition.collection} has no custom action fetcher`);
|
|
2473
|
+
result = throwIfSmrtWebError(await fetchers.custom(definition.action, args, definition.route), `${definition.action}(${definition.collection})`);
|
|
2474
|
+
}
|
|
2475
|
+
if (!definition.readOnly && client) invalidateSmrtWebCollections(client, [definition.collection, ...definition.relationships.map((relationship) => relationship.relatedCollection)]);
|
|
2476
|
+
return JSON.stringify(result);
|
|
2477
|
+
}
|
|
2211
2478
|
async function settleTransaction(transaction, collection, key, fallback) {
|
|
2212
2479
|
await transaction.isPersisted.promise;
|
|
2213
2480
|
const persisted = persistedMutationResults.get(collection)?.get(key);
|
|
@@ -2256,23 +2523,34 @@ function createHttpRequestError(collectionName, status, payload) {
|
|
|
2256
2523
|
return new SmrtWebRequestError(`[smrt-web] ${collectionName} request failed: ${detail.message ?? `HTTP ${status}`}`, payload, status, detail.code);
|
|
2257
2524
|
}
|
|
2258
2525
|
function unwrapListResult(result, collectionName) {
|
|
2526
|
+
throwIfSmrtWebError(result, `list(${collectionName})`);
|
|
2259
2527
|
if (Array.isArray(result)) return result;
|
|
2260
2528
|
if (result && typeof result === "object") {
|
|
2261
2529
|
const record = result;
|
|
2262
|
-
if (typeof record.error === "string") throw new SmrtWebRequestError(`[smrt-web] list(${collectionName}) failed: ${record.error}`, result);
|
|
2263
2530
|
if (Array.isArray(record.data)) return record.data;
|
|
2264
2531
|
}
|
|
2265
2532
|
throw new SmrtWebRequestError(`[smrt-web] list(${collectionName}) returned an unexpected payload shape`, result);
|
|
2266
2533
|
}
|
|
2267
2534
|
function unwrapItemResult(result, context) {
|
|
2535
|
+
throwIfSmrtWebError(result, context);
|
|
2268
2536
|
if (result && typeof result === "object" && !Array.isArray(result)) {
|
|
2269
2537
|
const record = result;
|
|
2270
|
-
if (typeof record.error === "string") throw new SmrtWebRequestError(`[smrt-web] ${context} failed: ${record.error}`, result);
|
|
2271
2538
|
if (record.data && typeof record.data === "object" && !Array.isArray(record.data)) return record.data;
|
|
2272
2539
|
return record;
|
|
2273
2540
|
}
|
|
2274
2541
|
throw new SmrtWebRequestError(`[smrt-web] ${context} returned an unexpected payload shape`, result);
|
|
2275
2542
|
}
|
|
2543
|
+
function throwIfSmrtWebError(result, context) {
|
|
2544
|
+
if (!result || typeof result !== "object" || Array.isArray(result)) return result;
|
|
2545
|
+
const error = result.error;
|
|
2546
|
+
if (typeof error === "string") throw new SmrtWebRequestError(`[smrt-web] ${context} failed: ${error}`, result);
|
|
2547
|
+
if (!error || typeof error !== "object" || Array.isArray(error)) return result;
|
|
2548
|
+
const failure = error;
|
|
2549
|
+
if (failure.ok !== false || typeof failure.code !== "string" || typeof failure.message !== "string") return result;
|
|
2550
|
+
const status = typeof failure.status === "number" && Number.isInteger(failure.status) && failure.status >= 400 && failure.status <= 599 ? failure.status : void 0;
|
|
2551
|
+
if (status !== void 0 && status >= 500) throw new SmrtWebRequestError(`[smrt-web] ${context} failed: server error`, void 0, status);
|
|
2552
|
+
throw new SmrtWebRequestError(`[smrt-web] ${context} failed: ${failure.message}`, result, status, failure.code);
|
|
2553
|
+
}
|
|
2276
2554
|
var SMRT_TO_REST_OPERATOR = {
|
|
2277
2555
|
">": "gt",
|
|
2278
2556
|
">=": "gte",
|
|
@@ -2374,12 +2652,12 @@ function createDefinitionFetchers(definition, basePath = "/api/v1", fetchFn = (.
|
|
|
2374
2652
|
if (customRoute.method !== "GET") init.body = JSON.stringify(body);
|
|
2375
2653
|
else {
|
|
2376
2654
|
const queryParams = new URLSearchParams();
|
|
2377
|
-
if (optionsBag) if (optionsValue === void 0) queryParams.set(CUSTOM_OPTIONS_QUERY_MARKER, "undefined");
|
|
2378
|
-
else if (optionsValue === null) queryParams.set(CUSTOM_OPTIONS_QUERY_MARKER, "null");
|
|
2655
|
+
if (optionsBag) if (optionsValue === void 0 && pathArgs.size === 0) queryParams.set(CUSTOM_OPTIONS_QUERY_MARKER, "undefined");
|
|
2656
|
+
else if (optionsValue === null && pathArgs.size === 0) queryParams.set(CUSTOM_OPTIONS_QUERY_MARKER, "null");
|
|
2379
2657
|
else {
|
|
2380
|
-
const entries = Object.entries(typeof optionsValue === "object" && !Array.isArray(optionsValue) ? optionsValue : {}).filter(([, value]) => value !== void 0 && value !== null);
|
|
2658
|
+
const entries = Object.entries(optionsValue !== null && typeof optionsValue === "object" && !Array.isArray(optionsValue) ? optionsValue : {}).filter(([, value]) => value !== void 0 && value !== null);
|
|
2381
2659
|
for (const [key, value] of entries) queryParams.set(key, typeof value === "object" ? JSON.stringify(value) : String(value));
|
|
2382
|
-
if (entries.length === 0) queryParams.set(CUSTOM_OPTIONS_QUERY_MARKER, "object");
|
|
2660
|
+
if (entries.length === 0 && pathArgs.size === 0) queryParams.set(CUSTOM_OPTIONS_QUERY_MARKER, "object");
|
|
2383
2661
|
}
|
|
2384
2662
|
else {
|
|
2385
2663
|
const queryBody = body !== null && typeof body === "object" && !Array.isArray(body) ? body : {};
|
|
@@ -2400,18 +2678,36 @@ function newLocalId() {
|
|
|
2400
2678
|
if (cryptoRef?.randomUUID) return cryptoRef.randomUUID();
|
|
2401
2679
|
return `local-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
2402
2680
|
}
|
|
2681
|
+
var smrtWebClientHandles = /* @__PURE__ */ new WeakSet();
|
|
2403
2682
|
function createSmrtWebClient() {
|
|
2404
|
-
|
|
2683
|
+
const engine = {
|
|
2405
2684
|
__smrtWebClient: "SmrtWebClient",
|
|
2406
2685
|
queryClient: new QueryClient()
|
|
2407
2686
|
};
|
|
2687
|
+
smrtWebClientHandles.add(engine);
|
|
2688
|
+
return engine;
|
|
2408
2689
|
}
|
|
2409
2690
|
function resolveQueryClient(client) {
|
|
2410
2691
|
if (!client) return new QueryClient();
|
|
2411
2692
|
const engine = client;
|
|
2412
|
-
if (engine.__smrtWebClient !== "SmrtWebClient" || !engine.queryClient) throw new SmrtWebRequestError("[smrt-web] options.client must be a handle from createSmrtWebClient()");
|
|
2693
|
+
if (!smrtWebClientHandles.has(engine) || engine.__smrtWebClient !== "SmrtWebClient" || !engine.queryClient) throw new SmrtWebRequestError("[smrt-web] options.client must be a handle from createSmrtWebClient()");
|
|
2413
2694
|
return engine.queryClient;
|
|
2414
2695
|
}
|
|
2696
|
+
function validateSmrtWebClient(client) {
|
|
2697
|
+
resolveQueryClient(client);
|
|
2698
|
+
}
|
|
2699
|
+
function invalidateCollectionQueries(queryClient, collectionNames) {
|
|
2700
|
+
if (collectionNames.size === 0) return;
|
|
2701
|
+
queryClient.invalidateQueries({ predicate: (query) => {
|
|
2702
|
+
const key = query.queryKey;
|
|
2703
|
+
if (!Array.isArray(key) || key.length === 0) return false;
|
|
2704
|
+
const collectionSegment = key[key.length - 1];
|
|
2705
|
+
return typeof collectionSegment === "string" && collectionNames.has(collectionSegment);
|
|
2706
|
+
} });
|
|
2707
|
+
}
|
|
2708
|
+
function invalidateSmrtWebCollections(client, collectionNames) {
|
|
2709
|
+
invalidateCollectionQueries(resolveQueryClient(client), new Set(collectionNames));
|
|
2710
|
+
}
|
|
2415
2711
|
function toPlainRow(row) {
|
|
2416
2712
|
const plain = {};
|
|
2417
2713
|
for (const [key, value] of Object.entries(row)) if (key.charCodeAt(0) !== 36) plain[key] = value;
|
|
@@ -2460,12 +2756,7 @@ function createSmrtCollection(definition, options) {
|
|
|
2460
2756
|
const invalidationTargets = /* @__PURE__ */ new Set([definition.name]);
|
|
2461
2757
|
for (const relationship of definition.relationships ?? []) invalidationTargets.add(relationship.relatedCollection);
|
|
2462
2758
|
const invalidateRelated = () => {
|
|
2463
|
-
queryClient
|
|
2464
|
-
const key = query.queryKey;
|
|
2465
|
-
if (!Array.isArray(key) || key.length === 0) return false;
|
|
2466
|
-
const collectionSegment = key[key.length - 1];
|
|
2467
|
-
return typeof collectionSegment === "string" && invalidationTargets.has(collectionSegment);
|
|
2468
|
-
} });
|
|
2759
|
+
invalidateCollectionQueries(queryClient, invalidationTargets);
|
|
2469
2760
|
};
|
|
2470
2761
|
const ctx = {
|
|
2471
2762
|
definition,
|
|
@@ -2629,7 +2920,7 @@ function createSmrtCollection(definition, options) {
|
|
|
2629
2920
|
data: {},
|
|
2630
2921
|
baseUpdatedAt: getBaseUpdatedAt(mutation.original)
|
|
2631
2922
|
};
|
|
2632
|
-
const outcome = await persistMutation(envelope, async () => fetchers.delete(key));
|
|
2923
|
+
const outcome = await persistMutation(envelope, async () => throwIfSmrtWebError(await fetchers.delete(key), `delete(${definition.name})`));
|
|
2633
2924
|
mutationResults.set(envelope.key, outcome.result);
|
|
2634
2925
|
anyHandled = anyHandled || outcome.handled;
|
|
2635
2926
|
}
|
|
@@ -2688,6 +2979,7 @@ function createSmrtCollection(definition, options) {
|
|
|
2688
2979
|
async action(action, args, route) {
|
|
2689
2980
|
if (!fetchers.custom) throw new Error(`${definition.name} has no custom action fetcher`);
|
|
2690
2981
|
const result = route === void 0 ? await fetchers.custom(action, args) : await fetchers.custom(action, args, route);
|
|
2982
|
+
throwIfSmrtWebError(result, `${action}(${definition.name})`);
|
|
2691
2983
|
invalidateRelated();
|
|
2692
2984
|
return result;
|
|
2693
2985
|
}
|
|
@@ -2706,6 +2998,6 @@ function createSmrtCollection(definition, options) {
|
|
|
2706
2998
|
return handle;
|
|
2707
2999
|
}
|
|
2708
3000
|
//#endregion
|
|
2709
|
-
export {
|
|
3001
|
+
export { MAX_SMRT_WEB_DATA_QUERY_RESULT_BYTES as A, registerDurableResource as C, MAX_SMRT_WEB_DATA_QUERY_FACET_VALUES as D, MAX_SMRT_WEB_DATA_QUERY_FACETS as E, normalizeSmrtWebDataQueryResult as F, runWrapMutation as I, MAX_SMRT_WEB_DATA_QUERY_STRING_LENGTH as M, MAX_SMRT_WEB_DATA_QUERY_WARNINGS as N, MAX_SMRT_WEB_DATA_QUERY_OFFSET as O, executeSmrtWebDataQuery as P, durableStoreNamespace as S, MAX_SMRT_WEB_DATA_QUERY_CONTAINER_ITEMS as T, createSmrtWebQuery as _, createSmrtWebClient as a, getOutboxHandle as b, newLocalId as c, unwrapListResult as d, validateSmrtWebClient as f, liveInvalidation as g, createSmrtWebEventSubscriber as h, createSmrtCollection as i, MAX_SMRT_WEB_DATA_QUERY_ROWS as j, MAX_SMRT_WEB_DATA_QUERY_PAGE_LIMIT as k, throwIfSmrtWebError as l, createUpdateState as m, buildListQuery as n, getEngineCollection as o, registerWebMcpTools as p, createDefinitionFetchers as r, invalidateSmrtWebCollections as s, SmrtWebRequestError as t, unwrapItemResult as u, DEFAULT_PERSIST_DEBOUNCE_MS as v, wipeDurableStore as w, offlineOutbox as x, persistCollection as y };
|
|
2710
3002
|
|
|
2711
|
-
//# sourceMappingURL=src-
|
|
3003
|
+
//# sourceMappingURL=src-n14q6RHC.js.map
|