@luckystack/core 0.4.0 → 0.5.0

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.
@@ -1,180 +1,180 @@
1
- # Session Types
2
-
3
- > Deep specs for the foundational session/auth types plus `validateRequest`, `isFalsy`, and `validateInputByType`. Source: `packages/core/src/sessionTypes.ts`, `validateRequest.ts`, `runtimeTypeValidation.ts`. Bijgewerkt: 2026-05-20.
4
-
5
- ## Overview
6
-
7
- `BaseSessionLayout` is the structural contract every project session type extends. It lives in `@luckystack/core` (not `@luckystack/login`) so `validateRequest` can consume it without creating a circular dep: login depends on core for prisma/redis, so the session shape has to live in the lower layer. `@luckystack/login` re-exports the type for ergonomics — existing `import type { BaseSessionLayout } from '@luckystack/login'` paths keep working.
8
-
9
- `AuthProps` defines the auth gate that every API/sync handler exports (`login: true/false` + an optional `additional[]` predicate list). `validateRequest({ auth, user })` is the runtime check that turns those declarations into a `ValidationResult`.
10
-
11
- `validateInputByType` is the runtime input validator used by the api/sync handlers in dev. It lazy-loads `@luckystack/devkit` (the heavy TS compiler resolver) only when running in non-production. In production it short-circuits to success because the prod esbuild bundle marks devkit as external and the validator should not need TS to validate request bodies at request time.
12
-
13
- ## Types
14
-
15
- ```typescript
16
- export interface SessionLocation {
17
- pathName: string;
18
- searchParams?: Record<string, string>;
19
- }
20
-
21
- export interface BaseSessionLayout {
22
- id: string;
23
- token: string;
24
- email?: string | null;
25
- name?: string | null;
26
- avatar?: string | null;
27
- avatarFallback?: string | null;
28
- admin?: boolean | null;
29
- language?: string | null;
30
- location?: SessionLocation;
31
- roomCodes?: string[];
32
- csrfToken?: string;
33
- lastLogin?: Date | string | null;
34
- previousLogin?: Date | string | null;
35
- }
36
-
37
- export interface AuthProps {
38
- login: boolean;
39
- additional?: {
40
- key: keyof BaseSessionLayout;
41
- value?: unknown;
42
- type?: 'string' | 'number' | 'boolean';
43
- nullish?: boolean;
44
- mustBeFalsy?: boolean;
45
- }[];
46
- }
47
-
48
- export interface ValidationResult {
49
- status: 'success' | 'error';
50
- errorCode?: string;
51
- errorParams?: { key: string; value: string | number | boolean }[];
52
- httpStatus?: number;
53
- }
54
- ```
55
-
56
- ### Extending `BaseSessionLayout`
57
-
58
- Projects extend the type in their own `config.ts`:
59
-
60
- ```typescript
61
- import type { BaseSessionLayout } from '@luckystack/core';
62
-
63
- export interface SessionLayout extends BaseSessionLayout {
64
- organizationId?: string;
65
- role?: 'member' | 'admin' | 'owner';
66
- }
67
- ```
68
-
69
- The framework's `validateRequest` only inspects keys it knows from `BaseSessionLayout`; project-specific keys are checked via `additional[]`.
70
-
71
- ## API Reference
72
-
73
- ### `isFalsy(value: unknown): boolean`
74
-
75
- **Signature:**
76
- ```typescript
77
- export const isFalsy = (value: unknown): boolean
78
- ```
79
-
80
- **Behavior:** Returns `true` for `false`, `0`, `0n`, `''`, `null`, `undefined`, or `NaN`. Everything else is truthy.
81
-
82
- **Note:** This is a stricter "falsy" than JavaScript's coercion-based check because it ALSO treats `NaN` as falsy. Used inside `validateRequest` for the `mustBeFalsy` predicate.
83
-
84
- ### `validateRequest({ auth, user }): ValidationResult`
85
-
86
- **Signature:**
87
- ```typescript
88
- export const validateRequest = ({
89
- auth,
90
- user,
91
- }: {
92
- auth: AuthProps;
93
- user: BaseSessionLayout;
94
- }): ValidationResult
95
- ```
96
-
97
- **Behavior (in order):**
98
- 1. If `auth.additional` is undefined → returns `{ status: 'success' }` immediately. (The `login` flag is enforced by the surrounding handler, not by this function.)
99
- 2. For each `condition` in `auth.additional`:
100
- - If `condition.key` is not in `user` → returns `{ status: 'error', errorCode: 'auth.invalidCondition', errorParams: [{ key: 'key', value: condition.key }], httpStatus: 500 }`. This is a developer setup error, not a runtime auth failure.
101
- - Reads `val = user[condition.key]`. Computes `isNullish = val === null || val === undefined`.
102
- - `nullish: true` requires `isNullish`; `nullish: false` requires NOT `isNullish`. Mismatch → forbid.
103
- - `type: 'string' | 'number' | 'boolean'` requires `typeof val === <type>` (skipped when `val` is nullish). Mismatch → forbid.
104
- - `'value' in condition` requires strict equality `val === condition.value`. The membership check distinguishes "no exact-value constraint" from `value: undefined` (the latter explicitly demands `val === undefined`).
105
- - `mustBeFalsy: true` requires `isFalsy(val)`; `mustBeFalsy: false` requires `!isFalsy(val)`. Mismatch → forbid.
106
- 3. After all conditions pass → returns `{ status: 'success' }`.
107
-
108
- **Forbid shape:**
109
- ```typescript
110
- { status: 'error', errorCode: 'auth.forbidden', errorParams: [{ key: 'key', value: <key> }], httpStatus: 403 }
111
- ```
112
-
113
- **Constraint AND-semantics:** Multiple fields on a single `additional` entry are AND'd (every specified field must pass). Entries across the array are also AND'd.
114
-
115
- **Example — admin-only endpoint:**
116
- ```typescript
117
- export const auth: AuthProps = {
118
- login: true,
119
- additional: [{ key: 'admin', value: true, type: 'boolean' }],
120
- };
121
- ```
122
-
123
- **Example — verified email required:**
124
- ```typescript
125
- export const auth: AuthProps = {
126
- login: true,
127
- additional: [{ key: 'email', nullish: false, type: 'string' }],
128
- };
129
- ```
130
-
131
- ### `validateInputByType({ typeText, value, rootKey, filePath? }): Promise<ValidationResult>`
132
-
133
- **Signature:**
134
- ```typescript
135
- type ValidationResult =
136
- | { status: 'success' }
137
- | { status: 'error'; message: string };
138
-
139
- export const validateInputByType = async ({
140
- typeText,
141
- value,
142
- rootKey,
143
- filePath,
144
- }: {
145
- typeText?: string;
146
- value: unknown;
147
- rootKey: string;
148
- filePath?: string;
149
- }): Promise<ValidationResult>
150
- ```
151
-
152
- **Behavior:**
153
- 1. When `typeText` is empty, whitespace, or literal `'any'` → returns `{ status: 'success' }`.
154
- 2. When `process.env.NODE_ENV === 'production'` → returns `{ status: 'success' }` (avoids loading the TS compiler in prod).
155
- 3. Otherwise lazy-imports `@luckystack/devkit` via an indirect module ID (`const devkitModuleId: string = '@luckystack/devkit'; await import(devkitModuleId)`) so `tsc` doesn't try to resolve devkit at build time (devkit depends on core — a literal import would be a build-time circular dep).
156
- 4. Calls `devkit.resolveRuntimeTypeText({ typeText, filePath })`. On `status: 'error'` returns `{ status: 'error', message: '<rootKey>: <message>' }`.
157
- 5. Otherwise calls the internal `validateType(resolvedType.typeText, value, rootKey)` recursive validator.
158
-
159
- **Supported type forms (validateType):**
160
- - Primitives: `string`, `number`, `boolean`, `true`, `false`, `null`, `undefined`, `Date` (accepts ISO string or `Date` instance).
161
- - String/number literal types (single-quoted or double-quoted).
162
- - Unions (`A | B`) — succeeds on first matching branch.
163
- - Intersections (`A & B`) — requires all branches to succeed.
164
- - Arrays (`T[]`) — checks every element.
165
- - `Record<K, V>` — accepts any non-array object.
166
- - Object literals `{ key?: T; key2: T2; [index: keyof IndexKey]: T }` — checks each named field, applies index signatures for unknown keys.
167
- - `any` / `unknown` — always succeeds.
168
- - Unresolved type aliases or generics return `{ status: 'error', message: '<path>: unresolved type <name>' }`.
169
-
170
- **Edge cases:**
171
- - The `__RUNTIME_UNRESOLVED__::<message>` sentinel from devkit surfaces as `{ status: 'error', message: '<path>: <message>' }` so a missing project type doesn't crash the request.
172
- - Object validation rejects unknown keys when there is no matching index signature.
173
- - Index-signature keys can be `string`, `number`, string-literal unions, or literal types; `number` keys require the key to match `/^-?\d+(\.\d+)?$/`.
174
-
175
- ## Related
176
-
177
- - Function INDEX: `packages/core/CLAUDE.md`
178
- - Architecture: `docs/ARCHITECTURE_SESSION.md`, `docs/ARCHITECTURE_AUTH.md`, `docs/ARCHITECTURE_API.md`
179
- - README: `packages/core/README.md`
180
- - Source: `packages/core/src/sessionTypes.ts`, `validateRequest.ts`, `runtimeTypeValidation.ts`
1
+ # Session Types
2
+
3
+ > Deep specs for the foundational session/auth types plus `validateRequest`, `isFalsy`, and `validateInputByType`. Source: `packages/core/src/sessionTypes.ts`, `validateRequest.ts`, `runtimeTypeValidation.ts`. Bijgewerkt: 2026-05-20.
4
+
5
+ ## Overview
6
+
7
+ `BaseSessionLayout` is the structural contract every project session type extends. It lives in `@luckystack/core` (not `@luckystack/login`) so `validateRequest` can consume it without creating a circular dep: login depends on core for prisma/redis, so the session shape has to live in the lower layer. `@luckystack/login` re-exports the type for ergonomics — existing `import type { BaseSessionLayout } from '@luckystack/login'` paths keep working.
8
+
9
+ `AuthProps` defines the auth gate that every API/sync handler exports (`login: true/false` + an optional `additional[]` predicate list). `validateRequest({ auth, user })` is the runtime check that turns those declarations into a `ValidationResult`.
10
+
11
+ `validateInputByType` is the runtime input validator used by the api/sync handlers in dev. It lazy-loads `@luckystack/devkit` (the heavy TS compiler resolver) only when running in non-production. In production it short-circuits to success because the prod esbuild bundle marks devkit as external and the validator should not need TS to validate request bodies at request time.
12
+
13
+ ## Types
14
+
15
+ ```typescript
16
+ export interface SessionLocation {
17
+ pathName: string;
18
+ searchParams?: Record<string, string>;
19
+ }
20
+
21
+ export interface BaseSessionLayout {
22
+ id: string;
23
+ token: string;
24
+ email?: string | null;
25
+ name?: string | null;
26
+ avatar?: string | null;
27
+ avatarFallback?: string | null;
28
+ admin?: boolean | null;
29
+ language?: string | null;
30
+ location?: SessionLocation;
31
+ roomCodes?: string[];
32
+ csrfToken?: string;
33
+ lastLogin?: Date | string | null;
34
+ previousLogin?: Date | string | null;
35
+ }
36
+
37
+ export interface AuthProps {
38
+ login: boolean;
39
+ additional?: {
40
+ key: keyof BaseSessionLayout;
41
+ value?: unknown;
42
+ type?: 'string' | 'number' | 'boolean';
43
+ nullish?: boolean;
44
+ mustBeFalsy?: boolean;
45
+ }[];
46
+ }
47
+
48
+ export interface ValidationResult {
49
+ status: 'success' | 'error';
50
+ errorCode?: string;
51
+ errorParams?: { key: string; value: string | number | boolean }[];
52
+ httpStatus?: number;
53
+ }
54
+ ```
55
+
56
+ ### Extending `BaseSessionLayout`
57
+
58
+ Projects extend the type in their own `config.ts`:
59
+
60
+ ```typescript
61
+ import type { BaseSessionLayout } from '@luckystack/core';
62
+
63
+ export interface SessionLayout extends BaseSessionLayout {
64
+ organizationId?: string;
65
+ role?: 'member' | 'admin' | 'owner';
66
+ }
67
+ ```
68
+
69
+ The framework's `validateRequest` only inspects keys it knows from `BaseSessionLayout`; project-specific keys are checked via `additional[]`.
70
+
71
+ ## API Reference
72
+
73
+ ### `isFalsy(value: unknown): boolean`
74
+
75
+ **Signature:**
76
+ ```typescript
77
+ export const isFalsy = (value: unknown): boolean
78
+ ```
79
+
80
+ **Behavior:** Returns `true` for `false`, `0`, `0n`, `''`, `null`, `undefined`, or `NaN`. Everything else is truthy.
81
+
82
+ **Note:** This is a stricter "falsy" than JavaScript's coercion-based check because it ALSO treats `NaN` as falsy. Used inside `validateRequest` for the `mustBeFalsy` predicate.
83
+
84
+ ### `validateRequest({ auth, user }): ValidationResult`
85
+
86
+ **Signature:**
87
+ ```typescript
88
+ export const validateRequest = ({
89
+ auth,
90
+ user,
91
+ }: {
92
+ auth: AuthProps;
93
+ user: BaseSessionLayout;
94
+ }): ValidationResult
95
+ ```
96
+
97
+ **Behavior (in order):**
98
+ 1. If `auth.additional` is undefined → returns `{ status: 'success' }` immediately. (The `login` flag is enforced by the surrounding handler, not by this function.)
99
+ 2. For each `condition` in `auth.additional`:
100
+ - If `condition.key` is not in `user` → returns `{ status: 'error', errorCode: 'auth.invalidCondition', errorParams: [{ key: 'key', value: condition.key }], httpStatus: 500 }`. This is a developer setup error, not a runtime auth failure.
101
+ - Reads `val = user[condition.key]`. Computes `isNullish = val === null || val === undefined`.
102
+ - `nullish: true` requires `isNullish`; `nullish: false` requires NOT `isNullish`. Mismatch → forbid.
103
+ - `type: 'string' | 'number' | 'boolean'` requires `typeof val === <type>` (skipped when `val` is nullish). Mismatch → forbid.
104
+ - `'value' in condition` requires strict equality `val === condition.value`. The membership check distinguishes "no exact-value constraint" from `value: undefined` (the latter explicitly demands `val === undefined`).
105
+ - `mustBeFalsy: true` requires `isFalsy(val)`; `mustBeFalsy: false` requires `!isFalsy(val)`. Mismatch → forbid.
106
+ 3. After all conditions pass → returns `{ status: 'success' }`.
107
+
108
+ **Forbid shape:**
109
+ ```typescript
110
+ { status: 'error', errorCode: 'auth.forbidden', errorParams: [{ key: 'key', value: <key> }], httpStatus: 403 }
111
+ ```
112
+
113
+ **Constraint AND-semantics:** Multiple fields on a single `additional` entry are AND'd (every specified field must pass). Entries across the array are also AND'd.
114
+
115
+ **Example — admin-only endpoint:**
116
+ ```typescript
117
+ export const auth: AuthProps = {
118
+ login: true,
119
+ additional: [{ key: 'admin', value: true, type: 'boolean' }],
120
+ };
121
+ ```
122
+
123
+ **Example — verified email required:**
124
+ ```typescript
125
+ export const auth: AuthProps = {
126
+ login: true,
127
+ additional: [{ key: 'email', nullish: false, type: 'string' }],
128
+ };
129
+ ```
130
+
131
+ ### `validateInputByType({ typeText, value, rootKey, filePath? }): Promise<ValidationResult>`
132
+
133
+ **Signature:**
134
+ ```typescript
135
+ type ValidationResult =
136
+ | { status: 'success' }
137
+ | { status: 'error'; message: string };
138
+
139
+ export const validateInputByType = async ({
140
+ typeText,
141
+ value,
142
+ rootKey,
143
+ filePath,
144
+ }: {
145
+ typeText?: string;
146
+ value: unknown;
147
+ rootKey: string;
148
+ filePath?: string;
149
+ }): Promise<ValidationResult>
150
+ ```
151
+
152
+ **Behavior:**
153
+ 1. When `typeText` is empty, whitespace, or literal `'any'` → returns `{ status: 'success' }`.
154
+ 2. When `process.env.NODE_ENV === 'production'` → returns `{ status: 'success' }` (avoids loading the TS compiler in prod).
155
+ 3. Otherwise lazy-imports `@luckystack/devkit` via an indirect module ID (`const devkitModuleId: string = '@luckystack/devkit'; await import(devkitModuleId)`) so `tsc` doesn't try to resolve devkit at build time (devkit depends on core — a literal import would be a build-time circular dep).
156
+ 4. Calls `devkit.resolveRuntimeTypeText({ typeText, filePath })`. On `status: 'error'` returns `{ status: 'error', message: '<rootKey>: <message>' }`.
157
+ 5. Otherwise calls the internal `validateType(resolvedType.typeText, value, rootKey)` recursive validator.
158
+
159
+ **Supported type forms (validateType):**
160
+ - Primitives: `string`, `number`, `boolean`, `true`, `false`, `null`, `undefined`, `Date` (accepts ISO string or `Date` instance).
161
+ - String/number literal types (single-quoted or double-quoted).
162
+ - Unions (`A | B`) — succeeds on first matching branch.
163
+ - Intersections (`A & B`) — requires all branches to succeed.
164
+ - Arrays (`T[]`) — checks every element.
165
+ - `Record<K, V>` — accepts any non-array object.
166
+ - Object literals `{ key?: T; key2: T2; [index: keyof IndexKey]: T }` — checks each named field, applies index signatures for unknown keys.
167
+ - `any` / `unknown` — always succeeds.
168
+ - Unresolved type aliases or generics return `{ status: 'error', message: '<path>: unresolved type <name>' }`.
169
+
170
+ **Edge cases:**
171
+ - The `__RUNTIME_UNRESOLVED__::<message>` sentinel from devkit surfaces as `{ status: 'error', message: '<path>: <message>' }` so a missing project type doesn't crash the request.
172
+ - Object validation rejects unknown keys when there is no matching index signature.
173
+ - Index-signature keys can be `string`, `number`, string-literal unions, or literal types; `number` keys require the key to match `/^-?\d+(\.\d+)?$/`.
174
+
175
+ ## Related
176
+
177
+ - Function INDEX: `packages/core/CLAUDE.md`
178
+ - Architecture: `docs/ARCHITECTURE_SESSION.md`, `docs/ARCHITECTURE_AUTH.md`, `docs/ARCHITECTURE_API.md`
179
+ - README: `packages/core/README.md`
180
+ - Source: `packages/core/src/sessionTypes.ts`, `validateRequest.ts`, `runtimeTypeValidation.ts`