@zeyolabs/kit 2026.11.8 → 2026.11.10
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/eslint-config.js +68 -0
- package/package.json +1 -1
- package/server/turnstile.ts +57 -6
package/eslint-config.js
CHANGED
|
@@ -113,3 +113,71 @@ export const drizzleRules = {
|
|
|
113
113
|
'drizzle/enforce-update-with-where': ['error', { drizzleObjectName }],
|
|
114
114
|
},
|
|
115
115
|
};
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Layer boundaries (see `standards/codex`): edges (remote functions, endpoints, hooks) reach the db
|
|
119
|
+
* only through managers; services never import managers; clients and libraries import nothing
|
|
120
|
+
* above them. Files are matched by name, so features keep `manager.server.ts`,
|
|
121
|
+
* `service.server.ts` and `client.server.ts` (or a `clients/` folder) and libraries live in `utils/`.
|
|
122
|
+
*
|
|
123
|
+
* - Starts as a warning so existing code can migrate; pass `severity: 'error'` once it is clean.
|
|
124
|
+
* - A file belongs to the first group it matches, in the order edge, service, client, library, so
|
|
125
|
+
* `utils/clients/x.ts` is a client and gets only the client rules.
|
|
126
|
+
* - Flat config does not merge `no-restricted-imports` across entries. Put these rules LAST, and
|
|
127
|
+
* pass your own restricted-import patterns as `extraPatterns` (added to every group).
|
|
128
|
+
* - Matching is by import specifier, so a barrel re-export bypasses it. An app with another layout
|
|
129
|
+
* (e.g. `services/*.ts`) passes its own `globs` (which files are which layer) and `imports`
|
|
130
|
+
* (which specifiers each layer is imported by). A green lint is not proof the boundaries hold.
|
|
131
|
+
* @param {{
|
|
132
|
+
* severity?: 'error' | 'warn',
|
|
133
|
+
* globs?: { client?: string[], edge?: string[], library?: string[], service?: string[] },
|
|
134
|
+
* imports?: { client?: string[], manager?: string[], service?: string[] },
|
|
135
|
+
* extraPatterns?: { group: string[], message?: string }[],
|
|
136
|
+
* }} [options]
|
|
137
|
+
*/
|
|
138
|
+
export function layerBoundaryRules(options = {}) {
|
|
139
|
+
const severity = options.severity ?? 'warn';
|
|
140
|
+
const extra = options.extraPatterns ?? [];
|
|
141
|
+
const globs = {
|
|
142
|
+
client: ['**/client.server.ts', '**/clients/**/*.ts'],
|
|
143
|
+
edge: ['**/*.remote.ts', '**/+server.ts', '**/+page.server.ts', '**/hooks.server.ts'],
|
|
144
|
+
library: ['**/utils/**/*.ts'],
|
|
145
|
+
service: ['**/service.server.ts'],
|
|
146
|
+
...options.globs,
|
|
147
|
+
};
|
|
148
|
+
const imports = {
|
|
149
|
+
client: ['**/client.server', '**/client.server.ts', '**/clients/**'],
|
|
150
|
+
manager: ['**/manager.server', '**/manager.server.ts'],
|
|
151
|
+
service: ['**/service.server', '**/service.server.ts'],
|
|
152
|
+
...options.imports,
|
|
153
|
+
};
|
|
154
|
+
const forbid = (group, message) => ({ group, message });
|
|
155
|
+
// First match wins: each group ignores the files of the groups before it.
|
|
156
|
+
const groups = [
|
|
157
|
+
[globs.edge, [forbid(imports.service, 'Edges call managers, not services (layer boundary).')]],
|
|
158
|
+
[
|
|
159
|
+
globs.service,
|
|
160
|
+
[forbid(imports.manager, 'Services must not import managers (layer boundary).')],
|
|
161
|
+
],
|
|
162
|
+
[
|
|
163
|
+
globs.client,
|
|
164
|
+
[
|
|
165
|
+
forbid(imports.manager, 'Clients must not import managers (layer boundary).'),
|
|
166
|
+
forbid(imports.service, 'Clients must not import services (layer boundary).'),
|
|
167
|
+
],
|
|
168
|
+
],
|
|
169
|
+
[
|
|
170
|
+
globs.library,
|
|
171
|
+
[
|
|
172
|
+
forbid(imports.manager, 'Libraries must not import managers (layer boundary).'),
|
|
173
|
+
forbid(imports.service, 'Libraries must not import services (layer boundary).'),
|
|
174
|
+
forbid(imports.client, 'Libraries must not import clients (layer boundary).'),
|
|
175
|
+
],
|
|
176
|
+
],
|
|
177
|
+
];
|
|
178
|
+
return groups.map(([files, patterns], index) => ({
|
|
179
|
+
files,
|
|
180
|
+
ignores: groups.slice(0, index).flatMap(([earlier]) => earlier),
|
|
181
|
+
rules: { 'no-restricted-imports': [severity, { patterns: [...patterns, ...extra] }] },
|
|
182
|
+
}));
|
|
183
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zeyolabs/kit",
|
|
3
|
-
"version": "2026.11.
|
|
3
|
+
"version": "2026.11.10",
|
|
4
4
|
"description": "Shared SvelteKit-on-Cloudflare server toolkit: remote-function wrappers, passwordless auth (magic link + passkeys), email over a send_email binding or SMTP, Cloudflare context, monitoring, storage clients, utils and the ESLint config.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
package/server/turnstile.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
import { FormResult } from '../utils/form';
|
|
2
|
+
|
|
3
|
+
const SITEVERIFY_URL = 'https://challenges.cloudflare.com/turnstile/v0/siteverify';
|
|
4
|
+
|
|
1
5
|
/**
|
|
2
6
|
* Server-side verification of a Cloudflare Turnstile token against the siteverify API. Pure
|
|
3
|
-
* `fetch`, no framework or binding dependency.
|
|
4
|
-
* a rejection surfaces (see `apps/web/src/lib/server/turnstile.ts`).
|
|
7
|
+
* `fetch`, no framework or binding dependency. Prefer `Turnstile.verify` / `Turnstile.gate`.
|
|
5
8
|
*/
|
|
6
9
|
export async function verifyTurnstile(
|
|
7
10
|
token: string,
|
|
@@ -10,10 +13,58 @@ export async function verifyTurnstile(
|
|
|
10
13
|
): Promise<boolean> {
|
|
11
14
|
const params = new URLSearchParams({ response: token, secret: secretKey });
|
|
12
15
|
if (ip) params.set('remoteip', ip);
|
|
13
|
-
const response = await fetch('
|
|
14
|
-
body: params,
|
|
15
|
-
method: 'POST',
|
|
16
|
-
});
|
|
16
|
+
const response = await fetch(SITEVERIFY_URL, { body: params, method: 'POST' });
|
|
17
17
|
const result = (await response.json()) as { success?: boolean };
|
|
18
18
|
return result.success === true;
|
|
19
19
|
}
|
|
20
|
+
|
|
21
|
+
/** Where a gate gets the request's secret key and client address. The app supplies both. */
|
|
22
|
+
export type TurnstileGateOptions = {
|
|
23
|
+
/** Client address per call (SvelteKit's `getRequestEvent().getClientAddress()`); may be null. */
|
|
24
|
+
ip?: () => null | string | undefined;
|
|
25
|
+
/** The error shown when the check fails. */
|
|
26
|
+
message?: string;
|
|
27
|
+
/** The secret key: a string, or a function called per check (a Secrets Store binding, a loopback test key). */
|
|
28
|
+
secret: (() => Promise<string> | string) | string;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Turnstile helpers: a library (an object of static functions, no state, no bindings), like
|
|
33
|
+
* `Schema` and `FormResult`.
|
|
34
|
+
*/
|
|
35
|
+
export const Turnstile = {
|
|
36
|
+
/**
|
|
37
|
+
* Gates a form handler behind Turnstile. The validated input carries `turnstileToken`; the gate
|
|
38
|
+
* verifies it and strips it before calling the handler. A missing or rejected token returns
|
|
39
|
+
* `FormResult.err(message)` and the handler never runs; a throwing `secret` or `fetch` also
|
|
40
|
+
* stops the handler (fails closed).
|
|
41
|
+
*/
|
|
42
|
+
gate({
|
|
43
|
+
ip = () => null,
|
|
44
|
+
message = 'Bot check failed. Please try again.',
|
|
45
|
+
secret,
|
|
46
|
+
}: TurnstileGateOptions) {
|
|
47
|
+
return function gated<TData extends { turnstileToken: string }, TResult>(
|
|
48
|
+
handler: (data: Omit<TData, 'turnstileToken'>) => Promise<FormResult<TResult | void>>
|
|
49
|
+
): (data: TData) => Promise<FormResult<TResult | void>> {
|
|
50
|
+
return async ({ turnstileToken, ...data }: TData) => {
|
|
51
|
+
if (!(await Turnstile.verify(turnstileToken, ip(), secret))) return FormResult.err(message);
|
|
52
|
+
return handler(data as Omit<TData, 'turnstileToken'>);
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* True only when siteverify accepts the token. An empty token is false without a request, and
|
|
59
|
+
* `secret` is only resolved when there is a token to check.
|
|
60
|
+
*/
|
|
61
|
+
async verify(
|
|
62
|
+
token: string,
|
|
63
|
+
ip: null | string | undefined,
|
|
64
|
+
secret: (() => Promise<string> | string) | string
|
|
65
|
+
): Promise<boolean> {
|
|
66
|
+
if (!token) return false;
|
|
67
|
+
const key = typeof secret === 'function' ? await secret() : secret;
|
|
68
|
+
return verifyTurnstile(token, ip, key);
|
|
69
|
+
},
|
|
70
|
+
};
|