@somewhatintelligent/kit 0.0.1-beta.1783705886.297c5be

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.
@@ -0,0 +1,72 @@
1
+ /**
2
+ * HTTP-edge wrapper. Use inside framework-specific request middleware
3
+ * (TanStack Start `createMiddleware({type: "request"})`, Elysia `onRequest`/
4
+ * `onAfterHandle`, raw `fetch()` handlers) to emit one `event: "http"` line
5
+ * per inbound request — captures method/path automatically, lets the
6
+ * middleware add status/userId/etc via the builder.
7
+ *
8
+ * The default operation name is `"http.<method-lowercased>"`; pass
9
+ * `deriveOperation` to use route-derived ops like `"roadie.api.upload"`
10
+ * when the framework knows the matched route.
11
+ *
12
+ * Usage (TanStack Start middleware):
13
+ *
14
+ * const middleware = createMiddleware({ type: "request" }).server(
15
+ * async ({ next, request }) =>
16
+ * withRequestLog(
17
+ * {
18
+ * service: "roadie",
19
+ * resolveContext: (req) => ({
20
+ * requestId: req.headers.get("cf-request-id") ?? ulid(),
21
+ * actorKind: "anonymous",
22
+ * actorId: null,
23
+ * }),
24
+ * },
25
+ * request,
26
+ * async (log) => {
27
+ * const result = await next();
28
+ * log.add({ status: result.response.status });
29
+ * if (result.response.status >= 500) log.outcome("internal_error");
30
+ * else if (result.response.status >= 400) log.outcome(`http_${result.response.status}`);
31
+ * return result;
32
+ * },
33
+ * ),
34
+ * );
35
+ */
36
+ import { type CanonicalLogBuilder, type CanonicalLogContext, withCanonicalLog } from "./core";
37
+
38
+ type PartialContext = Omit<CanonicalLogContext, "service" | "event" | "operation">;
39
+
40
+ export interface RequestLogConfig {
41
+ service: string;
42
+ /**
43
+ * Optional. Provide explicit ctx fields when not relying on the
44
+ * request-context ALS. When omitted, `withCanonicalLog` reads
45
+ * requestId/actorKind/actorId/callerApp from ALS at emit time.
46
+ */
47
+ resolveContext?: (req: Request) => Promise<PartialContext> | PartialContext;
48
+ /** Override op name. Default: `"http.<method-lowercased>"`. */
49
+ deriveOperation?: (req: Request) => string;
50
+ }
51
+
52
+ export async function withRequestLog<T>(
53
+ config: RequestLogConfig,
54
+ req: Request,
55
+ fn: (log: CanonicalLogBuilder) => Promise<T>,
56
+ ): Promise<T> {
57
+ const partial = (await config.resolveContext?.(req)) ?? {};
58
+ const operation = config.deriveOperation?.(req) ?? `http.${req.method.toLowerCase()}`;
59
+ return withCanonicalLog(
60
+ {
61
+ ...partial,
62
+ service: config.service,
63
+ event: "http",
64
+ operation,
65
+ },
66
+ async (log) => {
67
+ const url = new URL(req.url);
68
+ log.add({ method: req.method, path: url.pathname });
69
+ return fn(log);
70
+ },
71
+ );
72
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Canonical log barrel — re-exports the core primitive (`./core`) plus the
3
+ * boundary wrappers (`./http`, `./instrumented`, `./scheduled`).
4
+ *
5
+ * Sub-modules import from `./core` rather than `./index` to keep this barrel
6
+ * acyclic. Read `./core` for the architectural overview.
7
+ */
8
+ export {
9
+ type CanonicalLogBuilder,
10
+ type CanonicalLogContext,
11
+ describeThrown,
12
+ getRequestLog,
13
+ type Level,
14
+ requireRequestLog,
15
+ withCanonicalLog,
16
+ } from "./core";
17
+ export { withRequestLog } from "./http";
18
+ export type { RequestLogConfig } from "./http";
19
+ export { instrumented, logged } from "./instrumented";
20
+ export type { InstrumentationConfig } from "./instrumented";
21
+ export { loggedJob } from "./scheduled";
22
+ export type { JobLogConfig } from "./scheduled";
@@ -0,0 +1,191 @@
1
+ /**
2
+ * Class-level instrumentation for RPC service classes.
3
+ *
4
+ * Walks the class prototype at install time and wraps every async method
5
+ * with a `withCanonicalLog` scope. Method bodies stay as plain method
6
+ * definitions (required for Cloudflare Workers RPC dispatch — arrow-fn
7
+ * class fields aren't on the prototype, so CF can't see them as RPC methods).
8
+ * Domain fields are accrued in handler bodies via `requireRequestLog().add()`.
9
+ *
10
+ * `@instrumented({...})` — TC39 Stage 3 class decorator. Use when the
11
+ * bundler transforms decorator syntax (vite-plus apps, CF Agents apps).
12
+ *
13
+ * Usage:
14
+ *
15
+ * @instrumented({
16
+ * service: "roadie",
17
+ * resolveContext: ({ methodName, args, instance }) => {
18
+ * const meta = args[1] as CallMeta;
19
+ * return {
20
+ * requestId: meta.requestId,
21
+ * actorKind: meta.actor.kind,
22
+ * actorId: actorIdFor(meta.actor),
23
+ * callerApp: readCallerApp(instance, meta),
24
+ * };
25
+ * },
26
+ * deriveOutcome: (ret) => ret.ok ? "ok" : ret.error,
27
+ * onError: (e) => err("internal_error", String(e)),
28
+ * })
29
+ * export class Roadie extends WorkerEntrypoint<RoadieEnv> {
30
+ * async signPart(input, meta) {
31
+ * requireRequestLog().add({ reference_id: input.referenceId });
32
+ * return ok(presigned);
33
+ * }
34
+ *
35
+ * @logged.skip
36
+ * async healthCheck() { return ok({ status: "healthy" }); }
37
+ * }
38
+ */
39
+ import { type CanonicalLogContext, describeThrown, withCanonicalLog } from "./core";
40
+
41
+ export interface InstrumentationConfig {
42
+ /** Service name for emitted lines — `"roadie"`, `"guestlist"`, `"promoter"`. */
43
+ service: string;
44
+ /**
45
+ * Resolve per-call canonical log context from method args/instance.
46
+ * Returns context fields excluding `service`/`event`/`operation` (filled
47
+ * by the wrapper). May override `operation` to use a non-default name.
48
+ */
49
+ resolveContext: (args: { methodName: string; args: unknown[]; instance: unknown }) => Omit<
50
+ CanonicalLogContext,
51
+ "service" | "event" | "operation"
52
+ > & {
53
+ operation?: string;
54
+ };
55
+ /**
56
+ * Inspect a method's normal return value to derive outcome. Useful for
57
+ * `Result<T, E>`-discriminant inference. Called only on normal return;
58
+ * thrown errors set outcome to `"internal_error"` automatically. Return
59
+ * `undefined` to keep the default `"ok"` outcome.
60
+ */
61
+ deriveOutcome?: (returnValue: unknown) => string | undefined;
62
+ /**
63
+ * Convert a thrown error into a return value. Used by services with
64
+ * Result-typed APIs (like Roadie) that document "methods never throw —
65
+ * always return Result<T, E>". When set, exceptions are caught, the
66
+ * line emits with `outcome: "internal_error"` + `error_message`, and
67
+ * the handler's return value is returned to the caller in place of
68
+ * rethrowing. When unset (default), exceptions propagate per kit's
69
+ * primitive behavior.
70
+ */
71
+ onError?: (e: unknown) => unknown;
72
+ }
73
+
74
+ const SKIPPED = new WeakSet<object>();
75
+
76
+ /**
77
+ * Method-level decorator. Mark a method to be excluded from class-level
78
+ * `@instrumented` wrapping — useful for health checks, internal helpers,
79
+ * or methods that emit their own log lines via `withCanonicalLog` directly.
80
+ */
81
+ export const logged = {
82
+ skip<This, Args extends unknown[], Return>(
83
+ target: (this: This, ...args: Args) => Return,
84
+ _context: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => Return>,
85
+ ): (this: This, ...args: Args) => Return {
86
+ SKIPPED.add(target as object);
87
+ return target;
88
+ },
89
+ };
90
+
91
+ // biome-ignore lint/suspicious/noExplicitAny: class decorators need broad constructor signatures
92
+ type Constructor = abstract new (...args: any[]) => object;
93
+
94
+ function makeWrapped(
95
+ name: string,
96
+ original: (...args: unknown[]) => unknown,
97
+ config: InstrumentationConfig,
98
+ ) {
99
+ return async function wrapped(this: unknown, ...args: unknown[]) {
100
+ let partial: ReturnType<InstrumentationConfig["resolveContext"]>;
101
+ try {
102
+ partial = config.resolveContext({ methodName: name, args, instance: this });
103
+ } catch (e) {
104
+ // `resolveContext` runs BEFORE the main scope opens and can throw — e.g. a
105
+ // misconfigured RPC binding whose `caller_app` can't be resolved. Without
106
+ // this guard that throw escapes with NO canonical line AND bypasses
107
+ // `onError`, so the runtime records `outcome:"exception"` with empty
108
+ // `logs`/`exceptions`: a fatal, every-request misconfiguration that is
109
+ // completely invisible in the tail — the single worst observability hole.
110
+ // Open a minimal scope (correlation fields still resolve from ALS at emit
111
+ // time) so the failure ALWAYS emits an actionable line, then honor
112
+ // `onError`/rethrow exactly like the body path below.
113
+ const { message, stack } = describeThrown(e);
114
+ return withCanonicalLog(
115
+ { service: config.service, event: "rpc", operation: `${config.service}.${name}` },
116
+ async (log) => {
117
+ log.outcome("internal_error");
118
+ log.add({
119
+ error_message: message,
120
+ error_stack: stack ?? message,
121
+ error_phase: "resolve_context",
122
+ });
123
+ if (!config.onError) throw e;
124
+ return config.onError(e);
125
+ },
126
+ );
127
+ }
128
+ const operation = partial.operation ?? `${config.service}.${name}`;
129
+ return withCanonicalLog(
130
+ {
131
+ service: config.service,
132
+ event: "rpc",
133
+ operation,
134
+ requestId: partial.requestId,
135
+ actorKind: partial.actorKind,
136
+ actorId: partial.actorId,
137
+ callerApp: partial.callerApp,
138
+ errorOutcomes: partial.errorOutcomes,
139
+ forbiddenFields: partial.forbiddenFields,
140
+ forbiddenPrefixes: partial.forbiddenPrefixes,
141
+ },
142
+ async (log) => {
143
+ let result: unknown;
144
+ try {
145
+ result = await (original as (...a: unknown[]) => Promise<unknown>).apply(this, args);
146
+ } catch (e) {
147
+ if (!config.onError) throw e;
148
+ const { message } = describeThrown(e);
149
+ log.outcome("internal_error");
150
+ log.add({ error_message: message });
151
+ return config.onError(e);
152
+ }
153
+ if (config.deriveOutcome) {
154
+ const outcome = config.deriveOutcome(result);
155
+ if (outcome !== undefined) log.outcome(outcome);
156
+ }
157
+ return result;
158
+ },
159
+ );
160
+ };
161
+ }
162
+
163
+ function applyInstrumentation<T extends Constructor>(
164
+ target: T,
165
+ config: InstrumentationConfig,
166
+ skip?: ReadonlySet<string>,
167
+ ): T {
168
+ const proto = target.prototype as Record<string, unknown>;
169
+ for (const name of Object.getOwnPropertyNames(proto)) {
170
+ if (name === "constructor") continue;
171
+ if (skip?.has(name)) continue;
172
+ const descriptor = Object.getOwnPropertyDescriptor(proto, name);
173
+ if (!descriptor || typeof descriptor.value !== "function") continue;
174
+ const original = descriptor.value as (...args: unknown[]) => unknown;
175
+ if (SKIPPED.has(original as object)) continue;
176
+ Object.defineProperty(proto, name, {
177
+ ...descriptor,
178
+ value: makeWrapped(name, original, config),
179
+ });
180
+ }
181
+ return target;
182
+ }
183
+
184
+ export function instrumented(config: InstrumentationConfig) {
185
+ return function decorate<T extends Constructor>(
186
+ target: T,
187
+ _context: ClassDecoratorContext<T>,
188
+ ): T {
189
+ return applyInstrumentation(target, config);
190
+ };
191
+ }
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Scheduled-job wrapper. Use to wrap Worker `scheduled()` handlers and
3
+ * any background task that runs outside an inbound request — synthesizes
4
+ * a `requestId` (crypto.randomUUID by default; pass `generateRequestId`
5
+ * to use a ulid factory or your platform's tracing ID), sets
6
+ * `actor: { kind: "service", id: service }`, and emits the canonical
7
+ * line as `event: "job"`.
8
+ *
9
+ * Usage:
10
+ *
11
+ * import { loggedJob, requireRequestLog } from "@somewhatintelligent/kit/log";
12
+ * import { ulid } from "@somewhatintelligent/kit/ids";
13
+ *
14
+ * const reapExpiredGrants = loggedJob(
15
+ * {
16
+ * service: "roadie",
17
+ * operation: "roadie.job.reap_expired_grants",
18
+ * generateRequestId: () => ulid(),
19
+ * },
20
+ * async (env: Env, ctx: ExecutionContext) => {
21
+ * const reaped = await sweepGrants(env);
22
+ * requireRequestLog().add({ reaped_count: reaped.length });
23
+ * },
24
+ * );
25
+ *
26
+ * export default {
27
+ * async scheduled(_controller, env, ctx) {
28
+ * await reapExpiredGrants(env, ctx);
29
+ * },
30
+ * };
31
+ */
32
+ import { withRequestContext } from "../request-context";
33
+ import { type CanonicalLogContext, withCanonicalLog } from "./core";
34
+
35
+ type ExtraContext = Partial<
36
+ Omit<
37
+ CanonicalLogContext,
38
+ "service" | "event" | "operation" | "requestId" | "actorKind" | "actorId"
39
+ >
40
+ >;
41
+
42
+ export interface JobLogConfig {
43
+ service: string;
44
+ operation: string;
45
+ /** Synthesize a requestId for this job run. Default: `crypto.randomUUID()`. */
46
+ generateRequestId?: () => string;
47
+ /** Optional extra context fields (callerApp, custom forbiddenFields, etc). */
48
+ resolveContext?: () => Promise<ExtraContext> | ExtraContext;
49
+ }
50
+
51
+ // Opens both the canonical-log scope (for the `event: "job"` line) and the
52
+ // request-context ALS scope so the handler — and anything it calls — can
53
+ // read the synthesized requestId via `getRequestId()` without closure
54
+ // tricks. Matches the pattern app fetch handlers use.
55
+ export function loggedJob<Args extends unknown[], Return>(
56
+ config: JobLogConfig,
57
+ handler: (...args: Args) => Promise<Return>,
58
+ ): (...args: Args) => Promise<Return> {
59
+ return async (...args) => {
60
+ const requestId = config.generateRequestId?.() ?? crypto.randomUUID();
61
+ const extra = (await config.resolveContext?.()) ?? {};
62
+ return withRequestContext(
63
+ {
64
+ requestId,
65
+ actorKind: "service",
66
+ actorId: config.service,
67
+ callerApp: extra.callerApp,
68
+ },
69
+ () =>
70
+ withCanonicalLog(
71
+ {
72
+ ...extra,
73
+ service: config.service,
74
+ event: "job",
75
+ operation: config.operation,
76
+ requestId,
77
+ actorKind: "service",
78
+ actorId: config.service,
79
+ },
80
+ async () => handler(...args),
81
+ ),
82
+ );
83
+ };
84
+ }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Framework-independent auth context.
3
+ *
4
+ * `createAuthContext<S>()` returns a `{ Context, useAuth, BaseAuthProvider }`
5
+ * triple. `BaseAuthProvider` is a thin wrapper around `Context.Provider` —
6
+ * it does NOT manage state, fetch sessions, or know about any data-fetching
7
+ * framework. State + refetch is layered on top by framework-specific
8
+ * providers (see `kit/react-start`'s `createReactStartAuthProvider` for the
9
+ * TanStack Start binding; future Expo / Storybook bindings would re-implement
10
+ * the provider with their own data idiom but reuse the same `useAuth()`
11
+ * consumer interface).
12
+ *
13
+ * Apps glue it together once per app:
14
+ *
15
+ * const authContext = createAuthContext<PlatformSession>();
16
+ * export const { useAuth } = authContext;
17
+ * export const AuthProvider = createReactStartAuthProvider({
18
+ * authContext,
19
+ * loadSession,
20
+ * });
21
+ */
22
+ import { createContext, useContext, type ReactNode } from "react";
23
+
24
+ export interface AuthContextValue<S> {
25
+ /** Current session value. `null` when unauthenticated. */
26
+ session: S | null;
27
+ /** Convenience: `session?.user ?? null`. */
28
+ user: S extends { user: infer U } ? U | null : null;
29
+ /** `session != null`. */
30
+ isAuthenticated: boolean;
31
+ /** `true` while a refetch is in-flight. */
32
+ isLoading: boolean;
33
+ /** Re-runs the loader; updates `session`/`isLoading` state. */
34
+ refetch: () => Promise<void>;
35
+ /** Strict equality match against `session.user.role`. */
36
+ hasRole: (role: string) => boolean;
37
+ /** Returns `true` if the user's role is in `roles`. */
38
+ hasAnyRole: (roles: ReadonlyArray<string>) => boolean;
39
+ }
40
+
41
+ export function createAuthContext<S extends { user: { role?: string | null } }>() {
42
+ const Ctx = createContext<AuthContextValue<S> | null>(null);
43
+
44
+ function useAuth(): AuthContextValue<S> {
45
+ const value = useContext(Ctx);
46
+ if (!value) {
47
+ throw new Error("useAuth() must be called inside an AuthProvider");
48
+ }
49
+ return value;
50
+ }
51
+
52
+ function BaseAuthProvider({
53
+ value,
54
+ children,
55
+ }: {
56
+ value: AuthContextValue<S>;
57
+ children: ReactNode;
58
+ }) {
59
+ return <Ctx.Provider value={value}>{children}</Ctx.Provider>;
60
+ }
61
+
62
+ return { Context: Ctx, useAuth, BaseAuthProvider };
63
+ }
@@ -0,0 +1,2 @@
1
+ export { createAuthContext } from "./auth";
2
+ export type { AuthContextValue } from "./auth";
@@ -0,0 +1,153 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { PLATFORM_HEADERS } from "@somewhatintelligent/auth";
3
+ import { createDevEnvelopeStamper } from "../dev-envelope";
4
+
5
+ // Fixture dev host — the stamper is host-agnostic; any dev-shaped host works.
6
+ const chatDevHost = "chat.platform.localhost";
7
+
8
+ // Well-known dev-only keypair fixture; the matching public half is what a
9
+ // consumer commits in its keyset under `kid: "dev"`. Safe to
10
+ // inline here for tests; this is the same key bouncer's dev `.dev.vars` uses.
11
+ const DEV_PRIV_PEM = `-----BEGIN PRIVATE KEY-----
12
+ MC4CAQAwBQYDK2VwBCIEINzNgiuDD9xbqVEPkfMt8twPcq7hTnIbAdKKHPjM7TmU
13
+ -----END PRIVATE KEY-----`;
14
+
15
+ const userFixture = {
16
+ id: "user_test",
17
+ role: "member",
18
+ name: "Alice",
19
+ email: "alice@example.com",
20
+ image: null,
21
+ };
22
+ const sessionFixture = {
23
+ id: "sess_test",
24
+ userId: "user_test",
25
+ expiresAt: new Date(Date.now() + 60_000),
26
+ };
27
+
28
+ function makeGuestlist(opts: {
29
+ session?: { user: typeof userFixture; session: typeof sessionFixture } | null;
30
+ calls?: { count: number };
31
+ }) {
32
+ return (_req: Request) => ({
33
+ getSession: async () => {
34
+ if (opts.calls) opts.calls.count += 1;
35
+ return opts.session ?? null;
36
+ },
37
+ });
38
+ }
39
+
40
+ describe("createDevEnvelopeStamper", () => {
41
+ test("non-dev env → hard no-op (request returned by identity, no cookies)", async () => {
42
+ const calls = { count: 0 };
43
+ for (const envLabel of ["production", "staging", "preview", undefined]) {
44
+ const stamper = createDevEnvelopeStamper({
45
+ getEnvironment: () => envLabel,
46
+ getSigner: () => {
47
+ throw new Error("signer must not be invoked outside dev");
48
+ },
49
+ getGuestlist: makeGuestlist({ session: null, calls }),
50
+ });
51
+ const req = new Request("https://chat.example/");
52
+ const out = await stamper(req);
53
+ expect(out.request).toBe(req);
54
+ expect(out.setCookies).toEqual([]);
55
+ }
56
+ expect(calls.count).toBe(0);
57
+ });
58
+
59
+ test("dev + existing envelope header → passthrough (no remint)", async () => {
60
+ let signerCalls = 0;
61
+ const stamper = createDevEnvelopeStamper({
62
+ getEnvironment: () => "development",
63
+ getSigner: () => {
64
+ signerCalls += 1;
65
+ return { privPem: DEV_PRIV_PEM, kid: "dev" };
66
+ },
67
+ getGuestlist: makeGuestlist({ session: null }),
68
+ });
69
+ const req = new Request("https://chat.example/", {
70
+ headers: { [PLATFORM_HEADERS.att]: "preexisting.jws.value" },
71
+ });
72
+ const out = await stamper(req);
73
+ expect(out.request).toBe(req);
74
+ expect(out.request.headers.get(PLATFORM_HEADERS.att)).toBe("preexisting.jws.value");
75
+ expect(out.setCookies).toEqual([]);
76
+ expect(signerCalls).toBe(0);
77
+ });
78
+
79
+ test("dev + no envelope + authed session → stamps a JWS-compact envelope", async () => {
80
+ const stamper = createDevEnvelopeStamper({
81
+ getEnvironment: () => "development",
82
+ getSigner: () => ({ privPem: DEV_PRIV_PEM, kid: "dev" }),
83
+ getGuestlist: makeGuestlist({
84
+ session: { user: userFixture, session: sessionFixture },
85
+ }),
86
+ });
87
+ const req = new Request("https://chat.example/whatever");
88
+ const out = await stamper(req);
89
+ expect(out.request).not.toBe(req);
90
+ const att = out.request.headers.get(PLATFORM_HEADERS.att);
91
+ expect(att).toBeTruthy();
92
+ const parts = att!.split(".");
93
+ expect(parts).toHaveLength(3);
94
+ const payload = JSON.parse(
95
+ new TextDecoder().decode(
96
+ Uint8Array.from(atob(parts[1]!.replace(/-/g, "+").replace(/_/g, "/")), (c) =>
97
+ c.charCodeAt(0),
98
+ ),
99
+ ),
100
+ );
101
+ expect(payload.iss).toBe("bouncer");
102
+ expect(payload.host).toBe("chat.example");
103
+ expect(payload.actor).toMatchObject({
104
+ kind: "user",
105
+ id: "user_test",
106
+ role: "member",
107
+ name: "Alice",
108
+ email: "alice@example.com",
109
+ });
110
+ expect(payload.session).toMatchObject({ id: "sess_test", userId: "user_test" });
111
+ });
112
+
113
+ test("dev + no envelope + no session → still mints a signed envelope with actor:null", async () => {
114
+ const stamper = createDevEnvelopeStamper({
115
+ getEnvironment: () => "development",
116
+ getSigner: () => ({ privPem: DEV_PRIV_PEM, kid: "dev" }),
117
+ getGuestlist: makeGuestlist({ session: null }),
118
+ });
119
+ const req = new Request("https://chat.example/public");
120
+ const out = await stamper(req);
121
+ const att = out.request.headers.get(PLATFORM_HEADERS.att);
122
+ expect(att).toBeTruthy();
123
+ const payload = JSON.parse(
124
+ new TextDecoder().decode(
125
+ Uint8Array.from(atob(att!.split(".")[1]!.replace(/-/g, "+").replace(/_/g, "/")), (c) =>
126
+ c.charCodeAt(0),
127
+ ),
128
+ ),
129
+ );
130
+ expect(payload.actor).toBeNull();
131
+ expect(payload.session).toBeNull();
132
+ });
133
+
134
+ test("expectedHost override pins payload.host", async () => {
135
+ const stamper = createDevEnvelopeStamper({
136
+ getEnvironment: () => "development",
137
+ getSigner: () => ({ privPem: DEV_PRIV_PEM, kid: "dev" }),
138
+ getGuestlist: makeGuestlist({ session: null }),
139
+ expectedHost: chatDevHost,
140
+ });
141
+ const req = new Request("http://127.0.0.1:8788/");
142
+ const out = await stamper(req);
143
+ const att = out.request.headers.get(PLATFORM_HEADERS.att)!;
144
+ const payload = JSON.parse(
145
+ new TextDecoder().decode(
146
+ Uint8Array.from(atob(att.split(".")[1]!.replace(/-/g, "+").replace(/_/g, "/")), (c) =>
147
+ c.charCodeAt(0),
148
+ ),
149
+ ),
150
+ );
151
+ expect(payload.host).toBe(chatDevHost);
152
+ });
153
+ });
@@ -0,0 +1,77 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import {
3
+ createGuestlistFactory,
4
+ createLoggingFunctionMiddleware,
5
+ createRequestLogger,
6
+ createRoadieFactory,
7
+ createSessionFactory,
8
+ } from "../index";
9
+ import { createReactStartAuthProvider } from "../client";
10
+ import { createAuthContext } from "../../react";
11
+
12
+ type FakeSession = { user: { id: string; email: string; role?: string | null } };
13
+
14
+ // Smoke: each factory returns a TSS middleware object with an `options.server`
15
+ // callback. Behavioural coverage lives in `kit/log` and `kit/request-context`
16
+ // — these tests exist to catch wiring regressions (missing peer-dep, broken
17
+ // re-export, factory signature drift).
18
+ describe("kit/react-start factories return TSS middleware objects", () => {
19
+ test("createLoggingFunctionMiddleware", () => {
20
+ const m = createLoggingFunctionMiddleware({ service: "test" });
21
+ expect(m).toBeDefined();
22
+ expect(typeof (m as { options?: { server?: unknown } }).options?.server).toBe("function");
23
+ });
24
+
25
+ test("createRequestLogger", () => {
26
+ const m = createRequestLogger({ service: "test" });
27
+ expect(m).toBeDefined();
28
+ expect(typeof (m as { options?: { server?: unknown } }).options?.server).toBe("function");
29
+ });
30
+ });
31
+
32
+ describe("kit/react-start service-client factories", () => {
33
+ test("createGuestlistFactory returns a callable", () => {
34
+ const getGuestlist = createGuestlistFactory({
35
+ callerApp: "test",
36
+ createClient: () => ({ ok: true }),
37
+ fetcher: () => globalThis.fetch,
38
+ });
39
+ expect(typeof getGuestlist).toBe("function");
40
+ });
41
+
42
+ test("createRoadieFactory returns a callable", () => {
43
+ const getRoadie = createRoadieFactory({
44
+ callerApp: "test",
45
+ createClient: () => ({ ok: true }),
46
+ getBinding: () => ({}) as unknown,
47
+ });
48
+ expect(typeof getRoadie).toBe("function");
49
+ });
50
+
51
+ test("createSessionFactory returns a createServerOnlyFn-wrapped getSession", () => {
52
+ const getSession = createSessionFactory<FakeSession>({
53
+ getGuestlistFallback: async () => null,
54
+ });
55
+ expect(typeof getSession).toBe("function");
56
+ });
57
+ });
58
+
59
+ describe("kit/react-start auth-provider factory", () => {
60
+ test("createReactStartAuthProvider returns a React component", () => {
61
+ const authContext = createAuthContext<FakeSession>();
62
+ const Provider = createReactStartAuthProvider<FakeSession>({
63
+ authContext,
64
+ loadSession: async () => null,
65
+ });
66
+ expect(typeof Provider).toBe("function");
67
+ });
68
+ });
69
+
70
+ describe("kit/react createAuthContext", () => {
71
+ test("returns { Context, useAuth, BaseAuthProvider }", () => {
72
+ const ctx = createAuthContext<FakeSession>();
73
+ expect(ctx.Context).toBeDefined();
74
+ expect(typeof ctx.useAuth).toBe("function");
75
+ expect(typeof ctx.BaseAuthProvider).toBe("function");
76
+ });
77
+ });