@pikku/core 0.12.14 → 0.12.15

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 (123) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/dist/errors/errors.d.ts +4 -0
  3. package/dist/errors/errors.js +12 -0
  4. package/dist/function/function-runner.js +2 -1
  5. package/dist/function/functions.types.js +1 -0
  6. package/dist/function/index.d.ts +2 -1
  7. package/dist/function/index.js +1 -0
  8. package/dist/handle-error.d.ts +2 -2
  9. package/dist/handle-error.js +8 -8
  10. package/dist/index.d.ts +1 -2
  11. package/dist/index.js +1 -2
  12. package/dist/middleware/index.d.ts +2 -0
  13. package/dist/middleware/index.js +2 -0
  14. package/dist/middleware/remote-auth.js +5 -2
  15. package/dist/permissions.d.ts +13 -1
  16. package/dist/permissions.js +51 -0
  17. package/dist/pikku-state.d.ts +0 -1
  18. package/dist/pikku-state.js +1 -4
  19. package/dist/remote.d.ts +10 -0
  20. package/dist/remote.js +32 -0
  21. package/dist/schema.js +2 -0
  22. package/dist/services/deployment-service.d.ts +12 -1
  23. package/dist/services/in-memory-queue-service.d.ts +7 -0
  24. package/dist/services/in-memory-queue-service.js +28 -0
  25. package/dist/services/index.d.ts +4 -1
  26. package/dist/services/index.js +2 -1
  27. package/dist/services/logger-console.d.ts +26 -40
  28. package/dist/services/logger-console.js +102 -46
  29. package/dist/services/logger.d.ts +5 -0
  30. package/dist/services/meta-service.d.ts +175 -0
  31. package/dist/services/meta-service.js +310 -0
  32. package/dist/services/workflow-service.d.ts +6 -1
  33. package/dist/testing/service-tests.js +0 -8
  34. package/dist/types/core.types.d.ts +8 -0
  35. package/dist/types/state.types.d.ts +2 -2
  36. package/dist/wirings/ai-agent/ai-agent-prepare.js +42 -21
  37. package/dist/wirings/ai-agent/ai-agent-registry.js +2 -2
  38. package/dist/wirings/ai-agent/ai-agent-runner.js +18 -1
  39. package/dist/wirings/ai-agent/ai-agent-stream.js +1 -0
  40. package/dist/wirings/ai-agent/ai-agent.types.d.ts +5 -3
  41. package/dist/wirings/channel/channel-runner.js +3 -3
  42. package/dist/wirings/cli/cli-runner.js +3 -2
  43. package/dist/wirings/cli/command-parser.js +7 -3
  44. package/dist/wirings/http/http-runner.d.ts +1 -1
  45. package/dist/wirings/http/http-runner.js +23 -11
  46. package/dist/wirings/http/http.types.d.ts +2 -0
  47. package/dist/wirings/mcp/mcp-runner.js +5 -3
  48. package/dist/wirings/queue/queue-runner.d.ts +3 -1
  49. package/dist/wirings/queue/queue-runner.js +7 -3
  50. package/dist/wirings/rpc/rpc-runner.js +56 -90
  51. package/dist/wirings/scheduler/scheduler-runner.d.ts +3 -1
  52. package/dist/wirings/scheduler/scheduler-runner.js +9 -5
  53. package/dist/wirings/trigger/trigger-runner.js +4 -2
  54. package/dist/wirings/workflow/dsl/workflow-runner.d.ts +1 -1
  55. package/dist/wirings/workflow/dsl/workflow-runner.js +6 -9
  56. package/dist/wirings/workflow/graph/graph-runner.d.ts +1 -1
  57. package/dist/wirings/workflow/graph/graph-runner.js +18 -4
  58. package/dist/wirings/workflow/index.d.ts +4 -2
  59. package/dist/wirings/workflow/index.js +4 -2
  60. package/dist/wirings/workflow/pikku-workflow-service.d.ts +16 -4
  61. package/dist/wirings/workflow/pikku-workflow-service.js +216 -24
  62. package/dist/wirings/workflow/workflow-queue-workers.d.ts +40 -0
  63. package/dist/wirings/workflow/workflow-queue-workers.js +41 -0
  64. package/dist/wirings/workflow/workflow.types.d.ts +17 -1
  65. package/package.json +4 -1
  66. package/src/errors/errors.ts +12 -0
  67. package/src/function/function-runner.ts +5 -4
  68. package/src/function/functions.types.ts +1 -0
  69. package/src/function/index.ts +10 -0
  70. package/src/handle-error.test.ts +2 -2
  71. package/src/handle-error.ts +8 -8
  72. package/src/index.ts +1 -2
  73. package/src/middleware/index.ts +2 -0
  74. package/src/middleware/remote-auth.test.ts +4 -4
  75. package/src/middleware/remote-auth.ts +7 -2
  76. package/src/permissions.ts +70 -0
  77. package/src/pikku-state.test.ts +0 -26
  78. package/src/pikku-state.ts +1 -5
  79. package/src/remote.ts +46 -0
  80. package/src/schema.ts +1 -0
  81. package/src/services/deployment-service.ts +17 -1
  82. package/src/services/in-memory-queue-service.ts +46 -0
  83. package/src/services/index.ts +21 -1
  84. package/src/services/logger-console.ts +127 -47
  85. package/src/services/logger.ts +6 -0
  86. package/src/services/meta-service.ts +523 -0
  87. package/src/services/workflow-service.ts +8 -0
  88. package/src/testing/service-tests.ts +0 -10
  89. package/src/types/core.types.ts +8 -0
  90. package/src/types/state.types.ts +2 -2
  91. package/src/wirings/ai-agent/ai-agent-prepare.ts +51 -40
  92. package/src/wirings/ai-agent/ai-agent-registry.ts +3 -3
  93. package/src/wirings/ai-agent/ai-agent-runner.ts +16 -1
  94. package/src/wirings/ai-agent/ai-agent-stream.ts +8 -0
  95. package/src/wirings/ai-agent/ai-agent.types.ts +5 -3
  96. package/src/wirings/channel/channel-runner.ts +4 -5
  97. package/src/wirings/cli/cli-runner.test.ts +8 -7
  98. package/src/wirings/cli/cli-runner.ts +4 -3
  99. package/src/wirings/cli/command-parser.ts +8 -3
  100. package/src/wirings/http/http-runner.ts +27 -11
  101. package/src/wirings/http/http.types.ts +2 -0
  102. package/src/wirings/mcp/mcp-runner.ts +7 -9
  103. package/src/wirings/queue/queue-runner.test.ts +5 -11
  104. package/src/wirings/queue/queue-runner.ts +11 -3
  105. package/src/wirings/rpc/rpc-runner.ts +82 -115
  106. package/src/wirings/scheduler/scheduler-runner.test.ts +5 -11
  107. package/src/wirings/scheduler/scheduler-runner.ts +13 -5
  108. package/src/wirings/trigger/trigger-runner.test.ts +10 -11
  109. package/src/wirings/trigger/trigger-runner.ts +6 -4
  110. package/src/wirings/workflow/dsl/workflow-runner.ts +11 -10
  111. package/src/wirings/workflow/graph/graph-runner.ts +19 -4
  112. package/src/wirings/workflow/index.ts +17 -6
  113. package/src/wirings/workflow/pikku-workflow-service.ts +284 -24
  114. package/src/wirings/workflow/workflow-queue-workers.ts +85 -0
  115. package/src/wirings/workflow/workflow.types.ts +16 -1
  116. package/tsconfig.tsbuildinfo +1 -1
  117. package/dist/wirings/ai-agent/agent-dynamic-workflow.d.ts +0 -6
  118. package/dist/wirings/ai-agent/agent-dynamic-workflow.js +0 -468
  119. package/dist/wirings/workflow/workflow-helpers.d.ts +0 -36
  120. package/dist/wirings/workflow/workflow-helpers.js +0 -38
  121. package/src/wirings/ai-agent/agent-dynamic-workflow.ts +0 -610
  122. package/src/wirings/workflow/workflow-helpers.test.ts +0 -129
  123. package/src/wirings/workflow/workflow-helpers.ts +0 -79
@@ -252,7 +252,7 @@ describe('pikkuRemoteAuthMiddleware', () => {
252
252
  http: {
253
253
  request: createMockRequest(
254
254
  { authorization: 'Bearer valid-token' },
255
- '/rpc/otherFunc'
255
+ '/remote/rpc/otherFunc'
256
256
  ),
257
257
  },
258
258
  } as any,
@@ -273,7 +273,7 @@ describe('pikkuRemoteAuthMiddleware', () => {
273
273
  http: {
274
274
  request: createMockRequest(
275
275
  { authorization: 'Bearer valid-token' },
276
- '/rpc/myFunc'
276
+ '/remote/rpc/myFunc'
277
277
  ),
278
278
  },
279
279
  } as any,
@@ -295,7 +295,7 @@ describe('pikkuRemoteAuthMiddleware', () => {
295
295
  http: {
296
296
  request: createMockRequest(
297
297
  { authorization: 'Bearer valid-token' },
298
- '/rpc/anyFunc'
298
+ '/remote/rpc/anyFunc'
299
299
  ),
300
300
  },
301
301
  } as any,
@@ -339,7 +339,7 @@ describe('pikkuRemoteAuthMiddleware', () => {
339
339
  http: {
340
340
  request: createMockRequest(
341
341
  { authorization: 'Bearer valid-token' },
342
- '/rpc/my%20func'
342
+ '/remote/rpc/my%20func'
343
343
  ),
344
344
  },
345
345
  } as any,
@@ -12,6 +12,9 @@ export const pikkuRemoteAuthMiddleware = pikkuMiddleware(
12
12
  try {
13
13
  secret = await secrets.getSecret('PIKKU_REMOTE_SECRET')
14
14
  } catch {
15
+ if (http.request.path().startsWith('/remote/rpc/')) {
16
+ throw new UnauthorizedError()
17
+ }
15
18
  return next()
16
19
  }
17
20
  if (!jwt) {
@@ -42,8 +45,10 @@ export const pikkuRemoteAuthMiddleware = pikkuMiddleware(
42
45
  throw new UnauthorizedError()
43
46
  }
44
47
 
45
- if (payload?.fn && http.request.path().startsWith('/rpc/')) {
46
- const fn = decodeURIComponent(http.request.path().slice('/rpc/'.length))
48
+ if (payload?.fn && http.request.path().startsWith('/remote/rpc/')) {
49
+ const fn = decodeURIComponent(
50
+ http.request.path().slice('/remote/rpc/'.length)
51
+ )
47
52
  if (fn && payload.fn !== fn) {
48
53
  throw new UnauthorizedError()
49
54
  }
@@ -1,5 +1,6 @@
1
1
  import type {
2
2
  CoreServices,
3
+ CoreUserSession,
3
4
  PikkuWiringTypes,
4
5
  PermissionMetadata,
5
6
  PikkuWire,
@@ -325,3 +326,72 @@ export const runPermissions = async (
325
326
  throw new ForbiddenError('Permission denied')
326
327
  }
327
328
  }
329
+
330
+ /**
331
+ * Checks whether a session passes the auth checks (pikkuAuth only) for a
332
+ * given function/agent. Skips pikkuPermission checks since those require
333
+ * request data which isn't available at filter time.
334
+ *
335
+ * @param funcPermissions - The PermissionMetadata[] from function or agent metadata
336
+ * @param session - The user's session
337
+ * @param services - Singleton services
338
+ * @param packageName - Optional package namespace
339
+ * @returns true if the session passes all auth checks (or no auth checks exist)
340
+ */
341
+ export const checkAuthPermissions = async (
342
+ funcPermissions: PermissionMetadata[] | undefined,
343
+ session: CoreUserSession,
344
+ services: CoreServices,
345
+ packageName: string | null = null
346
+ ): Promise<boolean> => {
347
+ if (!funcPermissions?.length) return true
348
+
349
+ const allPermissions = combinePermissions(
350
+ 'agent',
351
+ `authcheck:${Math.random()}`,
352
+ {
353
+ funcInheritedPermissions: funcPermissions,
354
+ packageName,
355
+ }
356
+ )
357
+
358
+ if (allPermissions.length === 0) return true
359
+
360
+ const wire = { session } as unknown as PikkuWire<
361
+ any,
362
+ never,
363
+ any,
364
+ never,
365
+ never,
366
+ never
367
+ >
368
+
369
+ // Extract only pikkuAuth permissions (marked with __pikkuAuth)
370
+ const authPerms: CorePikkuPermission[] = []
371
+ for (const permission of allPermissions) {
372
+ if (typeof permission === 'function') {
373
+ if ((permission as any).__pikkuAuth) {
374
+ authPerms.push(permission)
375
+ }
376
+ } else if (permission && typeof permission === 'object') {
377
+ for (const funcs of Object.values(permission)) {
378
+ const arr = Array.isArray(funcs) ? funcs : [funcs]
379
+ for (const fn of arr) {
380
+ if (typeof fn === 'function' && (fn as any).__pikkuAuth) {
381
+ authPerms.push(fn)
382
+ }
383
+ }
384
+ }
385
+ }
386
+ }
387
+
388
+ // No auth permissions = allowed (only data-dependent permissions exist)
389
+ if (authPerms.length === 0) return true
390
+
391
+ // All auth permissions must pass
392
+ for (const perm of authPerms) {
393
+ const result = await perm(services, null, wire)
394
+ if (result) return true
395
+ }
396
+ return false
397
+ }
@@ -7,7 +7,6 @@ import {
7
7
  getAllPackageStates,
8
8
  getSingletonServices,
9
9
  getCreateWireServices,
10
- getPikkuMetaDir,
11
10
  addPackageServiceFactories,
12
11
  } from './pikku-state.js'
13
12
 
@@ -176,31 +175,6 @@ describe('getCreateWireServices', () => {
176
175
  })
177
176
  })
178
177
 
179
- describe('getPikkuMetaDir', () => {
180
- test('should return null by default', () => {
181
- const result = getPikkuMetaDir()
182
- assert.strictEqual(result, null)
183
- })
184
-
185
- test('should return metaDir when set', () => {
186
- pikkuState(null, 'package', 'metaDir', '/some/path' as any)
187
- const result = getPikkuMetaDir()
188
- assert.strictEqual(result, '/some/path')
189
- })
190
-
191
- test('should return metaDir for specific package', () => {
192
- pikkuState('@test/pkg', 'package', 'metaDir', '/addon/path' as any)
193
- const result = getPikkuMetaDir('@test/pkg')
194
- assert.strictEqual(result, '/addon/path')
195
- })
196
-
197
- test('should treat null packageName as main', () => {
198
- pikkuState(null, 'package', 'metaDir', '/main/path' as any)
199
- const result = getPikkuMetaDir(null)
200
- assert.strictEqual(result, '/main/path')
201
- })
202
- })
203
-
204
178
  describe('addPackageServiceFactories', () => {
205
179
  test('should register factories for addon package', () => {
206
180
  const mockFactories = {
@@ -156,8 +156,8 @@ const createEmptyPackageState = (): PikkuPackageState => ({
156
156
  package: {
157
157
  factories: null,
158
158
  singletonServices: null,
159
- metaDir: null,
160
159
  credentialsMeta: null,
160
+ requiredParentServices: null,
161
161
  },
162
162
  })
163
163
 
@@ -188,10 +188,6 @@ if (!getAllPackageStates().has('__main__')) {
188
188
  resetPikkuState()
189
189
  }
190
190
 
191
- export const getPikkuMetaDir = (packageName?: string | null): string | null => {
192
- return pikkuState(packageName ?? null, 'package', 'metaDir')
193
- }
194
-
195
191
  export const getSingletonServices = (): CoreSingletonServices => {
196
192
  const services = pikkuState(null, 'package', 'singletonServices')
197
193
  if (!services) throw new Error('Singleton services not initialized')
package/src/remote.ts ADDED
@@ -0,0 +1,46 @@
1
+ import type { JWTService } from './services/jwt-service.js'
2
+ import type { SecretService } from './services/secret-service.js'
3
+ import { encryptJSON } from './crypto-utils.js'
4
+
5
+ /**
6
+ * Build Authorization headers with JWT-signed session and traceId for
7
+ * pikkuRemoteAuthMiddleware on the receiving end.
8
+ *
9
+ * Used by all deployment services (Kysely, Redis, MongoDB, Lambda, CF, Azure)
10
+ * regardless of transport (HTTP, Lambda Invoke, service bindings).
11
+ */
12
+ export async function buildRemoteHeaders(
13
+ jwt: JWTService | undefined,
14
+ secrets: SecretService | undefined,
15
+ funcName: string,
16
+ session?: unknown,
17
+ traceId?: string
18
+ ): Promise<Record<string, string>> {
19
+ const headers: Record<string, string> = {
20
+ 'content-type': 'application/json',
21
+ ...(traceId && { 'x-request-id': traceId }),
22
+ }
23
+
24
+ let secret: string | undefined
25
+ try {
26
+ secret = await secrets?.getSecret('PIKKU_REMOTE_SECRET')
27
+ } catch {}
28
+
29
+ if (secret && jwt) {
30
+ const sessionEnc = session
31
+ ? await encryptJSON(secret, { session })
32
+ : undefined
33
+ const token = await jwt.encode(
34
+ { value: 5, unit: 'minute' },
35
+ {
36
+ aud: 'pikku-remote',
37
+ fn: funcName,
38
+ iat: Math.floor(Date.now() / 1000),
39
+ session: sessionEnc,
40
+ }
41
+ )
42
+ headers.authorization = `Bearer ${token}`
43
+ }
44
+
45
+ return headers
46
+ }
package/src/schema.ts CHANGED
@@ -100,6 +100,7 @@ export const coerceTopLevelDataFromSchema = (
100
100
  packageName: string | null = null
101
101
  ) => {
102
102
  const schema = pikkuState(packageName, 'misc', 'schemas').get(schemaName)
103
+ if (!schema?.properties) return
103
104
  for (const key in schema.properties) {
104
105
  const property = schema.properties[key]
105
106
  if (typeof property === 'boolean') {
@@ -18,5 +18,21 @@ export interface DeploymentService {
18
18
  init(): Promise<void>
19
19
  start(config: DeploymentConfig): Promise<void>
20
20
  stop(): Promise<void>
21
- findFunction(name: string): Promise<DeploymentInfo[]>
21
+ /**
22
+ * Dispatch a remote RPC call to a function.
23
+ * The deployment service owns the full transport:
24
+ * - Resolving the target (endpoint, service binding, etc.)
25
+ * - Session propagation (JWT signing, headers)
26
+ * - The actual network call
27
+ *
28
+ * @param funcName - The function to invoke
29
+ * @param data - Input data for the function
30
+ * @param session - User session to propagate (optional)
31
+ */
32
+ invoke(
33
+ funcName: string,
34
+ data: unknown,
35
+ session?: unknown,
36
+ traceId?: string
37
+ ): Promise<unknown>
22
38
  }
@@ -0,0 +1,46 @@
1
+ import type {
2
+ QueueService,
3
+ QueueJob,
4
+ JobOptions,
5
+ } from '../wirings/queue/queue.types.js'
6
+ import { runQueueJob } from '../wirings/queue/queue-runner.js'
7
+
8
+ export class InMemoryQueueService implements QueueService {
9
+ readonly supportsResults = false
10
+ private jobCounter = 0
11
+
12
+ async add<T>(
13
+ queueName: string,
14
+ data: T,
15
+ options?: JobOptions
16
+ ): Promise<string> {
17
+ const jobId = `inmem-${++this.jobCounter}`
18
+
19
+ const job: QueueJob<T> = {
20
+ id: jobId,
21
+ queueName,
22
+ data,
23
+ status: () => 'active',
24
+ pikkuUserId: options?.pikkuUserId,
25
+ }
26
+
27
+ const delay = options?.delay ?? 0
28
+
29
+ setTimeout(async () => {
30
+ try {
31
+ await runQueueJob({ job })
32
+ } catch (e: any) {
33
+ console.error(
34
+ `[InMemoryQueue] Job ${jobId} on ${queueName} failed:`,
35
+ e.message
36
+ )
37
+ }
38
+ }, delay)
39
+
40
+ return jobId
41
+ }
42
+
43
+ async getJob(): Promise<null> {
44
+ return null
45
+ }
46
+ }
@@ -19,8 +19,9 @@ export { TypedVariablesService } from './typed-variables-service.js'
19
19
  export { LocalSecretService } from './local-secrets.js'
20
20
  export { LocalCredentialService } from './local-credential-service.js'
21
21
  export { LocalVariablesService } from './local-variables.js'
22
- export { ConsoleLogger } from './logger-console.js'
22
+ export { ConsoleLogger, JsonConsoleLogger } from './logger-console.js'
23
23
  export { InMemoryWorkflowService } from './in-memory-workflow-service.js'
24
+ export { InMemoryQueueService } from './in-memory-queue-service.js'
24
25
  export { InMemoryTriggerService } from './in-memory-trigger-service.js'
25
26
  export { InMemoryAIRunStateService } from './in-memory-ai-run-state-service.js'
26
27
  export { LocalGatewayService } from './local-gateway-service.js'
@@ -66,3 +67,22 @@ export type {
66
67
  CredentialMetaInfo,
67
68
  } from './typed-credential-service.js'
68
69
  export type { VariableStatus, VariableMeta } from './typed-variables-service.js'
70
+ export type { MetaService } from './meta-service.js'
71
+ export type {
72
+ MCPMeta,
73
+ RPCMetaRecord,
74
+ ServiceMeta,
75
+ ServicesMetaRecord,
76
+ MiddlewareDefinitionMeta,
77
+ MiddlewareInstanceMeta,
78
+ GroupMeta,
79
+ MiddlewareGroupsMeta,
80
+ PermissionDefinitionMeta,
81
+ PermissionsGroupsMeta,
82
+ FunctionsMeta,
83
+ FunctionMeta,
84
+ MiddlewareMeta,
85
+ PermissionMeta,
86
+ AgentsMeta,
87
+ AgentMeta,
88
+ } from './meta-service.js'
@@ -2,97 +2,177 @@ import type { Logger } from './logger.js'
2
2
  import { LogLevel } from './logger.js'
3
3
 
4
4
  /**
5
- * A logger implementation that logs messages to the console.
5
+ * Text-mode console logger.
6
+ * Output: `INFO: message` or `[traceId] INFO: message`
6
7
  */
7
8
  export class ConsoleLogger implements Logger {
8
- /**
9
- * The current logging level.
10
- */
11
9
  private level: LogLevel = LogLevel.info
10
+ private prefix: string
11
+
12
+ constructor(traceId?: string) {
13
+ this.prefix = traceId ? `[${traceId}]` : ''
14
+ }
12
15
 
13
- /**
14
- * Sets the logging level.
15
- * @param level - The logging level to set.
16
- */
17
16
  setLevel(level: LogLevel): void {
18
17
  this.level = level
19
18
  }
20
19
 
21
- /**
22
- * Logs a trace message.
23
- * @param message - The message to log.
24
- * @param meta - Additional metadata to log.
25
- */
26
- trace?(message: string, ...meta: any[]): void {
20
+ scope(traceId: string): Logger {
21
+ const scoped = new ConsoleLogger(traceId)
22
+ scoped.level = this.level
23
+ return scoped
24
+ }
25
+
26
+ private log_(
27
+ fn: (...args: unknown[]) => void,
28
+ label: string,
29
+ ...args: unknown[]
30
+ ): void {
31
+ if (this.prefix) {
32
+ fn(this.prefix, label, ...args)
33
+ } else {
34
+ fn(label, ...args)
35
+ }
36
+ }
37
+
38
+ trace(message: string, ...meta: any[]): void {
27
39
  if (this.level <= LogLevel.trace) {
28
- console.trace('TRACE:', message, ...meta)
40
+ this.log_(console.trace, 'TRACE:', message, ...meta)
29
41
  }
30
42
  }
31
43
 
32
- /**
33
- * Logs a debug message.
34
- * @param message - The message to log.
35
- * @param meta - Additional metadata to log.
36
- */
37
44
  debug(message: string, ...meta: any[]): void {
38
45
  if (this.level <= LogLevel.debug) {
39
- console.debug('DEBUG:', message, ...meta)
46
+ this.log_(console.debug, 'DEBUG:', message, ...meta)
40
47
  }
41
48
  }
42
49
 
43
- /**
44
- * Logs an informational message.
45
- * @param messageOrObj - The message or object to log.
46
- * @param meta - Additional metadata to log.
47
- */
48
50
  info(messageOrObj: string | Record<string, any>, ...meta: any[]): void {
49
51
  if (this.level <= LogLevel.info) {
50
- console.info('INFO:', messageOrObj, ...meta)
52
+ this.log_(console.info, 'INFO:', messageOrObj, ...meta)
51
53
  }
52
54
  }
53
55
 
54
- /**
55
- * Logs a warning message.
56
- * @param messageOrObj - The message or object to log.
57
- * @param meta - Additional metadata to log.
58
- */
59
56
  warn(messageOrObj: string | Record<string, any>, ...meta: any[]): void {
60
57
  if (this.level <= LogLevel.warn) {
61
- console.warn('WARN:', messageOrObj, ...meta)
58
+ this.log_(console.warn, 'WARN:', messageOrObj, ...meta)
62
59
  }
63
60
  }
64
61
 
65
- /**
66
- * Logs an error message.
67
- * @param messageOrObj - The message, object, or error to log.
68
- * @param meta - Additional metadata to log.
69
- */
70
62
  error(
71
63
  messageOrObj: string | Record<string, any> | Error,
72
64
  ...meta: any[]
73
65
  ): void {
74
66
  if (this.level <= LogLevel.error) {
75
- console.error(
67
+ this.log_(
68
+ console.error,
76
69
  'ERROR:',
77
70
  messageOrObj instanceof Error ? messageOrObj.message : messageOrObj,
78
71
  ...meta
79
72
  )
80
73
  if (messageOrObj instanceof Error) {
81
- console.error('STACK:', messageOrObj.stack)
74
+ this.log_(console.error, 'STACK:', messageOrObj.stack)
75
+ }
76
+ }
77
+ }
78
+
79
+ log(level: string, message: string, ...meta: any[]): void {
80
+ const logLevel = LogLevel[level as keyof typeof LogLevel]
81
+ if (this.level <= logLevel) {
82
+ this.log_(console.log, `${level.toUpperCase()}:`, message, ...meta)
83
+ }
84
+ }
85
+ }
86
+
87
+ /**
88
+ * JSON-mode console logger.
89
+ * Output: `{"level":"info","message":"...","traceId":"..."}`
90
+ * CF Workers Logs auto-indexes JSON keys for filtering.
91
+ */
92
+ export class JsonConsoleLogger implements Logger {
93
+ private level: LogLevel = LogLevel.info
94
+ private traceId: string | undefined
95
+
96
+ constructor(traceId?: string) {
97
+ this.traceId = traceId
98
+ }
99
+
100
+ setLevel(level: LogLevel): void {
101
+ this.level = level
102
+ }
103
+
104
+ scope(traceId: string): Logger {
105
+ const scoped = new JsonConsoleLogger(traceId)
106
+ scoped.level = this.level
107
+ return scoped
108
+ }
109
+
110
+ trace(message: string, ...meta: any[]): void {
111
+ if (this.level <= LogLevel.trace) {
112
+ this.emit(console.debug, 'trace', message, meta)
113
+ }
114
+ }
115
+
116
+ debug(message: string, ...meta: any[]): void {
117
+ if (this.level <= LogLevel.debug) {
118
+ this.emit(console.debug, 'debug', message, meta)
119
+ }
120
+ }
121
+
122
+ info(messageOrObj: string | Record<string, any>, ...meta: any[]): void {
123
+ if (this.level <= LogLevel.info) {
124
+ this.emit(console.info, 'info', messageOrObj, meta)
125
+ }
126
+ }
127
+
128
+ warn(messageOrObj: string | Record<string, any>, ...meta: any[]): void {
129
+ if (this.level <= LogLevel.warn) {
130
+ this.emit(console.warn, 'warn', messageOrObj, meta)
131
+ }
132
+ }
133
+
134
+ error(
135
+ messageOrObj: string | Record<string, any> | Error,
136
+ ...meta: any[]
137
+ ): void {
138
+ if (this.level <= LogLevel.error) {
139
+ if (messageOrObj instanceof Error) {
140
+ this.emit(console.error, 'error', messageOrObj.message, meta)
141
+ console.error(
142
+ JSON.stringify({
143
+ level: 'error',
144
+ message: 'stack',
145
+ stack: messageOrObj.stack,
146
+ ...(this.traceId ? { traceId: this.traceId } : {}),
147
+ })
148
+ )
149
+ } else {
150
+ this.emit(console.error, 'error', messageOrObj, meta)
82
151
  }
83
152
  }
84
153
  }
85
154
 
86
- /**
87
- * Logs a message at a specified level.
88
- * @param level - The logging level.
89
- * @param message - The message to log.
90
- * @param meta - Additional metadata to log.
91
- */
92
155
  log(level: string, message: string, ...meta: any[]): void {
93
156
  const logLevel = LogLevel[level as keyof typeof LogLevel]
94
157
  if (this.level <= logLevel) {
95
- console.log(`${level.toUpperCase()}:`, message, ...meta)
158
+ this.emit(console.log, level, message, meta)
159
+ }
160
+ }
161
+
162
+ private emit(
163
+ fn: (...args: any[]) => void,
164
+ level: string,
165
+ messageOrObj: string | Record<string, any>,
166
+ meta: any[]
167
+ ): void {
168
+ const entry: Record<string, unknown> = { level }
169
+ if (this.traceId) entry.traceId = this.traceId
170
+ if (typeof messageOrObj === 'string') {
171
+ entry.message = messageOrObj
172
+ } else {
173
+ Object.assign(entry, messageOrObj)
96
174
  }
175
+ if (meta.length > 0) entry.meta = meta
176
+ fn(JSON.stringify(entry))
97
177
  }
98
178
  }
@@ -54,4 +54,10 @@ export interface Logger {
54
54
  * @param level - The logging level to set.
55
55
  */
56
56
  setLevel(level: LogLevel): void
57
+
58
+ /**
59
+ * Creates a scoped logger with a traceId included in every log entry.
60
+ * Used per-request to correlate logs across function calls.
61
+ */
62
+ scope?(traceId: string): Logger
57
63
  }