@tollwyn/baukasten 1.3.1 → 1.3.3

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.
@@ -14,7 +14,10 @@ export declare class ApiError extends Error {
14
14
  }
15
15
  /** Runs a handler of the API: an ApiError becomes its answer, anything else a logged 500. */
16
16
  export declare function respond(log: Log, messages: Messages, handler: () => Promise<Response>): Promise<Response>;
17
- /** The JSON body of a request, parsed with `schema`: 400 when it is no JSON, 422 when it does not fit. */
17
+ /**
18
+ * The JSON body of a request, parsed with `schema`: 400 when it is no JSON, 413 when it is over the
19
+ * limit of the server, 422 when it does not fit.
20
+ */
18
21
  export declare function readBody<S extends z.ZodType>(request: Request, schema: S, messages: Messages): Promise<z.output<S>>;
19
22
  /** The key in `Authorization: Bearer <prefix>_<secret>`, or null. */
20
23
  export declare function bearerKey(headers: Headers, prefix: string): string | null;
@@ -30,14 +30,28 @@ export async function respond(log, messages, handler) {
30
30
  return errorResponse(new ApiError(500, 'internal', messages.api.internal));
31
31
  }
32
32
  }
33
- /** The JSON body of a request, parsed with `schema`: 400 when it is no JSON, 422 when it does not fit. */
33
+ /**
34
+ * SvelteKit errors the body of a request over BODY_SIZE_LIMIT with an error of its own that carries
35
+ * the status 413, and does so only while the body is read.
36
+ */
37
+ const overLimit = (error) => error instanceof Error && 'status' in error && error.status === 413;
38
+ /**
39
+ * The JSON body of a request, parsed with `schema`: 400 when it is no JSON, 413 when it is over the
40
+ * limit of the server, 422 when it does not fit.
41
+ */
34
42
  export async function readBody(request, schema, messages) {
35
43
  let body;
36
44
  try {
37
45
  body = await request.json();
38
46
  }
39
- catch {
40
- throw new ApiError(400, 'invalid_json', messages.api.invalidJson);
47
+ catch (caught) {
48
+ if (caught instanceof SyntaxError) {
49
+ throw new ApiError(400, 'invalid_json', messages.api.invalidJson);
50
+ }
51
+ if (overLimit(caught)) {
52
+ throw new ApiError(413, 'payload_too_large', messages.api.payloadTooLarge);
53
+ }
54
+ throw caught;
41
55
  }
42
56
  const result = schema.safeParse(body);
43
57
  if (!result.success) {
@@ -80,10 +94,15 @@ export async function asApiError(response, messages) {
80
94
  if (response.status < 400)
81
95
  return response;
82
96
  if (response.headers.get('content-type')?.startsWith('application/json') === true) {
97
+ // A body that claims to be JSON and is none gets the shape; a failure to read it goes on.
83
98
  const body = await response
84
99
  .clone()
85
100
  .json()
86
- .catch(() => null);
101
+ .catch((caught) => {
102
+ if (caught instanceof SyntaxError)
103
+ return null;
104
+ throw caught;
105
+ });
87
106
  if (isShaped(body))
88
107
  return response;
89
108
  }
@@ -71,7 +71,10 @@ export interface Kit<E extends ProductEnv = ProductEnv> {
71
71
  api: {
72
72
  /** Runs a handler: an ApiError becomes its answer, anything else a logged 500. */
73
73
  respond(handler: () => Promise<Response>): Promise<Response>;
74
- /** The JSON body of a request, parsed with `schema`: else 400 or 422 with each problem. */
74
+ /**
75
+ * The JSON body of a request, parsed with `schema`: else 400, 413 over the limit of the server,
76
+ * or 422 with each problem.
77
+ */
75
78
  body<S extends z.ZodType>(request: Request, schema: S): Promise<z.output<S>>;
76
79
  /** The account of the bearer key: else 401, and 429 past the limit of the key. */
77
80
  account(event: RequestEvent): Promise<Account>;
@@ -99,7 +99,7 @@ export interface Messages {
99
99
  url: string;
100
100
  price: string;
101
101
  quantity: string;
102
- errors: Record<'invalidJson' | 'invalidRequest' | 'emailTaken' | 'unauthorized' | 'rateLimited' | 'forbidden' | 'subscribed' | 'noCustomer' | 'notConfigured', string>;
102
+ errors: Record<'invalidJson' | 'payloadTooLarge' | 'invalidRequest' | 'emailTaken' | 'unauthorized' | 'rateLimited' | 'forbidden' | 'subscribed' | 'noCustomer' | 'notConfigured', string>;
103
103
  };
104
104
  }
105
105
  export declare const MESSAGES: Record<Language, Messages>;
@@ -103,6 +103,7 @@ const en = {
103
103
  quantity: 'For a subscription that counts a quantity, like seats; 1 without.',
104
104
  errors: {
105
105
  invalidJson: 'The body is not JSON (invalid_json).',
106
+ payloadTooLarge: 'The body is over the limit of the server (payload_too_large).',
106
107
  invalidRequest: 'The body is not valid (invalid_request); details name each problem.',
107
108
  emailTaken: 'An account with this email address exists (email_taken).',
108
109
  unauthorized: 'No valid API key (unauthorized).',
@@ -220,6 +221,7 @@ const de = {
220
221
  quantity: 'Für ein Abo mit einer Menge, etwa Plätzen; ohne 1.',
221
222
  errors: {
222
223
  invalidJson: 'Der Inhalt ist kein JSON (invalid_json).',
224
+ payloadTooLarge: 'Der Inhalt ist grösser, als der Server annimmt (payload_too_large).',
223
225
  invalidRequest: 'Der Inhalt ist nicht gültig (invalid_request); details nennt jedes Problem.',
224
226
  emailTaken: 'Zu dieser E-Mail-Adresse besteht ein Konto (email_taken).',
225
227
  unauthorized: 'Kein gültiger API-Schlüssel (unauthorized).',
@@ -49,6 +49,7 @@ export function openApiPart(origin, messages) {
49
49
  '400': error(o.errors.invalidJson),
50
50
  '403': error(o.errors.forbidden),
51
51
  '409': error(o.errors.emailTaken),
52
+ '413': error(o.errors.payloadTooLarge),
52
53
  '422': error(o.errors.invalidRequest),
53
54
  '429': error(o.errors.rateLimited)
54
55
  }
@@ -88,6 +89,7 @@ export function openApiPart(origin, messages) {
88
89
  '400': error(o.errors.invalidJson),
89
90
  '401': error(o.errors.unauthorized),
90
91
  '409': error(o.errors.subscribed),
92
+ '413': error(o.errors.payloadTooLarge),
91
93
  '422': error(o.errors.invalidRequest),
92
94
  '429': error(o.errors.rateLimited),
93
95
  '503': error(o.errors.notConfigured)
@@ -1,5 +1,6 @@
1
1
  import type { SmtpDouble } from './smtp.js';
2
2
  import type { StripeDouble } from './stripe.js';
3
+ /** A line of stdout as JSON object; a line that is none is { notJson } or, as JSON, { notAnObject }. */
3
4
  export type LogLine = Record<string, unknown>;
4
5
  export interface AppProcess {
5
6
  logs: LogLine[];
@@ -38,15 +38,8 @@ export function spawnNode(args, env, options = {}) {
38
38
  buffer += chunk.toString();
39
39
  let end;
40
40
  while ((end = buffer.indexOf('\n')) >= 0) {
41
- const text = buffer.slice(0, end);
41
+ const line = asLine(buffer.slice(0, end));
42
42
  buffer = buffer.slice(end + 1);
43
- let line;
44
- try {
45
- line = asLine(JSON.parse(text));
46
- }
47
- catch {
48
- line = { notJson: text };
49
- }
50
43
  logs.push(line);
51
44
  events.emit('line', line);
52
45
  }
@@ -101,10 +94,18 @@ export function spawnNode(args, env, options = {}) {
101
94
  }
102
95
  };
103
96
  }
104
- /** A log line is a JSON object; anything else is kept as text. */
105
- function asLine(value) {
97
+ /** A log line is a JSON object; anything else is kept as text, under notJson or notAnObject. */
98
+ function asLine(text) {
99
+ let value;
100
+ try {
101
+ value = JSON.parse(text);
102
+ }
103
+ catch {
104
+ // JSON.parse of a string fails only with a SyntaxError.
105
+ return { notJson: text };
106
+ }
106
107
  if (typeof value !== 'object' || value === null || Array.isArray(value)) {
107
- throw new Error('not an object');
108
+ return { notAnObject: text };
108
109
  }
109
110
  return Object.fromEntries(Object.entries(value));
110
111
  }
@@ -294,7 +294,7 @@ export function connectionTest(product, playwright) {
294
294
  });
295
295
  test('the log of the product is JSON lines without the address, and stderr stays empty, to its end', async () => {
296
296
  await app.stop();
297
- expect(app.logs.filter((line) => 'notJson' in line)).toEqual([]);
297
+ expect(app.logs.filter((line) => 'notJson' in line || 'notAnObject' in line)).toEqual([]);
298
298
  expect(JSON.stringify(app.logs)).not.toContain(EMAIL);
299
299
  expect(app.stderr()).toBe('');
300
300
  });
@@ -296,7 +296,7 @@ export function shopifyConnectionTest(app, playwright) {
296
296
  });
297
297
  test('it writes JSON lines only, and no token into its log', () => {
298
298
  expect(running.stderr()).toBe('');
299
- expect(running.logs.filter((line) => 'notJson' in line)).toEqual([]);
299
+ expect(running.logs.filter((line) => 'notJson' in line || 'notAnObject' in line)).toEqual([]);
300
300
  // Every ID token starts with the same header, so any of them in the log would show it.
301
301
  const [header = ''] = shopify.idToken(SHOP).split('.');
302
302
  expect(header).not.toBe('');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tollwyn/baukasten",
3
- "version": "1.3.1",
3
+ "version": "1.3.3",
4
4
  "description": "What every product built on this kit shares: configuration, log and server, database with two-layer migrations, health, accounts with sessions and API keys, limits, mail, payment through Stripe, the numbers for sales, the frame and pages, the stubs that hang them into a product and the check of a product, and the tools to test one, its connection test among them; and for a Shopify app its installation with tokens, the frame of the admin, its webhooks, its plan through the Partner API and its privacy text.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -60,35 +60,35 @@
60
60
  "svelte": "^5.57.0"
61
61
  },
62
62
  "dependencies": {
63
- "nodemailer": "10.0.16",
63
+ "nodemailer": "10.1.1",
64
64
  "pg": "8.23.1",
65
- "smol-toml": "1.9.0",
65
+ "smol-toml": "1.9.1",
66
66
  "stripe": "22.6.2",
67
- "zod": "4.6.5"
67
+ "zod": "4.6.7"
68
68
  },
69
69
  "devDependencies": {
70
- "@eslint-community/eslint-plugin-eslint-comments": "4.7.2",
70
+ "@eslint-community/eslint-plugin-eslint-comments": "4.8.1",
71
71
  "@eslint/js": "10.0.1",
72
- "@playwright/test": "1.62.1",
72
+ "@playwright/test": "1.64.0",
73
73
  "@sveltejs/kit": "2.70.3",
74
74
  "@sveltejs/package": "2.5.8",
75
- "@sveltejs/vite-plugin-svelte": "7.3.0",
76
- "@types/node": "24.19.1",
75
+ "@sveltejs/vite-plugin-svelte": "7.3.1",
76
+ "@types/node": "24.19.2",
77
77
  "@types/pg": "8.23.1",
78
78
  "@types/smtp-server": "3.5.13",
79
- "eslint": "10.9.1",
79
+ "eslint": "10.12.0",
80
80
  "eslint-config-prettier": "10.1.8",
81
- "eslint-plugin-svelte": "3.23.0",
82
- "globals": "17.12.0",
83
- "prettier": "3.9.6",
81
+ "eslint-plugin-svelte": "3.23.1",
82
+ "globals": "17.13.0",
83
+ "prettier": "3.9.10",
84
84
  "prettier-plugin-svelte": "4.1.1",
85
- "smtp-server": "3.19.17",
85
+ "smtp-server": "3.19.22",
86
86
  "svelte": "5.57.2",
87
87
  "svelte-check": "4.7.6",
88
88
  "typescript": "6.0.3",
89
- "typescript-eslint": "8.69.0",
90
- "vite": "8.2.2",
91
- "vitest": "5.0.3"
89
+ "typescript-eslint": "8.71.2",
90
+ "vite": "8.3.4",
91
+ "vitest": "5.0.4"
92
92
  },
93
93
  "overrides": {
94
94
  "cookie": "0.7.0"