stitchkit 0.38.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 (56) hide show
  1. package/README.md +1 -1
  2. package/dist/browser/client-url.d.ts +2 -0
  3. package/dist/browser/client-url.d.ts.map +1 -1
  4. package/dist/browser/client.d.ts +25 -1
  5. package/dist/browser/client.d.ts.map +1 -1
  6. package/dist/browser/http.d.ts +5 -6
  7. package/dist/browser/http.d.ts.map +1 -1
  8. package/dist/cli.js +2 -2
  9. package/dist/contract/define.d.ts +36 -12
  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-p3kwf73n.js → index-7rbzbnnf.js} +28 -15
  16. package/dist/{index-bgdd42pt.js → index-92gs1m5b.js} +1 -1
  17. package/dist/{index-mzx0an0s.js → index-fyfk537k.js} +50 -5
  18. package/dist/{index-x4wbc8sz.js → index-xax049k6.js} +73 -5
  19. package/dist/{index-n5t4gnfz.js → index-y5scd1cr.js} +1 -1
  20. package/dist/index.d.ts +2 -2
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +116 -17
  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/index.js +10 -9
  34. package/dist/server/middleware/cors.d.ts.map +1 -1
  35. package/dist/server/openapi.d.ts.map +1 -1
  36. package/dist/server/router.d.ts +2 -0
  37. package/dist/server/router.d.ts.map +1 -1
  38. package/dist/server/socket-io.d.ts +1 -1
  39. package/dist/server/types.d.ts +4 -6
  40. package/dist/server/types.d.ts.map +1 -1
  41. package/dist/tools/agent.d.ts +3 -0
  42. package/dist/tools/agent.d.ts.map +1 -1
  43. package/dist/tools/invoker.d.ts +33 -0
  44. package/dist/tools/invoker.d.ts.map +1 -0
  45. package/dist/tools/mcp.d.ts.map +1 -1
  46. package/dist/tools/names.d.ts +1 -1
  47. package/dist/tools/names.d.ts.map +1 -1
  48. package/dist/tools/native-mcp.d.ts +6 -33
  49. package/dist/tools/native-mcp.d.ts.map +1 -1
  50. package/dist/tools/runtime-tool.d.ts +58 -0
  51. package/dist/tools/runtime-tool.d.ts.map +1 -0
  52. package/dist/tools.d.ts +3 -1
  53. package/dist/tools.d.ts.map +1 -1
  54. package/dist/tools.js +181 -82
  55. package/llms-full.txt +417 -89
  56. package/package.json +1 -1
@@ -2,11 +2,66 @@ import {
2
2
  mapObject
3
3
  } from "./index-809wc1tt.js";
4
4
 
5
+ // src/contract/define.ts
6
+ import { z } from "zod";
7
+
8
+ // src/internal/route-pattern.ts
9
+ var PARAM_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
10
+ function parseTrailingWildcard(path) {
11
+ const segments = path.split("/").filter(Boolean);
12
+ const paramNames = new Set;
13
+ let wildcard = null;
14
+ for (const [segmentIndex, segment] of segments.entries()) {
15
+ if (segment.startsWith(":")) {
16
+ const name2 = segment.slice(1);
17
+ if (!PARAM_IDENTIFIER.test(name2)) {
18
+ throw new Error(`Invalid route parameter name "${name2}" in path "${path}"`);
19
+ }
20
+ if (paramNames.has(name2)) {
21
+ throw new Error(`Duplicate route parameter name "${name2}" in path "${path}"`);
22
+ }
23
+ paramNames.add(name2);
24
+ continue;
25
+ }
26
+ if (!segment.startsWith("*")) {
27
+ if (segment.includes("*")) {
28
+ throw new Error(`Wildcard must occupy its own segment in path "${path}"`);
29
+ }
30
+ continue;
31
+ }
32
+ const name = segment.slice(1);
33
+ if (!PARAM_IDENTIFIER.test(name)) {
34
+ throw new Error(`Trailing wildcard in path "${path}" must be named, for example "/*filePath"`);
35
+ }
36
+ if (segmentIndex !== segments.length - 1) {
37
+ throw new Error(`Wildcard "*${name}" must be the final segment in path "${path}"`);
38
+ }
39
+ if (wildcard) {
40
+ throw new Error(`Path "${path}" contains more than one wildcard`);
41
+ }
42
+ if (paramNames.has(name)) {
43
+ throw new Error(`Duplicate route parameter name "${name}" in path "${path}"`);
44
+ }
45
+ wildcard = { name, segmentIndex };
46
+ }
47
+ return wildcard;
48
+ }
49
+
5
50
  // src/contract/define.ts
6
51
  var ALL_TRANSPORTS = ["HTTP", "MCP", "AGENT", "CLI"];
7
52
  function defineContract(meta, endpoints) {
8
53
  const toolTransports = new Map;
9
54
  for (const [key, ep] of Object.entries(endpoints)) {
55
+ const wildcard = parseTrailingWildcard(ep.path);
56
+ if (wildcard) {
57
+ if (!ep.params) {
58
+ throw new Error(`Contract "${meta.prefix}": endpoint "${key}" wildcard "${wildcard.name}" requires a params schema field`);
59
+ }
60
+ const paramsJson = z.toJSONSchema(ep.params, { io: "input" });
61
+ if (!paramsJson.properties || !(wildcard.name in paramsJson.properties)) {
62
+ throw new Error(`Contract "${meta.prefix}": endpoint "${key}" params schema is missing wildcard field "${wildcard.name}"`);
63
+ }
64
+ }
10
65
  if (ep.desc.trim() === "") {
11
66
  throw new Error(`Contract "${meta.prefix}": endpoint "${key}" has an empty desc`);
12
67
  }
@@ -15,6 +70,8 @@ function defineContract(meta, endpoints) {
15
70
  }
16
71
  if (ep.rawResponse)
17
72
  assertRawEndpoint(meta.prefix, key, ep);
73
+ if (ep.method === "HEAD")
74
+ assertHeadEndpoint(meta.prefix, key, ep);
18
75
  if (ep.rawBody)
19
76
  assertRawBodyEndpoint(meta.prefix, key, ep);
20
77
  if ("responseMeta" in ep)
@@ -55,6 +112,17 @@ function assertRawEndpoint(prefix, key, ep) {
55
112
  throw new Error(`${where} is HTTP-only — remove ${nonHttp.join(", ")} from expose`);
56
113
  }
57
114
  }
115
+ function assertHeadEndpoint(prefix, key, ep) {
116
+ const where = `Contract "${prefix}": HEAD endpoint "${key}"`;
117
+ if (!ep.rawResponse)
118
+ throw new Error(`${where} must declare rawResponse: true`);
119
+ if (ep.input)
120
+ throw new Error(`${where} cannot declare an input schema`);
121
+ if (ep.multipart)
122
+ throw new Error(`${where} cannot be multipart`);
123
+ if (ep.rawBody)
124
+ throw new Error(`${where} cannot retain a raw body`);
125
+ }
58
126
  function assertRawBodyEndpoint(prefix, key, ep) {
59
127
  const where = `Contract "${prefix}": rawBody endpoint "${key}"`;
60
128
  if (!ep.input)
@@ -194,7 +262,7 @@ function createContractFactory() {
194
262
  };
195
263
  }
196
264
  // src/contract/pagination.ts
197
- import { z } from "zod";
265
+ import { z as z2 } from "zod";
198
266
 
199
267
  // src/internal/base64url.ts
200
268
  function bytesToBase64Url(bytes) {
@@ -214,9 +282,9 @@ function base64UrlToBytes(segment) {
214
282
 
215
283
  // src/contract/pagination.ts
216
284
  function paginatedSchema(itemSchema) {
217
- return z.object({
218
- items: z.array(itemSchema),
219
- nextCursor: z.string().nullable()
285
+ return z2.object({
286
+ items: z2.array(itemSchema),
287
+ nextCursor: z2.string().nullable()
220
288
  });
221
289
  }
222
290
  function toBase64Url(str) {
@@ -238,4 +306,4 @@ function decodeCursor(cursor, schema) {
238
306
  return null;
239
307
  }
240
308
  }
241
- export { ALL_TRANSPORTS, defineContract, AppError, notFound, badRequest, unauthorized, forbidden, conflict, rateLimited, STITCH_ERROR_STATUS, isStitchErrorCode, appError, defineErrors, createContractFactory, paginatedSchema, encodeCursor, decodeCursor };
309
+ export { parseTrailingWildcard, ALL_TRANSPORTS, defineContract, AppError, notFound, badRequest, unauthorized, forbidden, conflict, rateLimited, STITCH_ERROR_STATUS, isStitchErrorCode, appError, defineErrors, createContractFactory, paginatedSchema, encodeCursor, decodeCursor };
@@ -14,7 +14,7 @@ import {
14
14
  runWithRequestContext,
15
15
  safeJsonParse,
16
16
  validateHandlerOutput
17
- } from "./index-mzx0an0s.js";
17
+ } from "./index-fyfk537k.js";
18
18
 
19
19
  // src/tools/coerce.ts
20
20
  import { z } from "zod";
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- export { type ClientConfig, type ContractClientConfig, createClient, createClients, createUrlBuilder, createUrlBuilders, type PathPrefixArgs, type UrlBuilderConfig, } from './browser/client';
2
- export { ApiError, type ApiEvent, type ApiEventListener, type ConfiguredHttpClient, createHttpClient, type HeaderProvider, type HttpClient, type HttpClientConfig, type RequestOptions, } from './browser/http';
1
+ export { type ClientConfig, type ClientContract, type ClientRegistryValue, type ContractClientConfig, contractEndpointMatchers, createClient, createClients, createScopedClients, createUrlBuilder, createUrlBuilders, type PathPrefixArgs, type RegistryScope, type ScopeClientConfigs, type ScopedClientRegistry, type UrlBuilderConfig, } from './browser/client';
2
+ export { ApiError, type ApiEvent, type ApiEventListener, type ConfiguredHttpClient, createHttpClient, type HeaderProvider, type HttpClient, type HttpClientConfig, type RequestOptions, type UnauthorizedMatcher, } from './browser/http';
3
3
  export type { SocketEventMap, SocketIOClient, SocketIOClientConfig, } from './browser/socket-io';
4
4
  export { createSocketIOClient } from './browser/socket-io';
5
5
  export { type ParseSSEOptions, parseSSE } from './browser/stream';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,YAAY,EACjB,KAAK,oBAAoB,EACzB,YAAY,EACZ,aAAa,EACb,gBAAgB,EAChB,iBAAiB,EACjB,KAAK,cAAc,EACnB,KAAK,gBAAgB,GACtB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,QAAQ,EACR,KAAK,QAAQ,EACb,KAAK,gBAAgB,EACrB,KAAK,oBAAoB,EACzB,gBAAgB,EAChB,KAAK,cAAc,EACnB,KAAK,UAAU,EACf,KAAK,gBAAgB,EACrB,KAAK,cAAc,GACpB,MAAM,gBAAgB,CAAC;AACxB,YAAY,EACV,cAAc,EACd,cAAc,EACd,oBAAoB,GACrB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAC3D,OAAO,EAAE,KAAK,eAAe,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAClE,cAAc,YAAY,CAAC;AAG3B,OAAO,EACL,SAAS,EACT,kBAAkB,EAClB,iBAAiB,EACjB,gBAAgB,EAChB,KAAK,YAAY,GAClB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,oBAAoB,EAAE,KAAK,cAAc,EAAE,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,YAAY,EACjB,KAAK,cAAc,EACnB,KAAK,mBAAmB,EACxB,KAAK,oBAAoB,EACzB,wBAAwB,EACxB,YAAY,EACZ,aAAa,EACb,mBAAmB,EACnB,gBAAgB,EAChB,iBAAiB,EACjB,KAAK,cAAc,EACnB,KAAK,aAAa,EAClB,KAAK,kBAAkB,EACvB,KAAK,oBAAoB,EACzB,KAAK,gBAAgB,GACtB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,QAAQ,EACR,KAAK,QAAQ,EACb,KAAK,gBAAgB,EACrB,KAAK,oBAAoB,EACzB,gBAAgB,EAChB,KAAK,cAAc,EACnB,KAAK,UAAU,EACf,KAAK,gBAAgB,EACrB,KAAK,cAAc,EACnB,KAAK,mBAAmB,GACzB,MAAM,gBAAgB,CAAC;AACxB,YAAY,EACV,cAAc,EACd,cAAc,EACd,oBAAoB,GACrB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAC3D,OAAO,EAAE,KAAK,eAAe,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAClE,cAAc,YAAY,CAAC;AAG3B,OAAO,EACL,SAAS,EACT,kBAAkB,EAClB,iBAAiB,EACjB,gBAAgB,EAChB,KAAK,YAAY,GAClB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,oBAAoB,EAAE,KAAK,cAAc,EAAE,MAAM,YAAY,CAAC"}
package/dist/index.js CHANGED
@@ -14,9 +14,10 @@ import {
14
14
  isStitchErrorCode,
15
15
  notFound,
16
16
  paginatedSchema,
17
+ parseTrailingWildcard,
17
18
  rateLimited,
18
19
  unauthorized
19
- } from "./index-x4wbc8sz.js";
20
+ } from "./index-xax049k6.js";
20
21
  import {
21
22
  isRecord,
22
23
  mapObject,
@@ -86,14 +87,62 @@ function resolvePathPrefix(config, args) {
86
87
  }
87
88
  return config.pathPrefix(args);
88
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
+ }
89
136
  function extractParamNames(path) {
90
137
  const matches = path.match(/:(\w+)/g);
91
138
  const names = matches ? matches.map((match) => match.slice(1)) : [];
92
- if (path.endsWith("/*"))
93
- names.push("*");
139
+ const wildcard = parseTrailingWildcard(path);
140
+ if (wildcard)
141
+ names.push(wildcard.name);
94
142
  return names;
95
143
  }
96
144
  function fillPathParams(path, args) {
145
+ const wildcard = parseTrailingWildcard(path);
97
146
  let filled = path.replace(/:(\w+)/g, (_, key) => {
98
147
  const value = args[key];
99
148
  if (value === undefined || value === null) {
@@ -101,14 +150,14 @@ function fillPathParams(path, args) {
101
150
  }
102
151
  return encodeURIComponent(String(value));
103
152
  });
104
- if (!filled.endsWith("/*"))
153
+ if (!wildcard)
105
154
  return filled;
106
- const wildcard = args["*"];
107
- if (wildcard === undefined || wildcard === null) {
108
- throw new Error("Missing path param: *");
155
+ const wildcardValue = args[wildcard.name];
156
+ if (wildcardValue === undefined || wildcardValue === null) {
157
+ throw new Error(`Missing path param: ${wildcard.name}`);
109
158
  }
110
- const remainder = String(wildcard).split("/").map((segment) => encodeURIComponent(segment)).join("/");
111
- filled = `${filled.slice(0, -1)}${remainder}`;
159
+ const remainder = String(wildcardValue).split("/").map((segment) => encodeURIComponent(segment)).join("/");
160
+ filled = `${filled.slice(0, -(wildcard.name.length + 1))}${remainder}`;
112
161
  return filled;
113
162
  }
114
163
  function stripConsumedArgs(args, path, scopeKeys) {
@@ -232,7 +281,7 @@ function createHttpClient(config) {
232
281
  let ssrCookies = null;
233
282
  let isLoggedOut = false;
234
283
  const listeners = new Set;
235
- const authEndpoints = config.authEndpoints ?? ["/auth/"];
284
+ const suppressUnauthorizedFor = config.suppressUnauthorizedFor ?? [];
236
285
  const parseError = config.parseError ?? parseApiErrorBody;
237
286
  function emit(event) {
238
287
  for (const fn of listeners) {
@@ -271,7 +320,7 @@ function createHttpClient(config) {
271
320
  async ({ request: request2, response }) => {
272
321
  if (response.status === 401) {
273
322
  const url = new URL(request2.url).pathname;
274
- if (!isLoggedOut && !authEndpoints.some((path) => url.startsWith(path))) {
323
+ if (!isLoggedOut && !suppressUnauthorizedFor.some((matches) => matches(url))) {
275
324
  isLoggedOut = true;
276
325
  emit({ type: "unauthorized" });
277
326
  }
@@ -341,6 +390,7 @@ function createHttpClient(config) {
341
390
  return {
342
391
  baseUrl: config.baseUrl,
343
392
  get: (url, options) => request("get", url, undefined, options),
393
+ head: (url, options) => request("head", url, undefined, options),
344
394
  post: (url, data, options) => request("post", url, data, options),
345
395
  put: (url, data, options) => request("put", url, data, options),
346
396
  patch: (url, data, options) => request("patch", url, data, options),
@@ -379,6 +429,25 @@ function withOutput(endpoint, result) {
379
429
  return result;
380
430
  return result.then((value) => value === undefined ? undefined : schema.parse(value));
381
431
  }
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))
437
+ continue;
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
+ }
442
+ continue;
443
+ }
444
+ matchers.push(createClientRouteMatcher(endpoint, contract.meta.prefix, contractConfig));
445
+ }
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;
450
+ }
382
451
  function createClient(contract, configOrClient, contractConfig) {
383
452
  const client = {};
384
453
  const makeMethod = isHttpAdapter(configOrClient) ? (endpoint) => createHttpMethod(endpoint, contract.meta.prefix, configOrClient, contractConfig) : (endpoint) => createFetchMethod(endpoint, contract.meta.prefix, configOrClient, contractConfig);
@@ -392,6 +461,32 @@ function createClient(contract, configOrClient, contractConfig) {
392
461
  function createClients(contracts, configOrClient, contractConfig) {
393
462
  return mapObject(contracts, (_key, contract) => createClient(contract, configOrClient, contractConfig));
394
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;
489
+ }
395
490
  function isHttpAdapter(value) {
396
491
  return typeof value === "object" && "get" in value && typeof value.get === "function";
397
492
  }
@@ -408,7 +503,7 @@ function createHttpMethod(endpoint, prefix, client, config) {
408
503
  if (!isMultipartFile(file)) {
409
504
  throw new Error(`Missing multipart file field: ${endpoint.multipart}`);
410
505
  }
411
- if (httpMethod === "get" || httpMethod === "delete") {
506
+ if (httpMethod === "get" || httpMethod === "head" || httpMethod === "delete") {
412
507
  throw new Error(`Multipart endpoint ${endpoint.method} ${endpoint.path} must be POST / PUT / PATCH`);
413
508
  }
414
509
  const formData = new FormData;
@@ -416,8 +511,8 @@ function createHttpMethod(endpoint, prefix, client, config) {
416
511
  appendFormFields(formData, plan.remainingArgs, new Set([endpoint.multipart]));
417
512
  return withOutput(endpoint, client[httpMethod](plan.relativeUrl, formData, withTimeout(undefined, endpoint)));
418
513
  }
419
- if (httpMethod === "get") {
420
- return withOutput(endpoint, client.get(plan.relativeUrl, withTimeout(undefined, endpoint)));
514
+ if (httpMethod === "get" || httpMethod === "head") {
515
+ return withOutput(endpoint, client[httpMethod](plan.relativeUrl, withTimeout(undefined, endpoint)));
421
516
  }
422
517
  if (httpMethod === "delete") {
423
518
  return withOutput(endpoint, client.delete(plan.relativeUrl, withTimeout(undefined, endpoint)));
@@ -434,7 +529,7 @@ function createFetchMethod(endpoint, prefix, config, contractConfig) {
434
529
  ...typeof config.headers === "function" ? config.headers() : config.headers
435
530
  };
436
531
  const signal = endpoint.timeout !== undefined ? AbortSignal.timeout(endpoint.timeout) : undefined;
437
- const hasBody = endpoint.method !== "GET" && endpoint.method !== "DELETE" && !endpoint.multipart && endpoint.input && args;
532
+ const hasBody = endpoint.method !== "GET" && endpoint.method !== "HEAD" && endpoint.method !== "DELETE" && !endpoint.multipart && endpoint.input && args;
438
533
  if (hasBody)
439
534
  headers["Content-Type"] = "application/json";
440
535
  if (endpoint.multipart && args) {
@@ -494,12 +589,14 @@ async function throwForErrorResponse(res, config, fallbackBody) {
494
589
  function createUrlBuilder(contract, source, contractConfig) {
495
590
  const builder = {};
496
591
  for (const [key, endpoint] of typedEntries(contract.endpoints)) {
497
- if (endpoint.method !== "GET" || endpoint.multipart)
498
- continue;
499
592
  if (endpoint.expose && !endpoint.expose.includes("HTTP"))
500
593
  continue;
501
594
  setClientMethod(builder, key, (args) => {
502
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}`);
599
+ }
503
600
  return joinClientBaseUrl(source.baseUrl, plan.relativeUrl);
504
601
  });
505
602
  }
@@ -718,11 +815,13 @@ export {
718
815
  createUrlBuilder,
719
816
  createTraceContext,
720
817
  createSocketIOClient,
818
+ createScopedClients,
721
819
  createRetainedTopics,
722
820
  createHttpClient,
723
821
  createContractFactory,
724
822
  createClients,
725
823
  createClient,
824
+ contractEndpointMatchers,
726
825
  conflict,
727
826
  childSpan,
728
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-p3kwf73n.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-mzx0an0s.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-mzx0an0s.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
@@ -1 +1 @@
1
- {"version":3,"file":"react.d.ts","sourceRoot":"","sources":["../src/react.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,WAAW,EAChB,KAAK,iBAAiB,EACtB,KAAK,kBAAkB,EACvB,KAAK,kBAAkB,EACvB,KAAK,mBAAmB,EACxB,KAAK,iBAAiB,EACtB,iBAAiB,GAClB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,KAAK,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AACjF,OAAO,EACL,yBAAyB,EACzB,KAAK,cAAc,EACnB,KAAK,iBAAiB,EACtB,KAAK,mBAAmB,GACzB,MAAM,sBAAsB,CAAC"}
1
+ {"version":3,"file":"react.d.ts","sourceRoot":"","sources":["../src/react.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,WAAW,EAChB,KAAK,iBAAiB,EACtB,KAAK,kBAAkB,EACvB,KAAK,kBAAkB,EACvB,KAAK,mBAAmB,EACxB,KAAK,iBAAiB,EACtB,iBAAiB,GAClB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,KAAK,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AACjF,OAAO,EACL,yBAAyB,EACzB,KAAK,cAAc,EACnB,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,EACrB,KAAK,mBAAmB,EACxB,KAAK,cAAc,EACnB,KAAK,qBAAqB,EAC1B,KAAK,oBAAoB,GAC1B,MAAM,sBAAsB,CAAC"}