@zeyolabs/kit 2026.11.7 → 2026.11.8

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zeyolabs/kit",
3
- "version": "2026.11.7",
3
+ "version": "2026.11.8",
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": {
@@ -0,0 +1,53 @@
1
+ import { waitUntil } from '../cloudflare/wait-until';
2
+
3
+ // Shared shapes for the four layers every app is built from: libraries (static functions), clients
4
+ // (talk to the outside world), services (talk to the db) and managers (permission, service call,
5
+ // events). These are types and tiny helpers only: no base classes, so apps keep plain constructors.
6
+
7
+ /**
8
+ * Who a service acts for. A service is built as `new XService(ctx, db)` and every query it runs is
9
+ * scoped by `ctx.accountId`; `memberId` and `kind` identify the actor for audit and events.
10
+ */
11
+ export type ServiceCtx<Kind extends string = string> = {
12
+ accountId: number;
13
+ kind: Kind;
14
+ memberId: number;
15
+ };
16
+
17
+ /** What a service mutation returns: the result for the caller plus the events it caused. */
18
+ export type Mutation<T, E> = { events: E[]; result: T };
19
+
20
+ /** Builds a `Mutation`. `events` defaults to none. */
21
+ export function mutation<T, E>(result: T, events: E[] = []): Mutation<T, E> {
22
+ return { events, result };
23
+ }
24
+
25
+ /**
26
+ * Where a manager sends the events a service returned (a realtime hub, a queue, a log). `publish`
27
+ * never blocks the caller and never throws: a failed publish must not fail the mutation that was
28
+ * already committed.
29
+ */
30
+ export type EventPublisher<E> = { publish(events: E[]): void };
31
+
32
+ /**
33
+ * An `EventPublisher` that hands `send` the events in the background (`waitUntil` in the Workers
34
+ * runtime, fire-and-forget elsewhere) and swallows its errors, passing them to `onError` if given.
35
+ */
36
+ export function backgroundPublisher<E>(
37
+ send: (events: E[]) => Promise<unknown>,
38
+ onError?: (error: unknown) => void
39
+ ): EventPublisher<E> {
40
+ return {
41
+ publish(events) {
42
+ if (events.length === 0) return;
43
+ const run = (async () => {
44
+ try {
45
+ await send(events);
46
+ } catch (error) {
47
+ onError?.(error);
48
+ }
49
+ })();
50
+ void waitUntil(run);
51
+ },
52
+ };
53
+ }
@@ -0,0 +1,29 @@
1
+ import type { Membership } from '../auth/accounts/accounts';
2
+
3
+ import { Accounts } from '../auth/accounts/accounts';
4
+ import { Error403 } from '../utils/errors';
5
+
6
+ // The manager gate. A manager is permission check, then a service call, then events. Each edge (a
7
+ // SvelteKit hook, an MCP session, a Worker) resolves the principal its own way and passes the
8
+ // membership in, so managers need no request and are testable without one.
9
+
10
+ /** Throws a 403 unless `membership` grants `permission`. A missing membership fails closed. */
11
+ export function requirePermission(membership: Membership | null, permission: string): Membership {
12
+ if (!membership || !Accounts.can(membership, permission)) {
13
+ throw Error403('You do not have permission to do that');
14
+ }
15
+ return membership;
16
+ }
17
+
18
+ /**
19
+ * Runs `fn` with the principal (membership now non-null), or throws a 403 before `fn` is called.
20
+ * Anything else the edge put on the principal (member id, actor kind, account id) comes through.
21
+ */
22
+ export async function withPermission<P extends { membership: Membership | null }, T>(
23
+ principal: P,
24
+ permission: string,
25
+ fn: (principal: P & { membership: Membership }) => Promise<T> | T
26
+ ): Promise<T> {
27
+ const membership = requirePermission(principal.membership, permission);
28
+ return await fn({ ...principal, membership });
29
+ }