stitchkit 0.7.0 → 0.8.1

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/README.md CHANGED
@@ -352,10 +352,13 @@ A complete runnable app lives in [`packages/starter`](./packages/starter) — a
352
352
  notes CRUD with a contract, a typed client, `react-query-kit` hooks and a
353
353
  Socket.IO live-reload.
354
354
 
355
- ## Documentation
355
+ ## Documentation — two roads
356
356
 
357
- This README is the quick start. The full guide and API reference are in
358
- [`docs/`](./docs/README.md):
357
+ This README is the quick start. Where you go next depends on what you're doing:
358
+
359
+ ### 📦 Building an app **with** stitchkit
360
+
361
+ The full guide and API reference, in [`docs/`](./docs/README.md):
359
362
 
360
363
  - **Guide** — [getting started](./docs/guide/getting-started.md) ·
361
364
  [contracts](./docs/guide/contracts.md) ·
@@ -364,16 +367,23 @@ This README is the quick start. The full guide and API reference are in
364
367
  [MCP & agents](./docs/guide/mcp-and-agents.md) ·
365
368
  [realtime](./docs/guide/realtime.md) ·
366
369
  [auth & errors](./docs/guide/auth-and-errors.md) ·
367
- [testing & deployment](./docs/guide/testing-and-deployment.md)
370
+ [testing & deployment](./docs/guide/testing-and-deployment.md) ·
371
+ [multi-tenant](./docs/guide/multi-tenant.md) ·
372
+ [upgrading](./docs/guide/upgrading.md)
368
373
  - **[API reference](./docs/api/reference.md)** — every export, by entrypoint.
369
- - **[Roadmap](./ROADMAP.md)** where stitchkit is going.
370
- - **[Architecture decisions](./docs/decisions/)**the *why* behind the design.
374
+ - **Coding agent?** The package ships **`llms.txt`** (a curated index) and
375
+ **`llms-full.txt`** (the whole guide inlined) — your agent reads them from
376
+ `node_modules/stitchkit/`. For Claude Code, the repo also provides a
377
+ [stitchkit skill](./skills/stitchkit) you can drop into `.claude/skills/`.
371
378
 
372
- ## Contributing
379
+ ### 🔧 Developing stitchkit itself
373
380
 
374
- Issues and PRs are welcome — see [CONTRIBUTING.md](./CONTRIBUTING.md). Released
375
- changes are in [CHANGELOG.md](./CHANGELOG.md); security issues go through
376
- [SECURITY.md](./SECURITY.md).
381
+ - **[AGENTS.md](./AGENTS.md)** — the development guide (setup, rules, hooks,
382
+ local-dev, breaking changes, release flow). One place for human and agent
383
+ contributors; [CONTRIBUTING.md](./CONTRIBUTING.md) points here.
384
+ - **[Architecture decisions](./docs/decisions/)** — the *why* behind the design.
385
+ - **[Roadmap](./ROADMAP.md)** · **[Changelog](./CHANGELOG.md)** ·
386
+ **[Backlog](./docs/backlog/)** · security via **[SECURITY.md](./SECURITY.md)**.
377
387
 
378
388
  ## License
379
389
 
package/dist/cli.js CHANGED
@@ -4,14 +4,10 @@ import {
4
4
  emitResult,
5
5
  parseCliArgs,
6
6
  pollUntilDone
7
- } from "./index-p8byjw3e.js";
7
+ } from "./index-f39j6twc.js";
8
8
  import"./index-0ed3bx43.js";
9
- import"./index-x3fcszf8.js";
10
- import"./index-4843j09b.js";
11
- import"./index-9bk6r5ff.js";
12
- import"./index-kzfs85xp.js";
13
- import"./index-809wc1tt.js";
14
- import"./index-37x76zdn.js";
9
+ import"./index-jgpsd7dy.js";
10
+ import"./index-tm7dqzxc.js";
15
11
  export {
16
12
  pollUntilDone,
17
13
  parseCliArgs,
@@ -1,4 +1,4 @@
1
1
  export { ALL_TRANSPORTS, type ContractDef, type ContractMeta, defineContract, type EndpointDef, type EndpointFn, type EndpointToolAnnotations, type EndpointUiMeta, type HandlerContext, type HttpMethod, type RuntimeContext, type ScopedEndpointFn, type ScopedHttpClient, type Transport, type TransportSource, type TypedClient, type TypedHttpClient, } from './define';
2
2
  export { AppError, appError, badRequest, conflict, type ErrorEnvelope, forbidden, isStitchErrorCode, notFound, rateLimited, STITCH_ERROR_STATUS, type StitchErrorCode, unauthorized, } from './errors';
3
- export { type Paginated, paginatedSchema } from './pagination';
3
+ export { decodeCursor, encodeCursor, type Paginated, paginatedSchema, } from './pagination';
4
4
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/contract/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,cAAc,EACd,KAAK,WAAW,EAChB,KAAK,YAAY,EACjB,cAAc,EACd,KAAK,WAAW,EAChB,KAAK,UAAU,EACf,KAAK,uBAAuB,EAC5B,KAAK,cAAc,EACnB,KAAK,cAAc,EACnB,KAAK,UAAU,EACf,KAAK,cAAc,EACnB,KAAK,gBAAgB,EACrB,KAAK,gBAAgB,EACrB,KAAK,SAAS,EACd,KAAK,eAAe,EACpB,KAAK,WAAW,EAChB,KAAK,eAAe,GACrB,MAAM,UAAU,CAAC;AAElB,OAAO,EACL,QAAQ,EACR,QAAQ,EACR,UAAU,EACV,QAAQ,EACR,KAAK,aAAa,EAClB,SAAS,EACT,iBAAiB,EACjB,QAAQ,EACR,WAAW,EACX,mBAAmB,EACnB,KAAK,eAAe,EACpB,YAAY,GACb,MAAM,UAAU,CAAC;AAElB,OAAO,EAAE,KAAK,SAAS,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/contract/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,cAAc,EACd,KAAK,WAAW,EAChB,KAAK,YAAY,EACjB,cAAc,EACd,KAAK,WAAW,EAChB,KAAK,UAAU,EACf,KAAK,uBAAuB,EAC5B,KAAK,cAAc,EACnB,KAAK,cAAc,EACnB,KAAK,UAAU,EACf,KAAK,cAAc,EACnB,KAAK,gBAAgB,EACrB,KAAK,gBAAgB,EACrB,KAAK,SAAS,EACd,KAAK,eAAe,EACpB,KAAK,WAAW,EAChB,KAAK,eAAe,GACrB,MAAM,UAAU,CAAC;AAElB,OAAO,EACL,QAAQ,EACR,QAAQ,EACR,UAAU,EACV,QAAQ,EACR,KAAK,aAAa,EAClB,SAAS,EACT,iBAAiB,EACjB,QAAQ,EACR,WAAW,EACX,mBAAmB,EACnB,KAAK,eAAe,EACpB,YAAY,GACb,MAAM,UAAU,CAAC;AAElB,OAAO,EACL,YAAY,EACZ,YAAY,EACZ,KAAK,SAAS,EACd,eAAe,GAChB,MAAM,cAAc,CAAC"}
@@ -5,15 +5,16 @@ import {
5
5
  appError,
6
6
  badRequest,
7
7
  conflict,
8
+ decodeCursor,
8
9
  defineContract,
10
+ encodeCursor,
9
11
  forbidden,
10
12
  isStitchErrorCode,
11
13
  notFound,
12
14
  paginatedSchema,
13
15
  rateLimited,
14
16
  unauthorized
15
- } from "../index-9bk6r5ff.js";
16
- import"../index-37x76zdn.js";
17
+ } from "../index-eq29zkrx.js";
17
18
  export {
18
19
  unauthorized,
19
20
  rateLimited,
@@ -21,7 +22,9 @@ export {
21
22
  notFound,
22
23
  isStitchErrorCode,
23
24
  forbidden,
25
+ encodeCursor,
24
26
  defineContract,
27
+ decodeCursor,
25
28
  conflict,
26
29
  badRequest,
27
30
  appError,
@@ -16,4 +16,15 @@ export declare function paginatedSchema<T extends ZodType>(itemSchema: T): z.Zod
16
16
  items: z.ZodArray<T>;
17
17
  nextCursor: z.ZodNullable<z.ZodString>;
18
18
  }, z.core.$strip>;
19
+ /**
20
+ * Encode a JSON-serializable value (e.g. a keyset `{ v, id }`) into an opaque,
21
+ * URL-safe cursor string for `nextCursor`.
22
+ */
23
+ export declare function encodeCursor(value: unknown): string;
24
+ /**
25
+ * Decode a cursor produced by {@link encodeCursor} and validate it against
26
+ * `schema`. Returns `null` for a missing, malformed or schema-invalid cursor —
27
+ * so a garbage cursor in a URL is treated as "no cursor" rather than throwing.
28
+ */
29
+ export declare function decodeCursor<T>(cursor: string | null | undefined, schema: ZodType<T>): T | null;
19
30
  //# sourceMappingURL=pagination.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"pagination.d.ts","sourceRoot":"","sources":["../../src/contract/pagination.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAEtC;;;;;;;GAOG;AACH,MAAM,WAAW,SAAS,CAAC,CAAC;IAC1B,KAAK,EAAE,CAAC,EAAE,CAAC;IACX,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAED,gEAAgE;AAChE,wBAAgB,eAAe,CAAC,CAAC,SAAS,OAAO,EAAE,UAAU,EAAE,CAAC;;;kBAK/D"}
1
+ {"version":3,"file":"pagination.d.ts","sourceRoot":"","sources":["../../src/contract/pagination.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAEtC;;;;;;;GAOG;AACH,MAAM,WAAW,SAAS,CAAC,CAAC;IAC1B,KAAK,EAAE,CAAC,EAAE,CAAC;IACX,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAED,gEAAgE;AAChE,wBAAgB,eAAe,CAAC,CAAC,SAAS,OAAO,EAAE,UAAU,EAAE,CAAC;;;kBAK/D;AA4BD;;;GAGG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAEnD;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,CAAC,EAC5B,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EACjC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,GACjB,CAAC,GAAG,IAAI,CAQV"}
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  getClientInfo,
3
3
  resolveSocketIp
4
- } from "./index-mwmpw6j1.js";
4
+ } from "./index-p9m9c0jw.js";
5
5
 
6
6
  // src/observability/trace.ts
7
7
  var TRACEPARENT_RE = /^00-([0-9a-f]{32})-([0-9a-f]{16})-[0-9a-f]{2}$/i;
@@ -1,14 +1,12 @@
1
1
  import {
2
2
  forbidden,
3
3
  unauthorized
4
- } from "./index-9bk6r5ff.js";
4
+ } from "./index-jgpsd7dy.js";
5
5
  import {
6
+ isRecord,
6
7
  isUnsafeKey,
7
8
  safeJsonParse
8
- } from "./index-kzfs85xp.js";
9
- import {
10
- isRecord
11
- } from "./index-809wc1tt.js";
9
+ } from "./index-tm7dqzxc.js";
12
10
 
13
11
  // src/server/middleware/cookies.ts
14
12
  function parseCookies(header) {
@@ -216,4 +214,9 @@ async function verifyPkce(verifier, challenge) {
216
214
  return derived === challenge;
217
215
  }
218
216
 
219
- export { parseCookies, serializeCookie, defineCookie, verifyJwt, signJwt, extractToken, createAuthHook, createBearerResolver, deriveCodeChallenge, verifyPkce };
217
+ // src/internal/http-input.ts
218
+ function inputIsQuery(method) {
219
+ return method === "GET" || method === "DELETE";
220
+ }
221
+
222
+ export { parseCookies, serializeCookie, defineCookie, verifyJwt, signJwt, extractToken, createAuthHook, createBearerResolver, deriveCodeChallenge, verifyPkce, inputIsQuery };
@@ -97,4 +97,29 @@ function paginatedSchema(itemSchema) {
97
97
  nextCursor: z.string().nullable()
98
98
  });
99
99
  }
100
- export { ALL_TRANSPORTS, defineContract, AppError, notFound, badRequest, unauthorized, forbidden, conflict, rateLimited, STITCH_ERROR_STATUS, isStitchErrorCode, appError, paginatedSchema };
100
+ function toBase64Url(str) {
101
+ const bytes = new TextEncoder().encode(str);
102
+ let binary = "";
103
+ for (const byte of bytes)
104
+ binary += String.fromCharCode(byte);
105
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
106
+ }
107
+ function fromBase64Url(value) {
108
+ const base64 = value.replace(/-/g, "+").replace(/_/g, "/");
109
+ const padded = base64.padEnd(Math.ceil(base64.length / 4) * 4, "=");
110
+ return new TextDecoder().decode(Uint8Array.from(atob(padded), (c) => c.charCodeAt(0)));
111
+ }
112
+ function encodeCursor(value) {
113
+ return toBase64Url(JSON.stringify(value));
114
+ }
115
+ function decodeCursor(cursor, schema) {
116
+ if (!cursor)
117
+ return null;
118
+ try {
119
+ const parsed = schema.safeParse(JSON.parse(fromBase64Url(cursor)));
120
+ return parsed.success ? parsed.data : null;
121
+ } catch {
122
+ return null;
123
+ }
124
+ }
125
+ export { ALL_TRANSPORTS, defineContract, AppError, notFound, badRequest, unauthorized, forbidden, conflict, rateLimited, STITCH_ERROR_STATUS, isStitchErrorCode, appError, paginatedSchema, encodeCursor, decodeCursor };
@@ -2,21 +2,17 @@ import {
2
2
  jsonSchemaFields,
3
3
  toJsonSchema
4
4
  } from "./index-0ed3bx43.js";
5
- import {
6
- isWithinDir
7
- } from "./index-x3fcszf8.js";
8
5
  import {
9
6
  formatZodError,
7
+ isWithinDir,
10
8
  normalizeError,
11
9
  validateHandlerOutput
12
- } from "./index-4843j09b.js";
10
+ } from "./index-jgpsd7dy.js";
13
11
  import {
12
+ isRecord,
14
13
  isUnsafeKey,
15
14
  safeJsonParse
16
- } from "./index-kzfs85xp.js";
17
- import {
18
- isRecord
19
- } from "./index-809wc1tt.js";
15
+ } from "./index-tm7dqzxc.js";
20
16
 
21
17
  // src/tools/coerce.ts
22
18
  import { z } from "zod";
@@ -0,0 +1,105 @@
1
+ // src/contract/errors.ts
2
+ class AppError extends Error {
3
+ code;
4
+ status;
5
+ details;
6
+ hint;
7
+ constructor(code, message, status = 500, details, hint) {
8
+ super(message ?? code);
9
+ this.code = code;
10
+ this.status = status;
11
+ this.details = details;
12
+ this.hint = hint;
13
+ this.name = "AppError";
14
+ }
15
+ static is(err) {
16
+ return err instanceof AppError;
17
+ }
18
+ toJSON() {
19
+ return {
20
+ error: {
21
+ code: this.code,
22
+ message: this.message,
23
+ ...this.details && { details: this.details },
24
+ ...this.hint && { hint: this.hint }
25
+ }
26
+ };
27
+ }
28
+ }
29
+ function notFound(message = "Not found") {
30
+ throw new AppError("NOT_FOUND", message, 404);
31
+ }
32
+ function badRequest(message, details) {
33
+ throw new AppError("BAD_REQUEST", message, 400, details);
34
+ }
35
+ function unauthorized(message = "Unauthorized") {
36
+ throw new AppError("UNAUTHORIZED", message, 401);
37
+ }
38
+ function forbidden(message = "Forbidden") {
39
+ throw new AppError("FORBIDDEN", message, 403);
40
+ }
41
+ function conflict(message = "Conflict", details) {
42
+ throw new AppError("CONFLICT", message, 409, details);
43
+ }
44
+ function rateLimited(message = "Too many requests") {
45
+ throw new AppError("RATE_LIMITED", message, 429);
46
+ }
47
+ var STITCH_ERROR_STATUS = {
48
+ BAD_REQUEST: 400,
49
+ UNAUTHORIZED: 401,
50
+ FORBIDDEN: 403,
51
+ NOT_FOUND: 404,
52
+ METHOD_NOT_ALLOWED: 405,
53
+ CONFLICT: 409,
54
+ RATE_LIMITED: 429,
55
+ VALIDATION_ERROR: 400,
56
+ INTERNAL_SERVER_ERROR: 500
57
+ };
58
+ function isStitchErrorCode(code) {
59
+ return code in STITCH_ERROR_STATUS;
60
+ }
61
+ function appError(code, message, details) {
62
+ throw new AppError(code, message, isStitchErrorCode(code) ? STITCH_ERROR_STATUS[code] : 500, details);
63
+ }
64
+ // src/contract/pagination.ts
65
+ import { z } from "zod";
66
+ // src/internal/errors.ts
67
+ import { z as z2 } from "zod";
68
+ function formatZodError(error) {
69
+ const issues = error.issues.slice(0, 5);
70
+ const lines = issues.map((issue) => {
71
+ const path = issue.path.length > 0 ? issue.path.join(".") : "(root)";
72
+ return `${path}: ${issue.message}`;
73
+ });
74
+ const suffix = error.issues.length > 5 ? `
75
+ ...and ${error.issues.length - 5} more issues` : "";
76
+ return lines.join(`
77
+ `) + suffix;
78
+ }
79
+ function normalizeError(err) {
80
+ if (AppError.is(err))
81
+ return err;
82
+ if (err instanceof z2.ZodError) {
83
+ return new AppError("VALIDATION_ERROR", formatZodError(err), 400);
84
+ }
85
+ console.error("[stitchkit] unhandled error:", err);
86
+ return new AppError("INTERNAL_SERVER_ERROR", "Internal server error", 500);
87
+ }
88
+ function validateHandlerOutput(schema, data) {
89
+ const parsed = schema.safeParse(data);
90
+ if (parsed.success)
91
+ return { ok: true, data: parsed.data };
92
+ return {
93
+ ok: false,
94
+ message: `Handler output does not match the contract: ${formatZodError(parsed.error)}`
95
+ };
96
+ }
97
+
98
+ // src/internal/within-dir.ts
99
+ import { sep } from "node:path";
100
+ function isWithinDir(root, target) {
101
+ const base = root.endsWith(sep) ? root.slice(0, -sep.length) : root;
102
+ return target === root || target === base || target.startsWith(base + sep);
103
+ }
104
+
105
+ export { AppError, notFound, badRequest, unauthorized, forbidden, conflict, rateLimited, STITCH_ERROR_STATUS, isStitchErrorCode, appError, formatZodError, normalizeError, validateHandlerOutput, isWithinDir };
@@ -1,9 +1,7 @@
1
1
  import {
2
+ isRecord,
2
3
  isUnsafeKey
3
- } from "./index-kzfs85xp.js";
4
- import {
5
- isRecord
6
- } from "./index-809wc1tt.js";
4
+ } from "./index-tm7dqzxc.js";
7
5
 
8
6
  // src/server/request.ts
9
7
  function generateTraceId() {
@@ -1,31 +1,23 @@
1
1
  import {
2
- isWithinDir
3
- } from "./index-x3fcszf8.js";
4
- import {
2
+ AppError,
3
+ badRequest,
4
+ isWithinDir,
5
5
  normalizeError,
6
6
  validateHandlerOutput
7
- } from "./index-4843j09b.js";
8
- import {
9
- AppError,
10
- badRequest
11
- } from "./index-9bk6r5ff.js";
7
+ } from "./index-jgpsd7dy.js";
12
8
  import {
13
9
  extractIp,
14
10
  getClientInfo,
15
11
  parseQueryParams,
16
12
  resolveSocketIp,
17
13
  resolveTraceId
18
- } from "./index-mwmpw6j1.js";
14
+ } from "./index-p9m9c0jw.js";
19
15
  import {
16
+ __require,
20
17
  isUnsafeKey,
21
- safeJsonParse
22
- } from "./index-kzfs85xp.js";
23
- import {
18
+ safeJsonParse,
24
19
  typedEntries
25
- } from "./index-809wc1tt.js";
26
- import {
27
- __require
28
- } from "./index-37x76zdn.js";
20
+ } from "./index-tm7dqzxc.js";
29
21
 
30
22
  // src/server/multipart.ts
31
23
  var DEFAULT_MAX_UPLOAD_BYTES = 25 * 1024 * 1024;
@@ -266,9 +258,16 @@ function matchRawRoute(rawRoutes, httpMethod, pathname) {
266
258
  if (route.method !== "ALL" && route.method !== httpMethod)
267
259
  continue;
268
260
  if (route.path.endsWith("/*")) {
269
- const prefix = route.path.slice(0, -2);
270
- if (pathname === prefix || pathname.startsWith(`${prefix}/`)) {
271
- return { route, params: {} };
261
+ const prefixSegs = route.path.slice(0, -2).split("/").filter(Boolean);
262
+ const pathSegs = pathname.split("/").filter(Boolean);
263
+ if (pathSegs.length < prefixSegs.length)
264
+ continue;
265
+ const params = matchSegments(prefixSegs, pathSegs.slice(0, prefixSegs.length));
266
+ if (params) {
267
+ return {
268
+ route,
269
+ params: { ...params, "*": pathSegs.slice(prefixSegs.length).join("/") }
270
+ };
272
271
  }
273
272
  continue;
274
273
  }
@@ -0,0 +1,20 @@
1
+ import { createRequire } from "node:module";
2
+ var __require = /* @__PURE__ */ createRequire(import.meta.url);
3
+
4
+ // src/internal/safe-json.ts
5
+ function isUnsafeKey(key) {
6
+ return key === "__proto__";
7
+ }
8
+ function safeJsonParse(text) {
9
+ return JSON.parse(text, (key, value) => isUnsafeKey(key) ? undefined : value);
10
+ }
11
+
12
+ // src/internal/typed.ts
13
+ function typedEntries(value) {
14
+ return Object.entries(value);
15
+ }
16
+ function isRecord(value) {
17
+ return typeof value === "object" && value !== null && !Array.isArray(value);
18
+ }
19
+
20
+ export { __require, isUnsafeKey, safeJsonParse, typedEntries, isRecord };