@pikku/core 0.12.13 → 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 (150) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/dist/errors/errors.d.ts +18 -0
  3. package/dist/errors/errors.js +33 -0
  4. package/dist/function/function-runner.d.ts +1 -1
  5. package/dist/function/function-runner.js +10 -5
  6. package/dist/function/functions.types.js +1 -0
  7. package/dist/function/index.d.ts +2 -1
  8. package/dist/function/index.js +1 -0
  9. package/dist/handle-error.d.ts +2 -2
  10. package/dist/handle-error.js +8 -8
  11. package/dist/index.d.ts +1 -2
  12. package/dist/index.js +1 -2
  13. package/dist/middleware/index.d.ts +2 -0
  14. package/dist/middleware/index.js +2 -0
  15. package/dist/middleware/remote-auth.js +5 -2
  16. package/dist/permissions.d.ts +13 -1
  17. package/dist/permissions.js +51 -0
  18. package/dist/pikku-state.d.ts +0 -1
  19. package/dist/pikku-state.js +2 -4
  20. package/dist/remote.d.ts +10 -0
  21. package/dist/remote.js +32 -0
  22. package/dist/schema.js +2 -0
  23. package/dist/services/credential-service.d.ts +11 -0
  24. package/dist/services/credential-wire-service.d.ts +14 -3
  25. package/dist/services/credential-wire-service.js +49 -6
  26. package/dist/services/deployment-service.d.ts +12 -1
  27. package/dist/services/in-memory-queue-service.d.ts +7 -0
  28. package/dist/services/in-memory-queue-service.js +28 -0
  29. package/dist/services/index.d.ts +4 -1
  30. package/dist/services/index.js +2 -1
  31. package/dist/services/local-credential-service.d.ts +2 -0
  32. package/dist/services/local-credential-service.js +20 -0
  33. package/dist/services/logger-console.d.ts +26 -40
  34. package/dist/services/logger-console.js +102 -46
  35. package/dist/services/logger.d.ts +5 -0
  36. package/dist/services/meta-service.d.ts +175 -0
  37. package/dist/services/meta-service.js +310 -0
  38. package/dist/services/pikku-user-id.d.ts +3 -0
  39. package/dist/services/pikku-user-id.js +16 -0
  40. package/dist/services/typed-credential-service.d.ts +2 -0
  41. package/dist/services/typed-credential-service.js +6 -0
  42. package/dist/services/workflow-service.d.ts +6 -1
  43. package/dist/testing/service-tests.js +0 -8
  44. package/dist/types/core.types.d.ts +14 -2
  45. package/dist/types/state.types.d.ts +9 -2
  46. package/dist/wirings/ai-agent/ai-agent-prepare.d.ts +21 -0
  47. package/dist/wirings/ai-agent/ai-agent-prepare.js +90 -21
  48. package/dist/wirings/ai-agent/ai-agent-registry.js +2 -2
  49. package/dist/wirings/ai-agent/ai-agent-runner.js +24 -2
  50. package/dist/wirings/ai-agent/ai-agent-stream.d.ts +2 -1
  51. package/dist/wirings/ai-agent/ai-agent-stream.js +61 -3
  52. package/dist/wirings/ai-agent/ai-agent.types.d.ts +25 -4
  53. package/dist/wirings/ai-agent/index.d.ts +1 -1
  54. package/dist/wirings/ai-agent/index.js +1 -1
  55. package/dist/wirings/channel/channel-runner.js +3 -3
  56. package/dist/wirings/cli/cli-runner.js +4 -3
  57. package/dist/wirings/cli/command-parser.js +7 -3
  58. package/dist/wirings/http/http-runner.d.ts +1 -1
  59. package/dist/wirings/http/http-runner.js +23 -15
  60. package/dist/wirings/http/http.types.d.ts +2 -0
  61. package/dist/wirings/mcp/mcp-runner.js +5 -3
  62. package/dist/wirings/queue/queue-runner.d.ts +3 -1
  63. package/dist/wirings/queue/queue-runner.js +8 -3
  64. package/dist/wirings/queue/queue.types.d.ts +6 -0
  65. package/dist/wirings/rpc/rpc-runner.js +57 -90
  66. package/dist/wirings/scheduler/scheduler-runner.d.ts +3 -1
  67. package/dist/wirings/scheduler/scheduler-runner.js +9 -5
  68. package/dist/wirings/trigger/trigger-runner.js +4 -2
  69. package/dist/wirings/workflow/dsl/workflow-dsl.types.d.ts +2 -0
  70. package/dist/wirings/workflow/dsl/workflow-runner.d.ts +1 -1
  71. package/dist/wirings/workflow/dsl/workflow-runner.js +6 -9
  72. package/dist/wirings/workflow/graph/graph-runner.d.ts +1 -1
  73. package/dist/wirings/workflow/graph/graph-runner.js +18 -4
  74. package/dist/wirings/workflow/index.d.ts +4 -2
  75. package/dist/wirings/workflow/index.js +4 -2
  76. package/dist/wirings/workflow/pikku-workflow-service.d.ts +16 -4
  77. package/dist/wirings/workflow/pikku-workflow-service.js +218 -24
  78. package/dist/wirings/workflow/workflow-queue-workers.d.ts +40 -0
  79. package/dist/wirings/workflow/workflow-queue-workers.js +41 -0
  80. package/dist/wirings/workflow/workflow.types.d.ts +19 -1
  81. package/package.json +4 -1
  82. package/src/errors/errors.ts +44 -0
  83. package/src/function/function-runner.ts +24 -13
  84. package/src/function/functions.types.ts +1 -0
  85. package/src/function/index.ts +10 -0
  86. package/src/handle-error.test.ts +2 -2
  87. package/src/handle-error.ts +8 -8
  88. package/src/index.ts +1 -2
  89. package/src/middleware/index.ts +2 -0
  90. package/src/middleware/remote-auth.test.ts +4 -4
  91. package/src/middleware/remote-auth.ts +7 -2
  92. package/src/permissions.ts +70 -0
  93. package/src/pikku-state.test.ts +0 -26
  94. package/src/pikku-state.ts +2 -5
  95. package/src/remote.ts +46 -0
  96. package/src/schema.ts +1 -0
  97. package/src/services/credential-service.ts +13 -0
  98. package/src/services/credential-wire-service.test.ts +174 -0
  99. package/src/services/credential-wire-service.ts +52 -8
  100. package/src/services/deployment-service.ts +17 -1
  101. package/src/services/in-memory-queue-service.ts +46 -0
  102. package/src/services/index.ts +21 -1
  103. package/src/services/local-credential-service.ts +22 -0
  104. package/src/services/logger-console.ts +127 -47
  105. package/src/services/logger.ts +6 -0
  106. package/src/services/meta-service.ts +523 -0
  107. package/src/services/pikku-user-id.test.ts +62 -0
  108. package/src/services/pikku-user-id.ts +17 -0
  109. package/src/services/typed-credential-service.ts +8 -0
  110. package/src/services/workflow-service.ts +8 -0
  111. package/src/testing/service-tests.ts +0 -10
  112. package/src/types/core.types.ts +16 -2
  113. package/src/types/state.types.ts +7 -2
  114. package/src/wirings/ai-agent/ai-agent-prepare.ts +122 -41
  115. package/src/wirings/ai-agent/ai-agent-registry.ts +3 -3
  116. package/src/wirings/ai-agent/ai-agent-runner.ts +22 -3
  117. package/src/wirings/ai-agent/ai-agent-stream.ts +115 -3
  118. package/src/wirings/ai-agent/ai-agent.types.ts +27 -4
  119. package/src/wirings/ai-agent/index.ts +1 -0
  120. package/src/wirings/channel/channel-handler.ts +0 -1
  121. package/src/wirings/channel/channel-runner.ts +4 -5
  122. package/src/wirings/cli/cli-runner.test.ts +8 -7
  123. package/src/wirings/cli/cli-runner.ts +5 -4
  124. package/src/wirings/cli/command-parser.ts +8 -3
  125. package/src/wirings/http/http-runner.ts +27 -18
  126. package/src/wirings/http/http.types.ts +2 -0
  127. package/src/wirings/mcp/mcp-runner.ts +7 -10
  128. package/src/wirings/queue/queue-runner.test.ts +5 -11
  129. package/src/wirings/queue/queue-runner.ts +12 -4
  130. package/src/wirings/queue/queue.types.ts +6 -0
  131. package/src/wirings/rpc/rpc-runner.ts +91 -118
  132. package/src/wirings/scheduler/scheduler-runner.test.ts +5 -11
  133. package/src/wirings/scheduler/scheduler-runner.ts +13 -5
  134. package/src/wirings/trigger/trigger-runner.test.ts +10 -11
  135. package/src/wirings/trigger/trigger-runner.ts +6 -4
  136. package/src/wirings/workflow/dsl/workflow-dsl.types.ts +2 -0
  137. package/src/wirings/workflow/dsl/workflow-runner.ts +11 -10
  138. package/src/wirings/workflow/graph/graph-runner.ts +19 -4
  139. package/src/wirings/workflow/index.ts +17 -6
  140. package/src/wirings/workflow/pikku-workflow-service.ts +286 -24
  141. package/src/wirings/workflow/workflow-queue-workers.ts +85 -0
  142. package/src/wirings/workflow/workflow.types.ts +18 -1
  143. package/tsconfig.tsbuildinfo +1 -1
  144. package/dist/wirings/ai-agent/agent-dynamic-workflow.d.ts +0 -6
  145. package/dist/wirings/ai-agent/agent-dynamic-workflow.js +0 -468
  146. package/dist/wirings/workflow/workflow-helpers.d.ts +0 -36
  147. package/dist/wirings/workflow/workflow-helpers.js +0 -38
  148. package/src/wirings/ai-agent/agent-dynamic-workflow.ts +0 -610
  149. package/src/wirings/workflow/workflow-helpers.test.ts +0 -129
  150. package/src/wirings/workflow/workflow-helpers.ts +0 -79
@@ -28,8 +28,10 @@ import { parseVersionedId } from '../version.js'
28
28
  import type { SessionService } from '../services/user-session-service.js'
29
29
  import { createFunctionSessionWireProps } from '../services/user-session-service.js'
30
30
  import { ForbiddenError, ReadonlySessionError } from '../errors/errors.js'
31
- import type { PikkuCredentialWireService } from '../services/credential-wire-service.js'
32
- import { createWireServicesCredentialWireProps } from '../services/credential-wire-service.js'
31
+ import {
32
+ PikkuCredentialWireService,
33
+ createWireServicesCredentialWireProps,
34
+ } from '../services/credential-wire-service.js'
33
35
  import { rpcService } from '../wirings/rpc/rpc-runner.js'
34
36
  import { closeWireServices } from '../utils.js'
35
37
 
@@ -226,6 +228,21 @@ export const runPikkuFunc = async <In = any, Out = any>(
226
228
  )
227
229
  : wire
228
230
 
231
+ // Set up credential wire service early so middleware can use setCredential.
232
+ // Skip if already set up (e.g. addon functions reuse the parent wire).
233
+ if (!resolvedWire.getCredentials) {
234
+ const resolvedCredentialWireService =
235
+ credentialWireService ??
236
+ new PikkuCredentialWireService(
237
+ resolvedSingletonServices.credentialService,
238
+ resolvedWire
239
+ )
240
+ Object.assign(
241
+ resolvedWire,
242
+ createWireServicesCredentialWireProps(resolvedCredentialWireService)
243
+ )
244
+ }
245
+
229
246
  // Convert tags to PermissionMetadata and merge with inheritedPermissions
230
247
  let mergedInheritedPermissions: PermissionMetadata[]
231
248
  if (tags && tags.length > 0) {
@@ -316,18 +333,12 @@ export const runPikkuFunc = async <In = any, Out = any>(
316
333
  })
317
334
  }
318
335
 
319
- if (credentialWireService) {
320
- Object.assign(
321
- resolvedWire,
322
- createWireServicesCredentialWireProps(credentialWireService)
323
- )
324
- }
325
-
326
- const wireServices = await resolvedCreateWireServices?.(
327
- resolvedSingletonServices,
328
- resolvedWire
329
- )
336
+ let wireServices: Record<string, unknown> | undefined
330
337
  try {
338
+ wireServices = (await resolvedCreateWireServices?.(
339
+ resolvedSingletonServices,
340
+ resolvedWire
341
+ )) as Record<string, unknown> | undefined
331
342
  const services =
332
343
  wireServices && Object.keys(wireServices).length > 0
333
344
  ? { ...resolvedSingletonServices, ...wireServices }
@@ -257,6 +257,7 @@ export const pikkuAuth = <
257
257
  if (!session) return false
258
258
  return fn(services, session as Session)
259
259
  }
260
+ ;(wrapper as any).__pikkuAuth = true
260
261
  return wrapper as any
261
262
  }
262
263
 
@@ -1,5 +1,15 @@
1
1
  export { addFunction, getAllFunctionNames } from './function-runner.js'
2
+ export {
3
+ pikkuAuth,
4
+ pikkuPermission,
5
+ pikkuPermissionFactory,
6
+ pikkuApprovalDescription,
7
+ } from './functions.types.js'
2
8
  export type {
3
9
  CorePikkuFunction,
4
10
  CorePikkuFunctionSessionless,
11
+ CorePikkuFunctionConfig,
12
+ CorePikkuAuth,
13
+ CorePikkuAuthConfig,
14
+ CorePikkuPermission,
5
15
  } from './functions.types.js'
@@ -91,7 +91,7 @@ describe('handleHTTPError', () => {
91
91
  assert.deepStrictEqual(http._state.jsonBody, {
92
92
  message: 'The server cannot find the requested resource.',
93
93
  payload: undefined,
94
- traceId: 'tracker-1',
94
+ errorId: 'tracker-1',
95
95
  })
96
96
  })
97
97
 
@@ -112,7 +112,7 @@ describe('handleHTTPError', () => {
112
112
 
113
113
  assert.strictEqual(http._state.statusCode, 400)
114
114
  assert.ok(http._state.jsonBody.message.includes('client error'))
115
- assert.strictEqual(http._state.jsonBody.traceId, 'tracker-2')
115
+ assert.strictEqual(http._state.jsonBody.errorId, 'tracker-2')
116
116
  })
117
117
 
118
118
  test('should set status 403 for ForbiddenError', () => {
@@ -9,7 +9,7 @@ import type { PikkuHTTP } from './wirings/http/http.types.js'
9
9
  *
10
10
  * @param {any} e - The error that occurred
11
11
  * @param {PikkuHTTP | undefined} http - HTTP wire object
12
- * @param {string} trackerId - Unique ID for tracking this error
12
+ * @param {string} traceId - Unique ID for tracking this error
13
13
  * @param {Logger} logger - Logger service
14
14
  * @param {number[]} logWarningsForStatusCodes - HTTP status codes to log as warnings
15
15
  * @param {boolean} respondWith404 - Whether to respond with 404 for NotFoundError
@@ -18,7 +18,7 @@ import type { PikkuHTTP } from './wirings/http/http.types.js'
18
18
  export const handleHTTPError = (
19
19
  e: any,
20
20
  http: PikkuHTTP | undefined,
21
- trackerId: string | undefined,
21
+ traceId: string | undefined,
22
22
  logger: Logger,
23
23
  logWarningsForStatusCodes: number[],
24
24
  respondWith404: boolean,
@@ -38,13 +38,13 @@ export const handleHTTPError = (
38
38
  http?.response?.json({
39
39
  message: errorResponse.message,
40
40
  payload: (e as any).payload,
41
- traceId: trackerId,
41
+ errorId: traceId,
42
42
  })
43
43
 
44
44
  // Log certain status codes as warnings
45
45
  if (logWarningsForStatusCodes.includes(errorResponse.status)) {
46
- if (trackerId) {
47
- logger.warn(`Warning id: ${trackerId}`)
46
+ if (traceId) {
47
+ logger.warn(`Warning id: ${traceId}`)
48
48
  }
49
49
  logger.warn(e instanceof Error ? e.message : e)
50
50
  }
@@ -53,9 +53,9 @@ export const handleHTTPError = (
53
53
  logger.error(e instanceof Error ? e.message : e)
54
54
  http?.response?.status(500)
55
55
 
56
- if (trackerId) {
57
- logger.warn(`Error id: ${trackerId}`)
58
- const errorBody: Record<string, unknown> = { errorId: trackerId }
56
+ if (traceId) {
57
+ logger.warn(`Error id: ${traceId}`)
58
+ const errorBody: Record<string, unknown> = { errorId: traceId }
59
59
  if (exposeErrors && !isProduction() && e instanceof Error) {
60
60
  errorBody.message = e.message
61
61
  errorBody.stack = e.stack
package/src/index.ts CHANGED
@@ -103,12 +103,11 @@ export type { AIStorageService } from './services/ai-storage-service.js'
103
103
  export type { HTTPMethod } from './wirings/http/http.types.js'
104
104
  export type { GraphNodeConfig } from './wirings/workflow/graph/workflow-graph.types.js'
105
105
  export { createGraph } from './wirings/workflow/graph/graph-node.js'
106
- export { workflow as wireWorkflow } from './wirings/workflow/workflow-helpers.js'
107
106
  export { wireAddon } from './wirings/rpc/wire-addon.js'
108
107
  export type { WireAddonConfig } from './wirings/rpc/wire-addon.js'
109
108
  export type { PikkuPackageState } from './types/state.types.js'
110
109
  export { runMiddleware, addMiddleware } from './middleware-runner.js'
111
- export { addPermission } from './permissions.js'
110
+ export { addPermission, checkAuthPermissions } from './permissions.js'
112
111
  export { isSerializable, stopSingletonServices } from './utils.js'
113
112
  export { getSingletonServices, getCreateWireServices } from './pikku-state.js'
114
113
  export {
@@ -3,3 +3,5 @@ export { authCookie } from './auth-cookie.js'
3
3
  export { authBearer } from './auth-bearer.js'
4
4
  export { pikkuRemoteAuthMiddleware } from './remote-auth.js'
5
5
  export { cors } from './cors.js'
6
+ export { addMiddleware, runMiddleware } from '../middleware-runner.js'
7
+ export { addPermission } from '../permissions.js'
@@ -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,7 +156,8 @@ const createEmptyPackageState = (): PikkuPackageState => ({
156
156
  package: {
157
157
  factories: null,
158
158
  singletonServices: null,
159
- metaDir: null,
159
+ credentialsMeta: null,
160
+ requiredParentServices: null,
160
161
  },
161
162
  })
162
163
 
@@ -187,10 +188,6 @@ if (!getAllPackageStates().has('__main__')) {
187
188
  resetPikkuState()
188
189
  }
189
190
 
190
- export const getPikkuMetaDir = (packageName?: string | null): string | null => {
191
- return pikkuState(packageName ?? null, 'package', 'metaDir')
192
- }
193
-
194
191
  export const getSingletonServices = (): CoreSingletonServices => {
195
192
  const services = pikkuState(null, 'package', 'singletonServices')
196
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') {
@@ -41,4 +41,17 @@ export interface CredentialService {
41
41
  * @returns A record of credential name to value.
42
42
  */
43
43
  getAll(userId: string): Promise<Record<string, unknown>>
44
+
45
+ /**
46
+ * Lists all user IDs that have a specific credential configured.
47
+ * @param name - The credential name.
48
+ * @returns Array of user IDs.
49
+ */
50
+ getUsersWithCredential(name: string): Promise<string[]>
51
+
52
+ /**
53
+ * Lists all unique user IDs that have any credential configured.
54
+ * @returns Array of user IDs.
55
+ */
56
+ getAllUsers(): Promise<string[]>
44
57
  }
@@ -0,0 +1,174 @@
1
+ import { describe, test } from 'node:test'
2
+ import assert from 'node:assert'
3
+ import {
4
+ PikkuCredentialWireService,
5
+ createMiddlewareCredentialWireProps,
6
+ createWireServicesCredentialWireProps,
7
+ } from './credential-wire-service.js'
8
+ import type { CredentialService } from './credential-service.js'
9
+ import type { PikkuWire } from '../types/core.types.js'
10
+
11
+ const mockCredentialService = (
12
+ creds: Record<string, unknown>
13
+ ): CredentialService => ({
14
+ get: async (name: string) => creds[name] ?? null,
15
+ set: async () => {},
16
+ delete: async () => {},
17
+ has: async (name: string) => name in creds,
18
+ getAll: async () => creds,
19
+ })
20
+
21
+ describe('PikkuCredentialWireService', () => {
22
+ test('should set and get credentials manually', async () => {
23
+ const service = new PikkuCredentialWireService()
24
+ service.set('stripe', { apiKey: 'sk_test' })
25
+ const result = await service.getAll()
26
+ assert.deepStrictEqual(result, { stripe: { apiKey: 'sk_test' } })
27
+ })
28
+
29
+ test('should return sync after first load (no credential service)', async () => {
30
+ const service = new PikkuCredentialWireService()
31
+ service.set('foo', 'bar')
32
+ // First call triggers lazy load (no-op without credentialService)
33
+ await service.getAll()
34
+ // Second call should be sync
35
+ const result = service.getAll()
36
+ assert.ok(!(result instanceof Promise), 'expected sync return')
37
+ assert.deepStrictEqual(result, { foo: 'bar' })
38
+ })
39
+
40
+ test('should lazy-load from credential service on first getAll', async () => {
41
+ const credService = mockCredentialService({
42
+ stripe: { apiKey: 'sk_live' },
43
+ github: { token: 'ghp_abc' },
44
+ })
45
+ const wire: PikkuWire = { session: { userId: 'user-1' } as any }
46
+ const service = new PikkuCredentialWireService(credService, wire)
47
+
48
+ const result = await service.getAll()
49
+ assert.deepStrictEqual(result, {
50
+ stripe: { apiKey: 'sk_live' },
51
+ github: { token: 'ghp_abc' },
52
+ })
53
+ })
54
+
55
+ test('should set pikkuUserId on wire after lazy load', async () => {
56
+ const credService = mockCredentialService({})
57
+ const wire: PikkuWire = { session: { userId: 'user-1' } as any }
58
+ const service = new PikkuCredentialWireService(credService, wire)
59
+
60
+ await service.getAll()
61
+ assert.strictEqual(wire.pikkuUserId, 'user-1')
62
+ })
63
+
64
+ test('should not overwrite manually set credentials during lazy load', async () => {
65
+ const credService = mockCredentialService({
66
+ stripe: { apiKey: 'from-db' },
67
+ })
68
+ const wire: PikkuWire = { session: { userId: 'user-1' } as any }
69
+ const service = new PikkuCredentialWireService(credService, wire)
70
+
71
+ service.set('stripe', { apiKey: 'from-middleware' })
72
+ const result = await service.getAll()
73
+ assert.deepStrictEqual(result.stripe, { apiKey: 'from-middleware' })
74
+ })
75
+
76
+ test('should return sync on subsequent calls after lazy load', async () => {
77
+ const credService = mockCredentialService({ stripe: { key: '123' } })
78
+ const wire: PikkuWire = { session: { userId: 'user-1' } as any }
79
+ const service = new PikkuCredentialWireService(credService, wire)
80
+
81
+ await service.getAll()
82
+ const result = service.getAll()
83
+ assert.ok(!(result instanceof Promise), 'expected sync return')
84
+ assert.deepStrictEqual(result, { stripe: { key: '123' } })
85
+ })
86
+
87
+ test('should not load when no userId resolvable', async () => {
88
+ let called = false
89
+ const credService: CredentialService = {
90
+ get: async () => null,
91
+ set: async () => {},
92
+ delete: async () => {},
93
+ has: async () => false,
94
+ getAll: async () => {
95
+ called = true
96
+ return {}
97
+ },
98
+ }
99
+ const wire: PikkuWire = {}
100
+ const service = new PikkuCredentialWireService(credService, wire)
101
+
102
+ const result = await service.getAll()
103
+ assert.deepStrictEqual(result, {})
104
+ assert.strictEqual(called, false)
105
+ })
106
+
107
+ test('should deduplicate concurrent lazy load calls', async () => {
108
+ let callCount = 0
109
+ const credService: CredentialService = {
110
+ get: async () => null,
111
+ set: async () => {},
112
+ delete: async () => {},
113
+ has: async () => false,
114
+ getAll: async () => {
115
+ callCount++
116
+ return { stripe: { key: 'test' } }
117
+ },
118
+ }
119
+ const wire: PikkuWire = { session: { userId: 'user-1' } as any }
120
+ const service = new PikkuCredentialWireService(credService, wire)
121
+
122
+ const [r1, r2] = await Promise.all([service.getAll(), service.getAll()])
123
+ assert.strictEqual(callCount, 1)
124
+ assert.deepStrictEqual(r1, r2)
125
+ })
126
+
127
+ test('getScoped should only return allowed names', async () => {
128
+ const credService = mockCredentialService({
129
+ stripe: { key: '1' },
130
+ github: { token: '2' },
131
+ slack: { webhook: '3' },
132
+ })
133
+ const wire: PikkuWire = { session: { userId: 'user-1' } as any }
134
+ const service = new PikkuCredentialWireService(credService, wire)
135
+
136
+ const result = await service.getScoped(['stripe', 'slack'])
137
+ assert.deepStrictEqual(result, {
138
+ stripe: { key: '1' },
139
+ slack: { webhook: '3' },
140
+ })
141
+ })
142
+ })
143
+
144
+ describe('createMiddlewareCredentialWireProps', () => {
145
+ test('should provide setCredential that writes to service', async () => {
146
+ const service = new PikkuCredentialWireService()
147
+ const props = createMiddlewareCredentialWireProps(service)
148
+ props.setCredential('stripe', { apiKey: 'test' })
149
+ const result = await service.getAll()
150
+ assert.deepStrictEqual(result, { stripe: { apiKey: 'test' } })
151
+ })
152
+ })
153
+
154
+ describe('createWireServicesCredentialWireProps', () => {
155
+ test('should provide both setCredential and getCredentials', async () => {
156
+ const service = new PikkuCredentialWireService()
157
+ const props = createWireServicesCredentialWireProps(service)
158
+ props.setCredential('stripe', { apiKey: 'test' })
159
+ const result = await props.getCredentials()
160
+ assert.deepStrictEqual(result, { stripe: { apiKey: 'test' } })
161
+ })
162
+
163
+ test('should scope getCredentials when allowedNames provided', async () => {
164
+ const credService = mockCredentialService({
165
+ stripe: { key: '1' },
166
+ github: { token: '2' },
167
+ })
168
+ const wire: PikkuWire = { session: { userId: 'user-1' } as any }
169
+ const service = new PikkuCredentialWireService(credService, wire)
170
+ const props = createWireServicesCredentialWireProps(service, ['stripe'])
171
+ const result = await props.getCredentials()
172
+ assert.deepStrictEqual(result, { stripe: { key: '1' } })
173
+ })
174
+ })