@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.
- package/CHANGELOG.md +46 -0
- package/dist/errors/errors.d.ts +18 -0
- package/dist/errors/errors.js +33 -0
- package/dist/function/function-runner.d.ts +1 -1
- package/dist/function/function-runner.js +10 -5
- package/dist/function/functions.types.js +1 -0
- package/dist/function/index.d.ts +2 -1
- package/dist/function/index.js +1 -0
- package/dist/handle-error.d.ts +2 -2
- package/dist/handle-error.js +8 -8
- package/dist/index.d.ts +1 -2
- package/dist/index.js +1 -2
- package/dist/middleware/index.d.ts +2 -0
- package/dist/middleware/index.js +2 -0
- package/dist/middleware/remote-auth.js +5 -2
- package/dist/permissions.d.ts +13 -1
- package/dist/permissions.js +51 -0
- package/dist/pikku-state.d.ts +0 -1
- package/dist/pikku-state.js +2 -4
- package/dist/remote.d.ts +10 -0
- package/dist/remote.js +32 -0
- package/dist/schema.js +2 -0
- package/dist/services/credential-service.d.ts +11 -0
- package/dist/services/credential-wire-service.d.ts +14 -3
- package/dist/services/credential-wire-service.js +49 -6
- package/dist/services/deployment-service.d.ts +12 -1
- package/dist/services/in-memory-queue-service.d.ts +7 -0
- package/dist/services/in-memory-queue-service.js +28 -0
- package/dist/services/index.d.ts +4 -1
- package/dist/services/index.js +2 -1
- package/dist/services/local-credential-service.d.ts +2 -0
- package/dist/services/local-credential-service.js +20 -0
- package/dist/services/logger-console.d.ts +26 -40
- package/dist/services/logger-console.js +102 -46
- package/dist/services/logger.d.ts +5 -0
- package/dist/services/meta-service.d.ts +175 -0
- package/dist/services/meta-service.js +310 -0
- package/dist/services/pikku-user-id.d.ts +3 -0
- package/dist/services/pikku-user-id.js +16 -0
- package/dist/services/typed-credential-service.d.ts +2 -0
- package/dist/services/typed-credential-service.js +6 -0
- package/dist/services/workflow-service.d.ts +6 -1
- package/dist/testing/service-tests.js +0 -8
- package/dist/types/core.types.d.ts +14 -2
- package/dist/types/state.types.d.ts +9 -2
- package/dist/wirings/ai-agent/ai-agent-prepare.d.ts +21 -0
- package/dist/wirings/ai-agent/ai-agent-prepare.js +90 -21
- package/dist/wirings/ai-agent/ai-agent-registry.js +2 -2
- package/dist/wirings/ai-agent/ai-agent-runner.js +24 -2
- package/dist/wirings/ai-agent/ai-agent-stream.d.ts +2 -1
- package/dist/wirings/ai-agent/ai-agent-stream.js +61 -3
- package/dist/wirings/ai-agent/ai-agent.types.d.ts +25 -4
- package/dist/wirings/ai-agent/index.d.ts +1 -1
- package/dist/wirings/ai-agent/index.js +1 -1
- package/dist/wirings/channel/channel-runner.js +3 -3
- package/dist/wirings/cli/cli-runner.js +4 -3
- package/dist/wirings/cli/command-parser.js +7 -3
- package/dist/wirings/http/http-runner.d.ts +1 -1
- package/dist/wirings/http/http-runner.js +23 -15
- package/dist/wirings/http/http.types.d.ts +2 -0
- package/dist/wirings/mcp/mcp-runner.js +5 -3
- package/dist/wirings/queue/queue-runner.d.ts +3 -1
- package/dist/wirings/queue/queue-runner.js +8 -3
- package/dist/wirings/queue/queue.types.d.ts +6 -0
- package/dist/wirings/rpc/rpc-runner.js +57 -90
- package/dist/wirings/scheduler/scheduler-runner.d.ts +3 -1
- package/dist/wirings/scheduler/scheduler-runner.js +9 -5
- package/dist/wirings/trigger/trigger-runner.js +4 -2
- package/dist/wirings/workflow/dsl/workflow-dsl.types.d.ts +2 -0
- package/dist/wirings/workflow/dsl/workflow-runner.d.ts +1 -1
- package/dist/wirings/workflow/dsl/workflow-runner.js +6 -9
- package/dist/wirings/workflow/graph/graph-runner.d.ts +1 -1
- package/dist/wirings/workflow/graph/graph-runner.js +18 -4
- package/dist/wirings/workflow/index.d.ts +4 -2
- package/dist/wirings/workflow/index.js +4 -2
- package/dist/wirings/workflow/pikku-workflow-service.d.ts +16 -4
- package/dist/wirings/workflow/pikku-workflow-service.js +218 -24
- package/dist/wirings/workflow/workflow-queue-workers.d.ts +40 -0
- package/dist/wirings/workflow/workflow-queue-workers.js +41 -0
- package/dist/wirings/workflow/workflow.types.d.ts +19 -1
- package/package.json +4 -1
- package/src/errors/errors.ts +44 -0
- package/src/function/function-runner.ts +24 -13
- package/src/function/functions.types.ts +1 -0
- package/src/function/index.ts +10 -0
- package/src/handle-error.test.ts +2 -2
- package/src/handle-error.ts +8 -8
- package/src/index.ts +1 -2
- package/src/middleware/index.ts +2 -0
- package/src/middleware/remote-auth.test.ts +4 -4
- package/src/middleware/remote-auth.ts +7 -2
- package/src/permissions.ts +70 -0
- package/src/pikku-state.test.ts +0 -26
- package/src/pikku-state.ts +2 -5
- package/src/remote.ts +46 -0
- package/src/schema.ts +1 -0
- package/src/services/credential-service.ts +13 -0
- package/src/services/credential-wire-service.test.ts +174 -0
- package/src/services/credential-wire-service.ts +52 -8
- package/src/services/deployment-service.ts +17 -1
- package/src/services/in-memory-queue-service.ts +46 -0
- package/src/services/index.ts +21 -1
- package/src/services/local-credential-service.ts +22 -0
- package/src/services/logger-console.ts +127 -47
- package/src/services/logger.ts +6 -0
- package/src/services/meta-service.ts +523 -0
- package/src/services/pikku-user-id.test.ts +62 -0
- package/src/services/pikku-user-id.ts +17 -0
- package/src/services/typed-credential-service.ts +8 -0
- package/src/services/workflow-service.ts +8 -0
- package/src/testing/service-tests.ts +0 -10
- package/src/types/core.types.ts +16 -2
- package/src/types/state.types.ts +7 -2
- package/src/wirings/ai-agent/ai-agent-prepare.ts +122 -41
- package/src/wirings/ai-agent/ai-agent-registry.ts +3 -3
- package/src/wirings/ai-agent/ai-agent-runner.ts +22 -3
- package/src/wirings/ai-agent/ai-agent-stream.ts +115 -3
- package/src/wirings/ai-agent/ai-agent.types.ts +27 -4
- package/src/wirings/ai-agent/index.ts +1 -0
- package/src/wirings/channel/channel-handler.ts +0 -1
- package/src/wirings/channel/channel-runner.ts +4 -5
- package/src/wirings/cli/cli-runner.test.ts +8 -7
- package/src/wirings/cli/cli-runner.ts +5 -4
- package/src/wirings/cli/command-parser.ts +8 -3
- package/src/wirings/http/http-runner.ts +27 -18
- package/src/wirings/http/http.types.ts +2 -0
- package/src/wirings/mcp/mcp-runner.ts +7 -10
- package/src/wirings/queue/queue-runner.test.ts +5 -11
- package/src/wirings/queue/queue-runner.ts +12 -4
- package/src/wirings/queue/queue.types.ts +6 -0
- package/src/wirings/rpc/rpc-runner.ts +91 -118
- package/src/wirings/scheduler/scheduler-runner.test.ts +5 -11
- package/src/wirings/scheduler/scheduler-runner.ts +13 -5
- package/src/wirings/trigger/trigger-runner.test.ts +10 -11
- package/src/wirings/trigger/trigger-runner.ts +6 -4
- package/src/wirings/workflow/dsl/workflow-dsl.types.ts +2 -0
- package/src/wirings/workflow/dsl/workflow-runner.ts +11 -10
- package/src/wirings/workflow/graph/graph-runner.ts +19 -4
- package/src/wirings/workflow/index.ts +17 -6
- package/src/wirings/workflow/pikku-workflow-service.ts +286 -24
- package/src/wirings/workflow/workflow-queue-workers.ts +85 -0
- package/src/wirings/workflow/workflow.types.ts +18 -1
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/wirings/ai-agent/agent-dynamic-workflow.d.ts +0 -6
- package/dist/wirings/ai-agent/agent-dynamic-workflow.js +0 -468
- package/dist/wirings/workflow/workflow-helpers.d.ts +0 -36
- package/dist/wirings/workflow/workflow-helpers.js +0 -38
- package/src/wirings/ai-agent/agent-dynamic-workflow.ts +0 -610
- package/src/wirings/workflow/workflow-helpers.test.ts +0 -129
- 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
|
|
32
|
-
|
|
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
|
-
|
|
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 }
|
package/src/function/index.ts
CHANGED
|
@@ -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'
|
package/src/handle-error.test.ts
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
115
|
+
assert.strictEqual(http._state.jsonBody.errorId, 'tracker-2')
|
|
116
116
|
})
|
|
117
117
|
|
|
118
118
|
test('should set status 403 for ForbiddenError', () => {
|
package/src/handle-error.ts
CHANGED
|
@@ -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}
|
|
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
|
-
|
|
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
|
-
|
|
41
|
+
errorId: traceId,
|
|
42
42
|
})
|
|
43
43
|
|
|
44
44
|
// Log certain status codes as warnings
|
|
45
45
|
if (logWarningsForStatusCodes.includes(errorResponse.status)) {
|
|
46
|
-
if (
|
|
47
|
-
logger.warn(`Warning id: ${
|
|
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 (
|
|
57
|
-
logger.warn(`Error id: ${
|
|
58
|
-
const errorBody: Record<string, unknown> = { errorId:
|
|
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 {
|
package/src/middleware/index.ts
CHANGED
|
@@ -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(
|
|
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
|
}
|
package/src/permissions.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/pikku-state.test.ts
CHANGED
|
@@ -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 = {
|
package/src/pikku-state.ts
CHANGED
|
@@ -156,7 +156,8 @@ const createEmptyPackageState = (): PikkuPackageState => ({
|
|
|
156
156
|
package: {
|
|
157
157
|
factories: null,
|
|
158
158
|
singletonServices: null,
|
|
159
|
-
|
|
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
|
+
})
|