stitchkit 0.37.0 → 0.39.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 (61) hide show
  1. package/README.md +1 -1
  2. package/dist/browser/client-url.d.ts +13 -0
  3. package/dist/browser/client-url.d.ts.map +1 -0
  4. package/dist/browser/client.d.ts +41 -5
  5. package/dist/browser/client.d.ts.map +1 -1
  6. package/dist/browser/http.d.ts +10 -7
  7. package/dist/browser/http.d.ts.map +1 -1
  8. package/dist/cli.js +2 -2
  9. package/dist/contract/define.d.ts +102 -4
  10. package/dist/contract/define.d.ts.map +1 -1
  11. package/dist/contract/index.d.ts +1 -1
  12. package/dist/contract/index.d.ts.map +1 -1
  13. package/dist/contract/index.js +1 -1
  14. package/dist/{index-czmqks7r.js → index-6jypn22c.js} +1 -1
  15. package/dist/{index-4pvtq6h2.js → index-7rbzbnnf.js} +104 -30
  16. package/dist/{index-xq45akyd.js → index-92gs1m5b.js} +1 -1
  17. package/dist/{index-e497hxcy.js → index-fyfk537k.js} +50 -5
  18. package/dist/{index-g3jrbd0z.js → index-xax049k6.js} +101 -5
  19. package/dist/{index-s6yhmg1k.js → index-y5scd1cr.js} +3 -1
  20. package/dist/index.d.ts +2 -2
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +243 -111
  23. package/dist/internal/route-pattern.d.ts +7 -0
  24. package/dist/internal/route-pattern.d.ts.map +1 -0
  25. package/dist/node.js +3 -3
  26. package/dist/observability/index.js +1 -1
  27. package/dist/react/entity-cache.d.ts +56 -42
  28. package/dist/react/entity-cache.d.ts.map +1 -1
  29. package/dist/react.d.ts +1 -1
  30. package/dist/react.d.ts.map +1 -1
  31. package/dist/react.js +98 -46
  32. package/dist/server/create.d.ts.map +1 -1
  33. package/dist/server/implement.d.ts.map +1 -1
  34. package/dist/server/index.js +22 -7
  35. package/dist/server/middleware/cors.d.ts.map +1 -1
  36. package/dist/server/openapi.d.ts.map +1 -1
  37. package/dist/server/response-metadata.d.ts +9 -0
  38. package/dist/server/response-metadata.d.ts.map +1 -0
  39. package/dist/server/router.d.ts +2 -0
  40. package/dist/server/router.d.ts.map +1 -1
  41. package/dist/server/socket-io.d.ts +1 -1
  42. package/dist/server/types.d.ts +14 -8
  43. package/dist/server/types.d.ts.map +1 -1
  44. package/dist/tools/agent.d.ts +3 -0
  45. package/dist/tools/agent.d.ts.map +1 -1
  46. package/dist/tools/invoker.d.ts +33 -0
  47. package/dist/tools/invoker.d.ts.map +1 -0
  48. package/dist/tools/mcp.d.ts.map +1 -1
  49. package/dist/tools/mount.d.ts.map +1 -1
  50. package/dist/tools/names.d.ts +1 -1
  51. package/dist/tools/names.d.ts.map +1 -1
  52. package/dist/tools/native-mcp.d.ts +6 -33
  53. package/dist/tools/native-mcp.d.ts.map +1 -1
  54. package/dist/tools/remote.d.ts.map +1 -1
  55. package/dist/tools/runtime-tool.d.ts +58 -0
  56. package/dist/tools/runtime-tool.d.ts.map +1 -0
  57. package/dist/tools.d.ts +3 -1
  58. package/dist/tools.d.ts.map +1 -1
  59. package/dist/tools.js +295 -182
  60. package/llms-full.txt +562 -79
  61. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -14,20 +14,16 @@ import {
14
14
  isStitchErrorCode,
15
15
  notFound,
16
16
  paginatedSchema,
17
+ parseTrailingWildcard,
17
18
  rateLimited,
18
19
  unauthorized
19
- } from "./index-g3jrbd0z.js";
20
+ } from "./index-xax049k6.js";
20
21
  import {
21
22
  isRecord,
22
23
  mapObject,
23
24
  typedEntries
24
25
  } from "./index-809wc1tt.js";
25
26
 
26
- // src/internal/http-input.ts
27
- function inputIsQuery(method) {
28
- return method === "GET" || method === "DELETE";
29
- }
30
-
31
27
  // src/browser/client-multipart.ts
32
28
  function isFileDescriptor(value) {
33
29
  return typeof value === "object" && value !== null && !(value instanceof Blob) && "uri" in value && typeof value.uri === "string" && "name" in value && typeof value.name === "string" && "type" in value && typeof value.type === "string";
@@ -47,6 +43,170 @@ function appendFormFields(formData, values, skipKeys) {
47
43
  }
48
44
  }
49
45
 
46
+ // src/internal/http-input.ts
47
+ function inputIsQuery(method) {
48
+ return method === "GET" || method === "DELETE";
49
+ }
50
+
51
+ // src/browser/client-url.ts
52
+ function isParamArray(value) {
53
+ return Array.isArray(value) && value.every((item) => typeof item === "string" || typeof item === "number");
54
+ }
55
+ function collectQueryParams(args, endpoint) {
56
+ const params = {};
57
+ let hasParams = false;
58
+ for (const [key, value] of Object.entries(args)) {
59
+ if (value === undefined || value === null)
60
+ continue;
61
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean" || isParamArray(value)) {
62
+ params[key] = value;
63
+ hasParams = true;
64
+ continue;
65
+ }
66
+ const what = Array.isArray(value) ? "an array with non-primitive items" : typeof value === "object" ? "a nested object" : `a ${typeof value}`;
67
+ throw new Error(`${endpoint.method} ${endpoint.path}: input field "${key}" is ${what} — it cannot ` + "travel as a query parameter. GET / DELETE input must be flat (string / number / " + "boolean, or an array of string / number); flatten the field or move the " + "operation to a body verb (POST).");
68
+ }
69
+ return hasParams ? params : undefined;
70
+ }
71
+ function hasStringKeys(args, keys) {
72
+ for (const key of keys) {
73
+ if (typeof args[key] !== "string")
74
+ return false;
75
+ }
76
+ return true;
77
+ }
78
+ function resolvePathPrefix(config, args) {
79
+ if (!config?.pathPrefix)
80
+ return "";
81
+ if (typeof config.pathPrefix === "string")
82
+ return config.pathPrefix;
83
+ const keys = config.stripPrefixKeys ?? [];
84
+ if (!hasStringKeys(args, keys)) {
85
+ const missing = keys.find((key) => typeof args[key] !== "string");
86
+ throw new Error(`Missing path prefix key: ${missing}`);
87
+ }
88
+ return config.pathPrefix(args);
89
+ }
90
+ function escapeRegex(value) {
91
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
92
+ }
93
+ function decodePathSegment(value) {
94
+ try {
95
+ return decodeURIComponent(value);
96
+ } catch {
97
+ return value;
98
+ }
99
+ }
100
+ function createClientRouteMatcher(endpoint, contractPrefix, config) {
101
+ if (typeof config?.pathPrefix === "function" && (!config.stripPrefixKeys || config.stripPrefixKeys.length === 0)) {
102
+ throw new Error("Dynamic pathPrefix matchers require stripPrefixKeys");
103
+ }
104
+ const markerByKey = {};
105
+ for (const [index, key] of (config?.stripPrefixKeys ?? []).entries()) {
106
+ markerByKey[key] = `__stitch_scope_${index}__`;
107
+ }
108
+ const pathPrefix = resolvePathPrefix(config, markerByKey);
109
+ const route = [pathPrefix, contractPrefix, endpoint.path === "/" ? "" : endpoint.path].filter(Boolean).join("/");
110
+ const patternSegments = route.split("/").filter(Boolean);
111
+ const wildcard = patternSegments.at(-1)?.startsWith("*") === true;
112
+ const fixedCount = wildcard ? patternSegments.length - 1 : patternSegments.length;
113
+ const markers = Object.values(markerByKey);
114
+ return (pathname) => {
115
+ const actualSegments = pathname.split("/").filter(Boolean).map(decodePathSegment);
116
+ if (wildcard ? actualSegments.length < fixedCount : actualSegments.length !== fixedCount) {
117
+ return false;
118
+ }
119
+ for (let index = 0;index < fixedCount; index += 1) {
120
+ const pattern = patternSegments[index];
121
+ const actual = actualSegments[index];
122
+ if (!pattern || actual === undefined)
123
+ return false;
124
+ if (pattern.startsWith(":"))
125
+ continue;
126
+ let source = escapeRegex(pattern);
127
+ for (const marker of markers) {
128
+ source = source.replaceAll(escapeRegex(marker), "[^/]+");
129
+ }
130
+ if (!new RegExp(`^${source}$`).test(actual))
131
+ return false;
132
+ }
133
+ return true;
134
+ };
135
+ }
136
+ function extractParamNames(path) {
137
+ const matches = path.match(/:(\w+)/g);
138
+ const names = matches ? matches.map((match) => match.slice(1)) : [];
139
+ const wildcard = parseTrailingWildcard(path);
140
+ if (wildcard)
141
+ names.push(wildcard.name);
142
+ return names;
143
+ }
144
+ function fillPathParams(path, args) {
145
+ const wildcard = parseTrailingWildcard(path);
146
+ let filled = path.replace(/:(\w+)/g, (_, key) => {
147
+ const value = args[key];
148
+ if (value === undefined || value === null) {
149
+ throw new Error(`Missing path param: ${key}`);
150
+ }
151
+ return encodeURIComponent(String(value));
152
+ });
153
+ if (!wildcard)
154
+ return filled;
155
+ const wildcardValue = args[wildcard.name];
156
+ if (wildcardValue === undefined || wildcardValue === null) {
157
+ throw new Error(`Missing path param: ${wildcard.name}`);
158
+ }
159
+ const remainder = String(wildcardValue).split("/").map((segment) => encodeURIComponent(segment)).join("/");
160
+ filled = `${filled.slice(0, -(wildcard.name.length + 1))}${remainder}`;
161
+ return filled;
162
+ }
163
+ function stripConsumedArgs(args, path, scopeKeys) {
164
+ const consumed = new Set(scopeKeys);
165
+ for (const name of extractParamNames(path))
166
+ consumed.add(name);
167
+ const remaining = {};
168
+ for (const [key, value] of Object.entries(args)) {
169
+ if (!consumed.has(key) && value !== undefined)
170
+ remaining[key] = value;
171
+ }
172
+ return remaining;
173
+ }
174
+ function appendQuery(relativeUrl, params) {
175
+ if (!params)
176
+ return relativeUrl;
177
+ const search = new URLSearchParams;
178
+ for (const [key, value] of Object.entries(params)) {
179
+ if (Array.isArray(value)) {
180
+ for (const item of value)
181
+ search.append(key, String(item));
182
+ } else {
183
+ search.set(key, String(value));
184
+ }
185
+ }
186
+ return search.size > 0 ? `${relativeUrl}?${search}` : relativeUrl;
187
+ }
188
+ function planClientRequest(endpoint, contractPrefix, args, config) {
189
+ let pathPrefix = resolvePathPrefix(config, args);
190
+ if (pathPrefix && !pathPrefix.endsWith("/"))
191
+ pathPrefix += "/";
192
+ if (pathPrefix.startsWith("/"))
193
+ pathPrefix = pathPrefix.slice(1);
194
+ const endpointPath = endpoint.path === "/" ? "" : endpoint.path;
195
+ let relativeUrl = fillPathParams(`${pathPrefix}${contractPrefix}${endpointPath}`, args);
196
+ if (relativeUrl.endsWith("/"))
197
+ relativeUrl = relativeUrl.slice(0, -1);
198
+ const remainingArgs = stripConsumedArgs(args, endpoint.path, config?.stripPrefixKeys ?? []);
199
+ if (inputIsQuery(endpoint.method)) {
200
+ relativeUrl = appendQuery(relativeUrl, collectQueryParams(remainingArgs, endpoint));
201
+ }
202
+ return { relativeUrl, remainingArgs };
203
+ }
204
+ function joinClientBaseUrl(baseUrl, relativeUrl) {
205
+ const base = baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
206
+ const path = relativeUrl.startsWith("/") ? relativeUrl : `/${relativeUrl}`;
207
+ return `${base}${path}`;
208
+ }
209
+
50
210
  // src/browser/http.ts
51
211
  import ky, { isHTTPError } from "ky";
52
212
 
@@ -121,7 +281,7 @@ function createHttpClient(config) {
121
281
  let ssrCookies = null;
122
282
  let isLoggedOut = false;
123
283
  const listeners = new Set;
124
- const authEndpoints = config.authEndpoints ?? ["/auth/"];
284
+ const suppressUnauthorizedFor = config.suppressUnauthorizedFor ?? [];
125
285
  const parseError = config.parseError ?? parseApiErrorBody;
126
286
  function emit(event) {
127
287
  for (const fn of listeners) {
@@ -160,7 +320,7 @@ function createHttpClient(config) {
160
320
  async ({ request: request2, response }) => {
161
321
  if (response.status === 401) {
162
322
  const url = new URL(request2.url).pathname;
163
- if (!isLoggedOut && !authEndpoints.some((path) => url.startsWith(path))) {
323
+ if (!isLoggedOut && !suppressUnauthorizedFor.some((matches) => matches(url))) {
164
324
  isLoggedOut = true;
165
325
  emit({ type: "unauthorized" });
166
326
  }
@@ -228,7 +388,9 @@ function createHttpClient(config) {
228
388
  }
229
389
  }
230
390
  return {
391
+ baseUrl: config.baseUrl,
231
392
  get: (url, options) => request("get", url, undefined, options),
393
+ head: (url, options) => request("head", url, undefined, options),
232
394
  post: (url, data, options) => request("post", url, data, options),
233
395
  put: (url, data, options) => request("put", url, data, options),
234
396
  patch: (url, data, options) => request("patch", url, data, options),
@@ -267,24 +429,24 @@ function withOutput(endpoint, result) {
267
429
  return result;
268
430
  return result.then((value) => value === undefined ? undefined : schema.parse(value));
269
431
  }
270
- function isParamArray(value) {
271
- return Array.isArray(value) && value.every((v) => typeof v === "string" || typeof v === "number");
272
- }
273
- function collectQueryParams(args, skipKeys, endpoint) {
274
- const params = {};
275
- let hasParams = false;
276
- for (const [key, value] of Object.entries(args)) {
277
- if (skipKeys.has(key) || value === undefined || value === null)
432
+ function contractEndpointMatchers(contract, endpointNames, contractConfig) {
433
+ const selected = endpointNames ? new Set(endpointNames) : null;
434
+ const matchers = [];
435
+ for (const [key, endpoint] of typedEntries(contract.endpoints)) {
436
+ if (selected && !selected.has(key))
278
437
  continue;
279
- if (typeof value === "string" || typeof value === "number" || typeof value === "boolean" || isParamArray(value)) {
280
- params[key] = value;
281
- hasParams = true;
438
+ if (endpoint.expose && !endpoint.expose.includes("HTTP")) {
439
+ if (selected) {
440
+ throw new Error(`Cannot create an HTTP route matcher for non-HTTP endpoint: ${String(key)}`);
441
+ }
282
442
  continue;
283
443
  }
284
- const what = Array.isArray(value) ? "an array with non-primitive items" : typeof value === "object" ? "a nested object" : `a ${typeof value}`;
285
- throw new Error(`${endpoint.method} ${endpoint.path}: input field "${key}" is ${what} — it cannot ` + "travel as a query parameter. GET / DELETE input must be flat (string / number / " + "boolean, or an array of string / number); flatten the field or move the " + "operation to a body verb (POST).");
444
+ matchers.push(createClientRouteMatcher(endpoint, contract.meta.prefix, contractConfig));
286
445
  }
287
- return hasParams ? params : undefined;
446
+ if (selected && matchers.length !== selected.size) {
447
+ throw new Error("Cannot create an HTTP route matcher for an unknown endpoint");
448
+ }
449
+ return matchers;
288
450
  }
289
451
  function createClient(contract, configOrClient, contractConfig) {
290
452
  const client = {};
@@ -296,8 +458,34 @@ function createClient(contract, configOrClient, contractConfig) {
296
458
  }
297
459
  return client;
298
460
  }
299
- function createClients(contracts, http) {
300
- return mapObject(contracts, (_key, contract) => createClient(contract, http));
461
+ function createClients(contracts, configOrClient, contractConfig) {
462
+ return mapObject(contracts, (_key, contract) => createClient(contract, configOrClient, contractConfig));
463
+ }
464
+ function createScopedClients(contracts, configOrClient, scopeConfigs) {
465
+ const registry = {};
466
+ for (const [namespace, value] of Object.entries(contracts)) {
467
+ const list = Array.isArray(value) ? value : [value];
468
+ const client = {};
469
+ for (const contract of list) {
470
+ const scope = contract.meta.scope;
471
+ if (!scope) {
472
+ throw new Error(`Contract in client namespace "${namespace}" has no scope`);
473
+ }
474
+ const scopeConfig = Object.entries(scopeConfigs).find(([configuredScope]) => configuredScope === scope)?.[1];
475
+ if (!scopeConfig) {
476
+ throw new Error(`Missing client config for scope: ${scope}`);
477
+ }
478
+ const scoped = createClient(contract, configOrClient, scopeConfig);
479
+ for (const [methodName, method] of Object.entries(scoped)) {
480
+ if (Object.hasOwn(client, methodName)) {
481
+ throw new Error(`Client namespace "${namespace}" has duplicate method: ${methodName}`);
482
+ }
483
+ client[methodName] = method;
484
+ }
485
+ }
486
+ registry[namespace] = client;
487
+ }
488
+ return registry;
301
489
  }
302
490
  function isHttpAdapter(value) {
303
491
  return typeof value === "object" && "get" in value && typeof value.get === "function";
@@ -307,90 +495,41 @@ function setClientMethod(target, key, method) {
307
495
  }
308
496
  function createHttpMethod(endpoint, prefix, client, config) {
309
497
  const httpMethod = endpoint.method.toLowerCase();
310
- const isGet = httpMethod === "get";
311
- const paramNames = extractParamNames(endpoint.path);
312
- const prefixKeys = new Set([...config?.stripPrefixKeys ?? [], ...paramNames]);
313
498
  return (...args) => {
314
499
  const firstArg = args[0] ?? {};
315
- let pathPrefixStr = "";
316
- if (config?.pathPrefix) {
317
- pathPrefixStr = typeof config.pathPrefix === "function" ? config.pathPrefix(firstArg) : config.pathPrefix;
318
- if (pathPrefixStr && !pathPrefixStr.endsWith("/"))
319
- pathPrefixStr += "/";
320
- }
321
- let url = `${pathPrefixStr}${prefix}${endpoint.path}`;
322
- for (const name of paramNames) {
323
- const value = firstArg[name];
324
- if (value === undefined || value === null) {
325
- throw new Error(`Missing path param: ${name}`);
326
- }
327
- url = url.replace(`:${name}`, encodeURIComponent(String(value)));
328
- }
329
- if (url.endsWith("/"))
330
- url = url.slice(0, -1);
500
+ const plan = planClientRequest(endpoint, prefix, firstArg, config);
331
501
  if (endpoint.multipart) {
332
502
  const file = firstArg[endpoint.multipart];
333
503
  if (!isMultipartFile(file)) {
334
504
  throw new Error(`Missing multipart file field: ${endpoint.multipart}`);
335
505
  }
336
- if (httpMethod === "get" || httpMethod === "delete") {
506
+ if (httpMethod === "get" || httpMethod === "head" || httpMethod === "delete") {
337
507
  throw new Error(`Multipart endpoint ${endpoint.method} ${endpoint.path} must be POST / PUT / PATCH`);
338
508
  }
339
509
  const formData = new FormData;
340
510
  appendMultipartFile(formData, endpoint.multipart, file);
341
- appendFormFields(formData, firstArg, new Set([...prefixKeys, endpoint.multipart]));
342
- return withOutput(endpoint, client[httpMethod](url, formData, withTimeout(undefined, endpoint)));
511
+ appendFormFields(formData, plan.remainingArgs, new Set([endpoint.multipart]));
512
+ return withOutput(endpoint, client[httpMethod](plan.relativeUrl, formData, withTimeout(undefined, endpoint)));
343
513
  }
344
- if (isGet) {
345
- const params = collectQueryParams(firstArg, prefixKeys, endpoint);
346
- return withOutput(endpoint, client.get(url, withTimeout(params ? { params } : undefined, endpoint)));
514
+ if (httpMethod === "get" || httpMethod === "head") {
515
+ return withOutput(endpoint, client[httpMethod](plan.relativeUrl, withTimeout(undefined, endpoint)));
347
516
  }
348
517
  if (httpMethod === "delete") {
349
- const params = collectQueryParams(firstArg, prefixKeys, endpoint);
350
- return withOutput(endpoint, client.delete(url, withTimeout(params ? { params } : undefined, endpoint)));
518
+ return withOutput(endpoint, client.delete(plan.relativeUrl, withTimeout(undefined, endpoint)));
351
519
  }
352
- const payload = {};
353
- for (const [key, value] of Object.entries(firstArg)) {
354
- if (!prefixKeys.has(key) && value !== undefined) {
355
- payload[key] = value;
356
- }
357
- }
358
- return withOutput(endpoint, client[httpMethod](url, Object.keys(payload).length > 0 ? payload : undefined, withTimeout(undefined, endpoint)));
520
+ return withOutput(endpoint, client[httpMethod](plan.relativeUrl, Object.keys(plan.remainingArgs).length > 0 ? plan.remainingArgs : undefined, withTimeout(undefined, endpoint)));
359
521
  };
360
522
  }
361
523
  function createFetchMethod(endpoint, prefix, config, contractConfig) {
362
- const prefixKeys = new Set(contractConfig?.stripPrefixKeys ?? []);
363
524
  return async (args) => {
364
- let pathPrefixStr = "";
365
- if (contractConfig?.pathPrefix) {
366
- pathPrefixStr = typeof contractConfig.pathPrefix === "function" ? contractConfig.pathPrefix(args ?? {}) : contractConfig.pathPrefix;
367
- if (pathPrefixStr && !pathPrefixStr.endsWith("/"))
368
- pathPrefixStr += "/";
369
- }
370
- let url = buildFetchUrl(config.baseUrl, prefix, endpoint.path, args, pathPrefixStr);
525
+ const plan = planClientRequest(endpoint, prefix, args ?? {}, contractConfig);
526
+ const url = joinClientBaseUrl(config.baseUrl, plan.relativeUrl);
371
527
  const headers = {
372
528
  Accept: "application/json",
373
529
  ...typeof config.headers === "function" ? config.headers() : config.headers
374
530
  };
375
531
  const signal = endpoint.timeout !== undefined ? AbortSignal.timeout(endpoint.timeout) : undefined;
376
- const isQuery = inputIsQuery(endpoint.method);
377
- const hasBody = !isQuery && !endpoint.multipart && endpoint.input && args;
378
- if (isQuery && args) {
379
- const remaining = stripParams(args, endpoint.path, prefixKeys);
380
- const params = collectQueryParams(remaining, new Set, endpoint);
381
- if (params) {
382
- const searchParams = new URLSearchParams;
383
- for (const [k, v] of Object.entries(params)) {
384
- if (Array.isArray(v)) {
385
- for (const item of v)
386
- searchParams.append(k, String(item));
387
- } else {
388
- searchParams.set(k, String(v));
389
- }
390
- }
391
- url += `?${searchParams}`;
392
- }
393
- }
532
+ const hasBody = endpoint.method !== "GET" && endpoint.method !== "HEAD" && endpoint.method !== "DELETE" && !endpoint.multipart && endpoint.input && args;
394
533
  if (hasBody)
395
534
  headers["Content-Type"] = "application/json";
396
535
  if (endpoint.multipart && args) {
@@ -400,7 +539,7 @@ function createFetchMethod(endpoint, prefix, config, contractConfig) {
400
539
  }
401
540
  const formData = new FormData;
402
541
  appendMultipartFile(formData, endpoint.multipart, file);
403
- appendFormFields(formData, stripParams(args, endpoint.path, prefixKeys), new Set([endpoint.multipart]));
542
+ appendFormFields(formData, plan.remainingArgs, new Set([endpoint.multipart]));
404
543
  const res2 = await fetch(url, {
405
544
  method: endpoint.method,
406
545
  headers,
@@ -424,7 +563,7 @@ function createFetchMethod(endpoint, prefix, config, contractConfig) {
424
563
  credentials: config.credentials,
425
564
  signal,
426
565
  ...hasBody && {
427
- body: JSON.stringify(stripParams(hasBody, endpoint.path, prefixKeys))
566
+ body: JSON.stringify(plan.remainingArgs)
428
567
  }
429
568
  });
430
569
  if (!res.ok) {
@@ -447,35 +586,24 @@ async function throwForErrorResponse(res, config, fallbackBody) {
447
586
  }
448
587
  throw new ApiError("HTTP_ERROR", res.status, { body });
449
588
  }
450
- function extractParamNames(path) {
451
- const matches = path.match(/:(\w+)/g);
452
- return matches ? matches.map((m) => m.slice(1)) : [];
453
- }
454
- function buildFetchUrl(baseUrl, prefix, path, args, pathPrefix = "") {
455
- let fullPath = `/${pathPrefix}${prefix}${path === "/" ? "" : path}`;
456
- if (args) {
457
- fullPath = fullPath.replace(/:(\w+)/g, (_, key) => {
458
- const val = args[key];
459
- if (val === undefined || val === null) {
460
- throw new Error(`Missing path param: ${key}`);
589
+ function createUrlBuilder(contract, source, contractConfig) {
590
+ const builder = {};
591
+ for (const [key, endpoint] of typedEntries(contract.endpoints)) {
592
+ if (endpoint.expose && !endpoint.expose.includes("HTTP"))
593
+ continue;
594
+ setClientMethod(builder, key, (args) => {
595
+ const plan = planClientRequest(endpoint, contract.meta.prefix, args ?? {}, contractConfig);
596
+ if (endpoint.method !== "GET" && endpoint.method !== "DELETE" && Object.keys(plan.remainingArgs).length > 0) {
597
+ const fields = Object.keys(plan.remainingArgs).join(", ");
598
+ throw new Error(`URL builder for ${endpoint.method} ${endpoint.path} received non-URL fields: ${fields}`);
461
599
  }
462
- return encodeURIComponent(String(val));
600
+ return joinClientBaseUrl(source.baseUrl, plan.relativeUrl);
463
601
  });
464
602
  }
465
- return `${baseUrl}${fullPath}`;
603
+ return builder;
466
604
  }
467
- function stripParams(args, path, extra) {
468
- const skip = new Set(extra);
469
- for (const match of path.matchAll(/:(\w+)/g)) {
470
- if (match[1])
471
- skip.add(match[1]);
472
- }
473
- const result = {};
474
- for (const [k, v] of Object.entries(args)) {
475
- if (!skip.has(k))
476
- result[k] = v;
477
- }
478
- return result;
605
+ function createUrlBuilders(contracts, source, contractConfig) {
606
+ return mapObject(contracts, (_key, contract) => createUrlBuilder(contract, source, contractConfig));
479
607
  }
480
608
  // src/retained.ts
481
609
  function createRetainedTopics() {
@@ -683,13 +811,17 @@ export {
683
811
  defineErrors,
684
812
  defineContract,
685
813
  decodeCursor,
814
+ createUrlBuilders,
815
+ createUrlBuilder,
686
816
  createTraceContext,
687
817
  createSocketIOClient,
818
+ createScopedClients,
688
819
  createRetainedTopics,
689
820
  createHttpClient,
690
821
  createContractFactory,
691
822
  createClients,
692
823
  createClient,
824
+ contractEndpointMatchers,
693
825
  conflict,
694
826
  childSpan,
695
827
  badRequest,
@@ -0,0 +1,7 @@
1
+ export interface TrailingWildcard {
2
+ name: string;
3
+ segmentIndex: number;
4
+ }
5
+ /** Parse and validate the route's single named terminal wildcard, if present. */
6
+ export declare function parseTrailingWildcard(path: string): TrailingWildcard | null;
7
+ //# sourceMappingURL=route-pattern.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"route-pattern.d.ts","sourceRoot":"","sources":["../../src/internal/route-pattern.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,iFAAiF;AACjF,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,gBAAgB,GAAG,IAAI,CA2C3E"}
package/dist/node.js CHANGED
@@ -3,8 +3,8 @@ import {
3
3
  createImplement,
4
4
  createSocketIOServer,
5
5
  implement
6
- } from "./index-4pvtq6h2.js";
7
- import"./index-czmqks7r.js";
6
+ } from "./index-7rbzbnnf.js";
7
+ import"./index-6jypn22c.js";
8
8
  import"./index-x3fcszf8.js";
9
9
  import {
10
10
  AppError,
@@ -15,7 +15,7 @@ import {
15
15
  notFound,
16
16
  rateLimited,
17
17
  unauthorized
18
- } from "./index-e497hxcy.js";
18
+ } from "./index-fyfk537k.js";
19
19
  // src/server/node.ts
20
20
  import { serve } from "srvx";
21
21
  async function serveNode(config) {
@@ -16,7 +16,7 @@ import {
16
16
  setRequestError,
17
17
  setRequestUser,
18
18
  wrapInRequestContext
19
- } from "../index-e497hxcy.js";
19
+ } from "../index-fyfk537k.js";
20
20
 
21
21
  // src/observability/sanitize.ts
22
22
  var DEFAULT_SENSITIVE_KEYS = /(password|passwd|pwd|secret|token|apikey|api[-_ ]?key|auth|authorization|bearer|session|cookie|init[-_ ]?data|credential|private[-_ ]?key)/i;
@@ -1,54 +1,68 @@
1
1
  /**
2
- * Declarative CRUD cache handlers for `createCacheBridge` the created /
3
- * updated / deleted socket events of one entity, patched into its list and
4
- * detail queries. Every project hand-rolls this same updater; this builds it
5
- * from a small config.
6
- *
7
- * It patches stitchkit's own `Paginated<T>` list envelope (plain or a TanStack
8
- * `InfiniteData` of it) and the entity's detail query. It does **not** flatten
9
- * pages or expose a `useAllX` surface — flattening stays in the component (a
10
- * deliberate boundary): this only keeps the cache correct.
11
- *
12
- * ```ts
13
- * const handlers = createEntityCacheHandlers<Widget>({
14
- * getId: (w) => w.id,
15
- * listKey: ['widgets'],
16
- * detailKey: (id) => ['widgets', id],
17
- * });
18
- * createCacheBridge({ socket, queryClient, handlers: {
19
- * widgetCreated: handlers.created,
20
- * widgetUpdated: handlers.updated,
21
- * widgetDeleted: handlers.deleted,
22
- * }});
23
- * ```
2
+ * Declarative CRUD cache handlers for `createCacheBridge`. One config projects
3
+ * a full event entity into the cached list item and patches plain, paginated or
4
+ * infinite TanStack Query data without changing its envelope metadata.
24
5
  */
25
6
  import type { QueryKey } from '@tanstack/react-query';
26
7
  import type { CacheBridgeHandler } from './cache-bridge';
27
- /** The `deleted` event may carry the whole entity or just its id. */
28
- export type DeletedPayload<T> = T | {
8
+ /** The `deleted` event may carry the whole entity or only its id. */
9
+ export type DeletedPayload<TData> = TData | {
29
10
  id: string;
30
11
  };
12
+ /** Typed event passed to dynamic list/detail query-key selectors. */
13
+ export type EntityCacheEvent<TData> = {
14
+ type: 'created';
15
+ entity: TData;
16
+ id: string;
17
+ } | {
18
+ type: 'updated';
19
+ entity: TData;
20
+ id: string;
21
+ } | {
22
+ type: 'deleted';
23
+ payload: DeletedPayload<TData>;
24
+ id: string;
25
+ };
26
+ /** A static cache key/prefix or an event-aware key factory. */
27
+ export type EntityCacheKey<TData> = QueryKey | ((event: EntityCacheEvent<TData>) => QueryKey);
28
+ export type EntityCacheListShape = 'array' | 'paginated' | 'infinite-array' | 'infinite-paginated';
29
+ /** List envelope, scoped key and explicit CRUD policies. */
30
+ export interface EntityCacheListConfig<TData, TListItem> {
31
+ /** Static query-key prefix or an event-aware scoped key factory. */
32
+ key: EntityCacheKey<TData>;
33
+ /** Edge/page used for a create or an inserted missing update. */
34
+ createAt: 'start' | 'end';
35
+ /** Explicit behavior when an update's id is absent from every cached page. */
36
+ updateMissing: 'skip' | 'insert';
37
+ /** Backend-equivalent ordering for each affected logical item array. */
38
+ compare?: (left: TListItem, right: TListItem) => number;
39
+ /** Cached data envelope; no runtime shape inference is performed. */
40
+ shape: EntityCacheListShape;
41
+ }
31
42
  /** Config for `createEntityCacheHandlers`. */
32
- export interface EntityCacheConfig<T> {
33
- /** Read the entity's id the identity used to match, replace and remove. */
34
- getId: (entity: T) => string;
35
- /** Read the id from a `deleted` payload (entity or `{ id }`). Default `getId` / `.id`. */
36
- getDeletedId?: (payload: DeletedPayload<T>) => string;
37
- /** Query key (or prefix) of the list(s) to patch — matched by partial equality. */
38
- listKey: QueryKey;
39
- /** Build the detail query key for an id. Omit to skip detail-cache updates. */
40
- detailKey?: (id: string) => QueryKey;
43
+ export interface EntityCacheConfig<TData, TListItem = TData> {
44
+ /** Canonical id from a full created/updated entity. */
45
+ getId: (entity: TData) => string;
46
+ /** Canonical id from the projected item stored in list caches. */
47
+ getListItemId: (item: TListItem) => string;
48
+ /** Project a full mutation entity into the list cache's item type. */
49
+ toListItem: (entity: TData) => TListItem;
50
+ /** Read a deleted id. Default: a string `.id`, otherwise `getId(payload)`. */
51
+ getDeletedId?: (payload: DeletedPayload<TData>) => string;
52
+ /** List envelope, key/prefix, insertion policy and optional ordering. */
53
+ list: EntityCacheListConfig<TData, TListItem>;
54
+ /** Static detail key or event-aware key factory. Omit to skip detail updates. */
55
+ detailKey?: EntityCacheKey<TData>;
41
56
  }
42
- /** The three handlers to wire onto a `createCacheBridge` `handlers` map. */
43
- export interface EntityCacheHandlers<T> {
44
- created: CacheBridgeHandler<T>;
45
- updated: CacheBridgeHandler<T>;
46
- deleted: CacheBridgeHandler<DeletedPayload<T>>;
57
+ /** The three handlers to wire onto a `createCacheBridge` handlers map. */
58
+ export interface EntityCacheHandlers<TData> {
59
+ created: CacheBridgeHandler<TData>;
60
+ updated: CacheBridgeHandler<TData>;
61
+ deleted: CacheBridgeHandler<DeletedPayload<TData>>;
47
62
  }
48
63
  /**
49
- * Build created / updated / deleted cache handlers for one entity. Each skips a
50
- * stale socket echo of a change the client just made (`ctx.isFresh`), keyed on
51
- * the entity's detail key when available, else the list key.
64
+ * Build created/updated/deleted cache handlers over one declared list shape.
65
+ * Every event resolves its own scoped keys and applies the same fresh-echo gate.
52
66
  */
53
- export declare function createEntityCacheHandlers<T>(config: EntityCacheConfig<T>): EntityCacheHandlers<T>;
67
+ export declare function createEntityCacheHandlers<TData, TListItem = TData>(config: EntityCacheConfig<TData, TListItem>): EntityCacheHandlers<TData>;
54
68
  //# sourceMappingURL=entity-cache.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"entity-cache.d.ts","sourceRoot":"","sources":["../../src/react/entity-cache.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,OAAO,KAAK,EAAgB,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AAGpE,OAAO,KAAK,EAAsB,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AAK7E,qEAAqE;AACrE,MAAM,MAAM,cAAc,CAAC,CAAC,IAAI,CAAC,GAAG;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE,CAAC;AAEnD,8CAA8C;AAC9C,MAAM,WAAW,iBAAiB,CAAC,CAAC;IAClC,6EAA6E;IAC7E,KAAK,EAAE,CAAC,MAAM,EAAE,CAAC,KAAK,MAAM,CAAC;IAC7B,0FAA0F;IAC1F,YAAY,CAAC,EAAE,CAAC,OAAO,EAAE,cAAc,CAAC,CAAC,CAAC,KAAK,MAAM,CAAC;IACtD,mFAAmF;IACnF,OAAO,EAAE,QAAQ,CAAC;IAClB,+EAA+E;IAC/E,SAAS,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,QAAQ,CAAC;CACtC;AAED,4EAA4E;AAC5E,MAAM,WAAW,mBAAmB,CAAC,CAAC;IACpC,OAAO,EAAE,kBAAkB,CAAC,CAAC,CAAC,CAAC;IAC/B,OAAO,EAAE,kBAAkB,CAAC,CAAC,CAAC,CAAC;IAC/B,OAAO,EAAE,kBAAkB,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,CAAC;CAChD;AAyBD;;;;GAIG;AACH,wBAAgB,yBAAyB,CAAC,CAAC,EACzC,MAAM,EAAE,iBAAiB,CAAC,CAAC,CAAC,GAC3B,mBAAmB,CAAC,CAAC,CAAC,CAuCxB"}
1
+ {"version":3,"file":"entity-cache.d.ts","sourceRoot":"","sources":["../../src/react/entity-cache.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,KAAK,EAAgB,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AAGpE,OAAO,KAAK,EAAsB,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AAE7E,qEAAqE;AACrE,MAAM,MAAM,cAAc,CAAC,KAAK,IAAI,KAAK,GAAG;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE,CAAC;AAE3D,qEAAqE;AACrE,MAAM,MAAM,gBAAgB,CAAC,KAAK,IAC9B;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,MAAM,EAAE,KAAK,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,GAC9C;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,MAAM,EAAE,KAAK,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,GAC9C;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,OAAO,EAAE,cAAc,CAAC,KAAK,CAAC,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,CAAC;AAEpE,+DAA+D;AAC/D,MAAM,MAAM,cAAc,CAAC,KAAK,IAAI,QAAQ,GAAG,CAAC,CAAC,KAAK,EAAE,gBAAgB,CAAC,KAAK,CAAC,KAAK,QAAQ,CAAC,CAAC;AAE9F,MAAM,MAAM,oBAAoB,GAC5B,OAAO,GACP,WAAW,GACX,gBAAgB,GAChB,oBAAoB,CAAC;AAEzB,4DAA4D;AAC5D,MAAM,WAAW,qBAAqB,CAAC,KAAK,EAAE,SAAS;IACrD,oEAAoE;IACpE,GAAG,EAAE,cAAc,CAAC,KAAK,CAAC,CAAC;IAC3B,iEAAiE;IACjE,QAAQ,EAAE,OAAO,GAAG,KAAK,CAAC;IAC1B,8EAA8E;IAC9E,aAAa,EAAE,MAAM,GAAG,QAAQ,CAAC;IACjC,wEAAwE;IACxE,OAAO,CAAC,EAAE,CAAC,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,SAAS,KAAK,MAAM,CAAC;IACxD,qEAAqE;IACrE,KAAK,EAAE,oBAAoB,CAAC;CAC7B;AAED,8CAA8C;AAC9C,MAAM,WAAW,iBAAiB,CAAC,KAAK,EAAE,SAAS,GAAG,KAAK;IACzD,uDAAuD;IACvD,KAAK,EAAE,CAAC,MAAM,EAAE,KAAK,KAAK,MAAM,CAAC;IACjC,kEAAkE;IAClE,aAAa,EAAE,CAAC,IAAI,EAAE,SAAS,KAAK,MAAM,CAAC;IAC3C,sEAAsE;IACtE,UAAU,EAAE,CAAC,MAAM,EAAE,KAAK,KAAK,SAAS,CAAC;IACzC,8EAA8E;IAC9E,YAAY,CAAC,EAAE,CAAC,OAAO,EAAE,cAAc,CAAC,KAAK,CAAC,KAAK,MAAM,CAAC;IAC1D,yEAAyE;IACzE,IAAI,EAAE,qBAAqB,CAAC,KAAK,EAAE,SAAS,CAAC,CAAC;IAC9C,iFAAiF;IACjF,SAAS,CAAC,EAAE,cAAc,CAAC,KAAK,CAAC,CAAC;CACnC;AAED,0EAA0E;AAC1E,MAAM,WAAW,mBAAmB,CAAC,KAAK;IACxC,OAAO,EAAE,kBAAkB,CAAC,KAAK,CAAC,CAAC;IACnC,OAAO,EAAE,kBAAkB,CAAC,KAAK,CAAC,CAAC;IACnC,OAAO,EAAE,kBAAkB,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC,CAAC;CACpD;AAiJD;;;GAGG;AACH,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,SAAS,GAAG,KAAK,EAChE,MAAM,EAAE,iBAAiB,CAAC,KAAK,EAAE,SAAS,CAAC,GAC1C,mBAAmB,CAAC,KAAK,CAAC,CAkD5B"}
package/dist/react.d.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  export { type CacheBridge, type CacheBridgeConfig, type CacheBridgeContext, type CacheBridgeHandler, type CacheBridgeHandlers, type CacheBridgeSocket, createCacheBridge, } from './react/cache-bridge';
2
2
  export { type CursorQueryConfig, createCursorQuery } from './react/cursor-query';
3
- export { createEntityCacheHandlers, type DeletedPayload, type EntityCacheConfig, type EntityCacheHandlers, } from './react/entity-cache';
3
+ export { createEntityCacheHandlers, type DeletedPayload, type EntityCacheConfig, type EntityCacheEvent, type EntityCacheHandlers, type EntityCacheKey, type EntityCacheListConfig, type EntityCacheListShape, } from './react/entity-cache';
4
4
  //# sourceMappingURL=react.d.ts.map