@spfn/core 0.3.0-beta.4 → 0.3.0-beta.6

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.
Files changed (45) hide show
  1. package/README.md +183 -4
  2. package/dist/authz/index.js +1 -381
  3. package/dist/authz/index.js.map +1 -1
  4. package/dist/db/index.d.ts +173 -27
  5. package/dist/db/index.js +192 -57
  6. package/dist/db/index.js.map +1 -1
  7. package/dist/env/loader.js +24 -1
  8. package/dist/env/loader.js.map +1 -1
  9. package/dist/errors/index.js +1 -381
  10. package/dist/errors/index.js.map +1 -1
  11. package/dist/logger/index.js +0 -12
  12. package/dist/logger/index.js.map +1 -1
  13. package/dist/middleware/index.js +6 -387
  14. package/dist/middleware/index.js.map +1 -1
  15. package/dist/nextjs/index.d.ts +18 -1
  16. package/dist/nextjs/index.js +40 -1
  17. package/dist/nextjs/index.js.map +1 -1
  18. package/dist/nextjs/server.d.ts +34 -1
  19. package/dist/nextjs/server.js +14 -0
  20. package/dist/nextjs/server.js.map +1 -1
  21. package/dist/ops/index.d.ts +61 -6
  22. package/dist/ops/index.js +330 -30
  23. package/dist/ops/index.js.map +1 -1
  24. package/dist/server/index.js +24 -1
  25. package/dist/server/index.js.map +1 -1
  26. package/docs/file-upload.md +195 -333
  27. package/package.json +6 -5
  28. package/src/cache/README.md +330 -0
  29. package/src/codegen/README.md +516 -0
  30. package/src/config/README.md +326 -0
  31. package/src/contract/README.md +326 -0
  32. package/src/db/README.md +589 -0
  33. package/src/db/manager/README.md +500 -0
  34. package/src/db/schema/README.md +344 -0
  35. package/src/db/transaction/README.md +822 -0
  36. package/src/env/README.md +651 -0
  37. package/src/errors/README.md +429 -0
  38. package/src/event/README.md +736 -0
  39. package/src/job/README.md +514 -0
  40. package/src/logger/README.md +321 -0
  41. package/src/middleware/README.md +634 -0
  42. package/src/nextjs/README.md +608 -0
  43. package/src/route/README.md +738 -0
  44. package/src/security/README.md +100 -0
  45. package/src/server/README.md +704 -0
@@ -0,0 +1,321 @@
1
+ # @spfn/core/logger — Zero-dependency structured logger (singleton + child loggers)
2
+
3
+ Transport-based logging for Next.js + SPFN server. One process-wide singleton `logger`,
4
+ five levels, automatic sensitive-data masking, and per-module child loggers. No external
5
+ logging library.
6
+
7
+ ## Import paths
8
+
9
+ One entry point. Everything public comes from `@spfn/core/logger`.
10
+
11
+ ```typescript
12
+ import { logger } from '@spfn/core/logger';
13
+ import type { LogLevel, Transport } from '@spfn/core/logger';
14
+ ```
15
+
16
+ There is **no** root `.` export for `@spfn/core`, so `import { logger } from '@spfn/core'`
17
+ does not resolve. `logger`, `Logger`, `LogLevel` and `Transport` all come from the
18
+ `@spfn/core/logger` subpath. (Older snippets showing the root form are stale.)
19
+
20
+ ---
21
+
22
+ ## Public API (complete)
23
+
24
+ From `@spfn/core/logger` (`src/logger/index.ts` is the only export surface):
25
+
26
+ - `logger` — the singleton `Logger` instance. This is what you use 99% of the time.
27
+ - `Logger` — the class. Exported mainly for typing; you do **not** construct it yourself
28
+ (the singleton is created in `factory.ts`). Use `logger` / `logger.child(...)`.
29
+ - Types: `LogLevel`, `Transport`.
30
+
31
+ Instance methods on `logger` / any child logger:
32
+
33
+ - `logger.debug(message, ...)`, `logger.info(...)`, `logger.warn(...)`, `logger.error(...)`,
34
+ `logger.fatal(...)` — the five level methods (overloaded, see Logging below).
35
+ - `logger.child(module: string): Logger` — new logger that tags every line with `[module=...]`.
36
+ - `logger.close(): Promise<void>` — close/flush all transports (graceful shutdown).
37
+ - `logger.level: LogLevel` — getter for the currently active level (read-only).
38
+
39
+ > **No such API — do not use these (they appear in old docs but are not in the code):**
40
+ > - `createLogger(name)` — does **not** exist. Use `logger.child(name)`.
41
+ > - `withLogContext(ctx, fn)` / any AsyncLocalStorage context helper — does **not** exist.
42
+ > Pass context explicitly as the last argument on each call.
43
+ > - File transport, `LOGGER_FILE_ENABLED`, `LOG_DIR` — there is **no** file transport.
44
+ > The only transport is console (stdout/stderr).
45
+ > - `LOG_LEVEL` env var — the level is read from `SPFN_LOG_LEVEL` /
46
+ > `NEXT_PUBLIC_SPFN_LOG_LEVEL`, **not** `LOG_LEVEL`.
47
+ > - Slack / Email / CloudWatch / Sentry transports — none exist and none are "already
48
+ > configured". The factory wires exactly one `ConsoleTransport`.
49
+ >
50
+ > Internal formatter functions (`maskSensitiveData`, `formatConsole`, `formatJSON`,
51
+ > `extractQueryInfo`, `formatUnhandledRejection`, etc.) live in `formatters.ts` but are
52
+ > **not** part of the public export surface — don't import them from `@spfn/core/logger`.
53
+ > In particular `formatJSON` exists but is **not wired into any transport**: production
54
+ > output is plain (uncolored) console text, not JSON (see Output format).
55
+
56
+ ---
57
+
58
+ ## Quick Start
59
+
60
+ ```typescript
61
+ import { logger } from '@spfn/core/logger';
62
+
63
+ logger.debug('Cache miss', { key: 'user:123' });
64
+ logger.info('Server started', { port: 3000 });
65
+ logger.warn('Retry attempt', { attempt: 3 });
66
+ logger.error('Operation failed', error, { userId: 123 }); // error as 2nd arg
67
+ logger.fatal('Database unreachable', error);
68
+ ```
69
+
70
+ No initialization, no `await`, no setup — importing `logger` is enough. The singleton is
71
+ constructed on first import in `factory.ts`.
72
+
73
+ ---
74
+
75
+ ## Logging: message + (error | context) + context
76
+
77
+ Each level method (`debug`/`info`/`warn`/`error`/`fatal`) shares the same overloads.
78
+ The second argument is detected at runtime, so argument **order and type matter**:
79
+
80
+ ```typescript
81
+ // 1. message only
82
+ logger.info('Server started');
83
+
84
+ // 2. message + context object → rendered as [key=value] pairs
85
+ logger.info('Request received', { method: 'POST', path: '/users' });
86
+
87
+ // 3. message + Error → stack trace (and cause chain) printed
88
+ logger.error('Query failed', error);
89
+
90
+ // 4. message + Error + context → both
91
+ logger.error('Query failed', error, { userId: 123, op: 'createUser' });
92
+
93
+ // 5. printf-style — if message contains a %s/%d/%i/%f/%j/%o/%O/%c token,
94
+ // the second arg is substituted via node:util.format (NOT treated as context)
95
+ logger.info('Fetching url: %s', requestUrl);
96
+ ```
97
+
98
+ How the second argument is classified (see `logWithLevel` in `logger.ts`):
99
+ - If `message` contains a printf token (`%s %d %i %f %j %o %O %c %%`), the 2nd arg is
100
+ formatted into the message string — it will **not** appear as context.
101
+ - An `Error` instance → logged as the error (stack + `cause` chain printed).
102
+ - A non-Error object **with** a string `stack` property → treated as an error and wrapped.
103
+ - A plain object **without** a `stack` property → treated as **context**.
104
+ - A `string` / `number` / `boolean` → wrapped into an `Error` (so `{ key: value }`
105
+ context must be an object, not a bare primitive).
106
+
107
+ This `stack`-based heuristic means an object you intend as context will be misread as an
108
+ error if it happens to carry a string `stack` field. Prefer the explicit 3-arg form
109
+ `logger.error(msg, error, context)` when you have both.
110
+
111
+ ---
112
+
113
+ ## Child loggers (per-module)
114
+
115
+ `logger.child(name)` returns a new `Logger` that inherits the level/transports and adds a
116
+ `[module=name]` tag to every line. Cheap to create; create one per module.
117
+
118
+ ```typescript
119
+ const dbLogger = logger.child('database');
120
+ const apiLogger = logger.child('api');
121
+
122
+ dbLogger.info('Connection established');
123
+ // [2026-06-10 10:30:00.123] [pid=12345] [module=database] (INFO): Connection established
124
+ ```
125
+
126
+ Child loggers do **not** stack — calling `.child('b')` on a child created with `.child('a')`
127
+ replaces the module name with `b`, it does not produce `a.b`.
128
+
129
+ ---
130
+
131
+ ## Log levels
132
+
133
+ Five levels, filtered by priority. A log is emitted only when its priority is `>=` the
134
+ configured level (filtering happens *before* metadata is built, so suppressed
135
+ `logger.debug(...)` calls are cheap).
136
+
137
+ | Level | Priority | Stream | Use case |
138
+ |---------|----------|--------|----------|
139
+ | `debug` | 0 | stdout | Development diagnostics |
140
+ | `info` | 1 | stdout | Normal operations (server start, etc.) |
141
+ | `warn` | 2 | stderr | Potential issues, retries |
142
+ | `error` | 3 | stderr | Failures needing attention |
143
+ | `fatal` | 4 | stderr | Critical / shutdown-level errors |
144
+
145
+ `warn`, `error`, `fatal` go to **stderr**; `debug`, `info` go to **stdout** (via
146
+ `console.error` / `console.log`).
147
+
148
+ `LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'fatal'`.
149
+
150
+ ---
151
+
152
+ ## Configuration (environment variables)
153
+
154
+ The level is resolved once, at module load, in `factory.ts`:
155
+
156
+ | Variable | Effect | Default |
157
+ |----------|--------|---------|
158
+ | `SPFN_LOG_LEVEL` | Minimum level. First choice. | `info` |
159
+ | `NEXT_PUBLIC_SPFN_LOG_LEVEL` | Fallback level (client-visible in Next.js). Used only if `SPFN_LOG_LEVEL` is unset. | `info` |
160
+ | `NODE_ENV` | `production` → colorized output **off**; anything else → colors **on**. | (warns if unset) |
161
+
162
+ ```bash
163
+ SPFN_LOG_LEVEL=debug # show everything
164
+ NODE_ENV=production # disable ANSI colors (plain text for log collectors)
165
+ ```
166
+
167
+ - An invalid `SPFN_LOG_LEVEL` value falls back to `info` with a stderr warning.
168
+ - Because the level is read **once at import time**, changing `process.env.SPFN_LOG_LEVEL`
169
+ at runtime has **no effect** — set it before the process starts.
170
+ - The console transport itself is created with level `debug` and is always enabled; the
171
+ effective floor is the logger-level computed from `SPFN_LOG_LEVEL`.
172
+
173
+ ---
174
+
175
+ ## Sensitive data masking
176
+
177
+ Context objects are recursively scanned and any key whose lowercased name *contains* a
178
+ sensitive token is replaced with the literal string `***MASKED***` before output. The
179
+ original value is never written to any transport.
180
+
181
+ ```typescript
182
+ logger.info('Login attempt', { username: 'john', password: pw, token: t });
183
+ // [username=john] [password=***MASKED***] [token=***MASKED***] (INFO): Login attempt
184
+ ```
185
+
186
+ Masking applies to nested objects and arrays, and circular references render as
187
+ `[Circular]`. Matching is substring + case-insensitive, so `userPassword`, `X-Api-Key`,
188
+ `refresh_token`, etc. all match.
189
+
190
+ Masked key tokens (a key matches if it contains any of these):
191
+ `password`, `passwd`, `pwd`, `secret`, `token`, `apikey`, `api_key`, `accesstoken`,
192
+ `access_token`, `refreshtoken`, `refresh_token`, `authorization`, `auth`, `cookie`,
193
+ `session`, `sessionid`, `session_id`, `privatekey`, `private_key`, `creditcard`,
194
+ `credit_card`, `cardnumber`, `card_number`, `cvv`, `ssn`, `pin`.
195
+
196
+ > Masking only applies to the **context** object. It does **not** scan the `message`
197
+ > string. Never interpolate a secret into the message — `logger.info(\`token: ${t}\`)`
198
+ > is logged verbatim, unmasked. Put potentially-sensitive data in the context object so
199
+ > the masker can catch it.
200
+
201
+ ---
202
+
203
+ ## Output format
204
+
205
+ There is exactly one transport (`ConsoleTransport`) and it always renders the
206
+ human-readable console format — colorized when `NODE_ENV !== 'production'`, plain text
207
+ otherwise. Layout:
208
+
209
+ ```
210
+ [timestamp] [pid=N] [module=name] [key=value]... (LEVEL): message
211
+ ```
212
+
213
+ ```
214
+ [2026-06-10 10:30:00.123] [pid=12345] [module=database] (INFO): Connection established
215
+ [2026-06-10 10:30:01.456] [pid=12345] [module=api] [userId=123] (ERROR): Request failed
216
+ Error: Connection timeout
217
+ at processRequest (/app/src/api.ts:45:11)
218
+ ```
219
+
220
+ Errors are printed on their own line after the message, including the `Caused by:` cause
221
+ chain. In containers (Docker/K8s), set `NODE_ENV=production` so output is plain text on
222
+ stdout/stderr, which the platform collects automatically.
223
+
224
+ ---
225
+
226
+ ## Pitfalls & anti-patterns
227
+
228
+ - **`createLogger` / `withLogContext` don't exist.** Use `logger.child(name)` for scoping
229
+ and pass context explicitly per call. There is no ambient/async context.
230
+ - **No JSON output.** `formatJSON` exists in the source but is unused; the only transport
231
+ emits console text. "Production = JSON logs" (from old docs) is false — production just
232
+ turns colors off.
233
+ - **Wrong env var.** The level comes from `SPFN_LOG_LEVEL` (or
234
+ `NEXT_PUBLIC_SPFN_LOG_LEVEL`), **not** `LOG_LEVEL`.
235
+ - **Level is frozen at import.** Mutating `process.env.SPFN_LOG_LEVEL` after startup does
236
+ nothing. Set it in the environment before launching.
237
+ - **Masked value is `***MASKED***`, not `***`.** Don't assert on `***` in tests/parsers.
238
+ - **Secrets in the message are NOT masked.** Only the context object is scanned. Keep
239
+ sensitive values out of the message string; put them in context.
240
+ - **Context vs error ambiguity.** A plain object with a string `stack` property is treated
241
+ as an error, not context. Use the 3-arg form `logger.error(msg, error, context)` when
242
+ you have both, and don't put a `stack` key on a context object.
243
+ - **Bare primitives as 2nd arg become errors.** `logger.info('x', 42)` logs `42` as an
244
+ Error (or printf-substitutes it if the message has a `%` token) — it is not context.
245
+ Context must be an object: `logger.info('x', { n: 42 })`.
246
+ - **Don't import internals.** `maskSensitiveData`, `formatConsole`, `formatJSON`,
247
+ `extractQueryInfo`, `formatUnhandledRejection`, `Logger` construction, etc. are not the
248
+ public surface. Use the `logger` singleton + `child`.
249
+ - **No file transport.** `LOGGER_FILE_ENABLED` / `LOG_DIR` do nothing. Route stdout/stderr
250
+ to your log collector (Loki/CloudWatch/ELK) instead.
251
+
252
+ ---
253
+
254
+ ## Complete example
255
+
256
+ ```typescript
257
+ import { logger } from '@spfn/core/logger';
258
+
259
+ const dbLogger = logger.child('database');
260
+
261
+ async function createUser(input: { email: string; password: string }): Promise<void>
262
+ {
263
+ dbLogger.debug('createUser called', { email: input.email });
264
+ // 'password' in context would be auto-masked if included.
265
+
266
+ try
267
+ {
268
+ await db.insert(/* ... */);
269
+ dbLogger.info('User created', { email: input.email });
270
+ }
271
+ catch (error)
272
+ {
273
+ // 3-arg form: message + Error (stack + cause) + extra context
274
+ dbLogger.error('User insert failed', error as Error, { email: input.email });
275
+ throw error;
276
+ }
277
+ }
278
+
279
+ // Graceful shutdown: flush transports
280
+ process.on('SIGTERM', async () =>
281
+ {
282
+ logger.info('Shutting down');
283
+ await logger.close();
284
+ process.exit(0);
285
+ });
286
+ ```
287
+
288
+ ---
289
+
290
+ ## Types reference
291
+
292
+ ```typescript
293
+ type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'fatal';
294
+
295
+ interface Transport
296
+ {
297
+ name: string;
298
+ level: LogLevel;
299
+ enabled: boolean;
300
+ log(metadata: LogMetadata): Promise<void>;
301
+ close?(): Promise<void>;
302
+ }
303
+ ```
304
+
305
+ `LogMetadata` (the object passed to a `Transport.log`; not exported, shown for
306
+ implementers) carries `timestamp: Date`, `level`, `message`, optional `module`,
307
+ optional `error: Error`, and optional `context` (already masked).
308
+
309
+ `logger.level` is a `LogLevel` getter (read-only).
310
+
311
+ The `Transport` interface is public so you *could* implement a custom transport, but the
312
+ factory does not expose a registration hook — there is currently no supported way to add a
313
+ transport to the singleton without editing `factory.ts`.
314
+
315
+ ---
316
+
317
+ ## Related
318
+
319
+ - `@spfn/core` — package root (re-exports `logger`).
320
+ - `src/logger/factory.ts` — where the singleton + console transport are wired and the
321
+ level is resolved from env.