@pikku/core 0.12.80 → 0.12.82
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 +312 -0
- package/dist/errors/index.d.ts +1 -1
- package/dist/errors/index.js +1 -1
- package/dist/function/function-runner.js +2 -5
- package/dist/function/index.d.ts +1 -1
- package/dist/index.d.ts +11 -11
- package/dist/index.js +3 -3
- package/dist/pikku-state.js +4 -0
- package/dist/services/ai-agent-runner-service.d.ts +7 -0
- package/dist/services/ai-run-state-service.d.ts +10 -0
- package/dist/services/in-memory-ai-run-state-service.d.ts +5 -1
- package/dist/services/in-memory-ai-run-state-service.js +9 -0
- package/dist/services/index.d.ts +15 -15
- package/dist/services/index.js +5 -5
- package/dist/services/scoped-credential-service.d.ts +21 -0
- package/dist/services/scoped-credential-service.js +53 -0
- package/dist/testing/service-tests/ai-storage-service-tests.js +76 -0
- package/dist/types/core.types.d.ts +0 -2
- package/dist/types/state.types.d.ts +13 -0
- package/dist/wirings/actor-flow/index.d.ts +1 -1
- package/dist/wirings/ai-agent/ai-agent-finalize.d.ts +58 -0
- package/dist/wirings/ai-agent/ai-agent-finalize.js +138 -0
- package/dist/wirings/ai-agent/ai-agent-interrupt.js +1 -0
- package/dist/wirings/ai-agent/ai-agent-memory.d.ts +2 -8
- package/dist/wirings/ai-agent/ai-agent-memory.js +34 -17
- package/dist/wirings/ai-agent/ai-agent-model-config.d.ts +7 -0
- package/dist/wirings/ai-agent/ai-agent-model-config.js +44 -1
- package/dist/wirings/ai-agent/ai-agent-prepare.js +2 -0
- package/dist/wirings/ai-agent/ai-agent-runner.js +61 -40
- package/dist/wirings/ai-agent/ai-agent-stream.js +89 -36
- package/dist/wirings/ai-agent/ai-agent-turn.d.ts +1 -0
- package/dist/wirings/ai-agent/ai-agent-turn.js +1 -0
- package/dist/wirings/ai-agent/ai-agent.types.d.ts +46 -1
- package/dist/wirings/ai-agent/index.d.ts +8 -7
- package/dist/wirings/ai-agent/index.js +5 -4
- package/dist/wirings/ai-scorer/ai-scorer-grade.d.ts +26 -0
- package/dist/wirings/ai-scorer/ai-scorer-grade.js +33 -0
- package/dist/wirings/ai-scorer/ai-scorer-judge.d.ts +17 -0
- package/dist/wirings/ai-scorer/ai-scorer-judge.js +92 -0
- package/dist/wirings/ai-scorer/ai-scorer-live.d.ts +15 -0
- package/dist/wirings/ai-scorer/ai-scorer-live.js +38 -0
- package/dist/wirings/ai-scorer/ai-scorer-registry.d.ts +18 -0
- package/dist/wirings/ai-scorer/ai-scorer-registry.js +46 -0
- package/dist/wirings/ai-scorer/ai-scorer-sampling.d.ts +8 -0
- package/dist/wirings/ai-scorer/ai-scorer-sampling.js +31 -0
- package/dist/wirings/ai-scorer/ai-scorer-snapshots.d.ts +10 -0
- package/dist/wirings/ai-scorer/ai-scorer-snapshots.js +40 -0
- package/dist/wirings/ai-scorer/ai-scorer-worker.d.ts +15 -0
- package/dist/wirings/ai-scorer/ai-scorer-worker.js +58 -0
- package/dist/wirings/ai-scorer/ai-scorer.d.ts +39 -0
- package/dist/wirings/ai-scorer/ai-scorer.js +40 -0
- package/dist/wirings/ai-scorer/ai-scorer.types.d.ts +90 -0
- package/dist/wirings/ai-scorer/ai-scorer.types.js +4 -0
- package/dist/wirings/ai-scorer/index.d.ts +6 -0
- package/dist/wirings/ai-scorer/index.js +5 -0
- package/dist/wirings/channel/index.d.ts +5 -6
- package/dist/wirings/channel/index.js +3 -4
- package/dist/wirings/channel/local/local-channel-runner.js +8 -1
- package/dist/wirings/cli/channel/cli-raw-channel-runner.js +9 -1
- package/dist/wirings/cli/channel/index.d.ts +1 -2
- package/dist/wirings/cli/channel/index.js +0 -1
- package/dist/wirings/cli/cli-runner.js +13 -1
- package/dist/wirings/credential/index.d.ts +1 -1
- package/dist/wirings/gateway/index.d.ts +1 -1
- package/dist/wirings/http/http-runner.js +8 -2
- package/dist/wirings/http/index.d.ts +1 -2
- package/dist/wirings/mcp/index.d.ts +1 -1
- package/dist/wirings/mcp/mcp-runner.d.ts +15 -0
- package/dist/wirings/mcp/mcp-runner.js +18 -5
- package/dist/wirings/persona/index.d.ts +3 -4
- package/dist/wirings/persona/index.js +2 -3
- package/dist/wirings/queue/index.d.ts +1 -3
- package/dist/wirings/queue/index.js +1 -3
- package/dist/wirings/rpc/addon-runner.d.ts +4 -0
- package/dist/wirings/rpc/addon-runner.js +19 -3
- package/dist/wirings/rpc/rpc-runner.js +2 -0
- package/dist/wirings/rpc/rpc-types.d.ts +4 -0
- package/dist/wirings/rpc/wire-addon.d.ts +13 -0
- package/dist/wirings/rpc/wire-addon.js +4 -0
- package/dist/wirings/scheduler/index.d.ts +1 -1
- package/dist/wirings/trigger/index.d.ts +1 -1
- package/dist/wirings/virtual-user/index.d.ts +5 -6
- package/dist/wirings/virtual-user/index.js +2 -4
- package/dist/wirings/workflow/dsl/workflow-dsl.types.d.ts +85 -15
- package/dist/wirings/workflow/index.d.ts +6 -6
- package/dist/wirings/workflow/index.js +2 -2
- package/dist/wirings/workflow/pikku-scenario-service.d.ts +7 -7
- package/dist/wirings/workflow/pikku-scenario-service.js +39 -13
- package/dist/wirings/workflow/pikku-workflow-service.js +17 -3
- package/dist/wirings/workflow/scenario-step.types.d.ts +8 -0
- package/dist/wirings/workflow/workflow-approval-audit.d.ts +16 -0
- package/dist/wirings/workflow/workflow-approval-audit.js +40 -0
- package/dist/wirings/workflow/workflow-approval-policy.d.ts +20 -0
- package/dist/wirings/workflow/workflow-approval-policy.js +48 -0
- package/dist/wirings/workflow/workflow-approval.d.ts +29 -1
- package/dist/wirings/workflow/workflow-approval.js +65 -2
- package/dist/wirings/workflow/workflow-run-ownership.d.ts +2 -1
- package/dist/wirings/workflow/workflow-run-ownership.js +2 -1
- package/dist/wirings/workflow/workflow.types.d.ts +1 -1
- package/knowledge/decisions/internals/addon-pikku-meta-ships-at-the-package-root-or-under-dist.md +32 -0
- package/knowledge/decisions/internals/an-addon-scope-root-loses-to-a-root-the-host-already-declares.md +39 -0
- package/knowledge/decisions/internals/index.md +30 -3
- package/knowledge/decisions/internals/validate-runs-checks-by-precondition.md +115 -0
- package/knowledge/decisions/security/a-function-never-receives-the-secret-service.md +37 -0
- package/knowledge/decisions/security/a-workflow-run-is-read-and-approved-by-its-owner.md +30 -14
- package/knowledge/decisions/security/an-approval-answer-outlives-the-run-it-answered.md +59 -0
- package/knowledge/decisions/security/index.md +3 -1
- package/knowledge/questions/index.md +1 -1
- package/package.json +3 -1
- package/scripts/generate-api-report.mts +143 -18
- package/src/api-report.test.ts +2 -2
- package/src/errors/index.ts +1 -1
- package/src/function/function-runner.test.ts +52 -0
- package/src/function/function-runner.ts +5 -9
- package/src/function/index.ts +0 -2
- package/src/index.ts +0 -35
- package/src/pikku-state.ts +5 -0
- package/src/public-surface.json +70 -94
- package/src/services/ai-agent-runner-service.ts +12 -1
- package/src/services/ai-run-state-service.ts +11 -0
- package/src/services/in-memory-ai-run-state-service.ts +13 -0
- package/src/services/index.ts +3 -43
- package/src/services/scoped-credential-service.test.ts +86 -0
- package/src/services/scoped-credential-service.ts +63 -0
- package/src/testing/service-tests/ai-storage-service-tests.ts +93 -0
- package/src/types/core.types.ts +3 -6
- package/src/types/state.types.ts +16 -0
- package/src/wirings/actor-flow/index.ts +0 -3
- package/src/wirings/ai-agent/ai-agent-finalize.test.ts +186 -0
- package/src/wirings/ai-agent/ai-agent-finalize.ts +197 -0
- package/src/wirings/ai-agent/ai-agent-interrupt.ts +1 -0
- package/src/wirings/ai-agent/ai-agent-memory.ts +54 -38
- package/src/wirings/ai-agent/ai-agent-model-config.test.ts +72 -3
- package/src/wirings/ai-agent/ai-agent-model-config.ts +49 -1
- package/src/wirings/ai-agent/ai-agent-prepare.ts +2 -0
- package/src/wirings/ai-agent/ai-agent-runner.ts +71 -40
- package/src/wirings/ai-agent/ai-agent-stream-output-hooks.test.ts +353 -0
- package/src/wirings/ai-agent/ai-agent-stream.ts +116 -54
- package/src/wirings/ai-agent/ai-agent-turn.test.ts +67 -0
- package/src/wirings/ai-agent/ai-agent-turn.ts +1 -0
- package/src/wirings/ai-agent/ai-agent.types.ts +64 -4
- package/src/wirings/ai-agent/index.ts +2 -16
- package/src/wirings/ai-scorer/ai-scorer-grade.test.ts +106 -0
- package/src/wirings/ai-scorer/ai-scorer-grade.ts +55 -0
- package/src/wirings/ai-scorer/ai-scorer-judge.test.ts +143 -0
- package/src/wirings/ai-scorer/ai-scorer-judge.ts +120 -0
- package/src/wirings/ai-scorer/ai-scorer-live.test.ts +174 -0
- package/src/wirings/ai-scorer/ai-scorer-live.ts +56 -0
- package/src/wirings/ai-scorer/ai-scorer-registry.ts +63 -0
- package/src/wirings/ai-scorer/ai-scorer-sampling.test.ts +34 -0
- package/src/wirings/ai-scorer/ai-scorer-sampling.ts +36 -0
- package/src/wirings/ai-scorer/ai-scorer-snapshots.test.ts +49 -0
- package/src/wirings/ai-scorer/ai-scorer-snapshots.ts +46 -0
- package/src/wirings/ai-scorer/ai-scorer-worker.test.ts +122 -0
- package/src/wirings/ai-scorer/ai-scorer-worker.ts +69 -0
- package/src/wirings/ai-scorer/ai-scorer.ts +76 -0
- package/src/wirings/ai-scorer/ai-scorer.types.ts +107 -0
- package/src/wirings/ai-scorer/index.ts +24 -0
- package/src/wirings/channel/index.ts +1 -20
- package/src/wirings/channel/local/local-channel-runner.test.ts +68 -0
- package/src/wirings/channel/local/local-channel-runner.ts +8 -1
- package/src/wirings/cli/channel/cli-raw-channel-runner.test.ts +23 -0
- package/src/wirings/cli/channel/cli-raw-channel-runner.ts +12 -1
- package/src/wirings/cli/channel/index.ts +0 -7
- package/src/wirings/cli/cli-runner.test.ts +68 -0
- package/src/wirings/cli/cli-runner.ts +18 -1
- package/src/wirings/credential/index.ts +0 -1
- package/src/wirings/gateway/index.ts +0 -3
- package/src/wirings/http/http-runner.test.ts +66 -0
- package/src/wirings/http/http-runner.ts +10 -2
- package/src/wirings/http/index.ts +1 -1
- package/src/wirings/mcp/index.ts +0 -1
- package/src/wirings/mcp/mcp-runner.test.ts +181 -0
- package/src/wirings/mcp/mcp-runner.ts +35 -5
- package/src/wirings/persona/index.ts +0 -8
- package/src/wirings/queue/index.ts +0 -14
- package/src/wirings/rpc/addon-runner.ts +34 -3
- package/src/wirings/rpc/addon-secrets.test.ts +261 -0
- package/src/wirings/rpc/rpc-runner.test.ts +2 -0
- package/src/wirings/rpc/rpc-runner.ts +2 -0
- package/src/wirings/rpc/rpc-types.ts +4 -0
- package/src/wirings/rpc/wire-addon.ts +17 -0
- package/src/wirings/scheduler/index.ts +0 -1
- package/src/wirings/trigger/index.ts +0 -1
- package/src/wirings/virtual-user/index.ts +0 -16
- package/src/wirings/workflow/dsl/workflow-dsl.types.ts +96 -16
- package/src/wirings/workflow/graph/graph-runner.test.ts +72 -0
- package/src/wirings/workflow/index.ts +2 -20
- package/src/wirings/workflow/pikku-scenario-service.ts +60 -15
- package/src/wirings/workflow/pikku-workflow-service.test.ts +13 -12
- package/src/wirings/workflow/pikku-workflow-service.ts +28 -4
- package/src/wirings/workflow/scenario-expectations.test.ts +75 -0
- package/src/wirings/workflow/scenario-hooks.test.ts +3 -2
- package/src/wirings/workflow/scenario-step.types.ts +8 -0
- package/src/wirings/workflow/workflow-approval-audit.ts +47 -0
- package/src/wirings/workflow/workflow-approval-policy.test.ts +524 -0
- package/src/wirings/workflow/workflow-approval-policy.ts +68 -0
- package/src/wirings/workflow/workflow-approval.ts +113 -9
- package/src/wirings/workflow/workflow-run-authority.test.ts +12 -15
- package/src/wirings/workflow/workflow-run-ownership.ts +2 -1
- package/src/wirings/workflow/workflow.types.ts +0 -9
- package/src/wirings-stay-decoupled.test.ts +6 -2
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/internal.d.ts +0 -3
- package/dist/internal.js +0 -2
- package/dist/middleware/timeout.d.ts +0 -9
- package/dist/middleware/timeout.js +0 -15
- package/dist/pikku-response.d.ts +0 -6
- package/dist/pikku-response.js +0 -6
- package/dist/services/gopass-secrets.d.ts +0 -15
- package/dist/services/gopass-secrets.js +0 -76
- package/dist/services/http-scenario-actors.d.ts +0 -75
- package/dist/services/http-scenario-actors.js +0 -195
- package/dist/services/http-user-flow-actors.d.ts +0 -67
- package/dist/services/http-user-flow-actors.js +0 -193
- package/dist/services/scenario-actors-service.d.ts +0 -127
- package/dist/services/scenario-actors-service.js +0 -40
- package/dist/services/user-flow-actors-service.d.ts +0 -39
- package/dist/services/user-flow-actors-service.js +0 -1
- package/dist/wirings/credential/wire-credential.d.ts +0 -48
- package/dist/wirings/credential/wire-credential.js +0 -47
- package/dist/wirings/oauth2/oauth2-client.d.ts +0 -47
- package/dist/wirings/oauth2/oauth2-client.js +0 -263
- package/dist/wirings/oauth2/oauth2-routes.d.ts +0 -35
- package/dist/wirings/oauth2/oauth2-routes.js +0 -146
- package/dist/wirings/scope/wire-scope.d.ts +0 -33
- package/dist/wirings/scope/wire-scope.js +0 -32
- package/dist/wirings/workflow/dsl/index.d.ts +0 -5
- package/dist/wirings/workflow/dsl/index.js +0 -4
- package/dist/wirings/workflow/graph/index.d.ts +0 -5
- package/dist/wirings/workflow/graph/index.js +0 -4
|
@@ -9,8 +9,11 @@ import {
|
|
|
9
9
|
wireMCPPrompt,
|
|
10
10
|
wireMCPResource,
|
|
11
11
|
} from './mcp-runner.js'
|
|
12
|
+
import { addGlobalMiddleware } from '../../middleware-runner.js'
|
|
12
13
|
import { addFunction } from '../../function/function-runner.js'
|
|
13
14
|
import { pikkuState, resetPikkuState } from '../../pikku-state.js'
|
|
15
|
+
import { pikkuMiddleware } from '../../types/core.types.js'
|
|
16
|
+
import { addTagMiddleware } from '../../middleware-runner.js'
|
|
14
17
|
|
|
15
18
|
const logger = {
|
|
16
19
|
debug: () => {},
|
|
@@ -166,6 +169,49 @@ describe('runMCPResource', () => {
|
|
|
166
169
|
},
|
|
167
170
|
})
|
|
168
171
|
})
|
|
172
|
+
|
|
173
|
+
test('runs a templated resource middleware when invoked by concrete uri', async () => {
|
|
174
|
+
let middlewareRan = false
|
|
175
|
+
// A tag-derived middleware group (this is where a tag-based auth gate lands).
|
|
176
|
+
addTagMiddleware('secure', [
|
|
177
|
+
pikkuMiddleware(async (_services, _wire, next) => {
|
|
178
|
+
middlewareRan = true
|
|
179
|
+
await next()
|
|
180
|
+
}),
|
|
181
|
+
])
|
|
182
|
+
pikkuState(null, 'mcp', 'resourcesMeta')['resource://secure/{id}'] = {
|
|
183
|
+
uri: 'resource://secure/{id}',
|
|
184
|
+
title: 'Secure',
|
|
185
|
+
description: 'Secure',
|
|
186
|
+
pikkuFuncId: 'secureResourceFunc',
|
|
187
|
+
inputSchema: null,
|
|
188
|
+
outputSchema: null,
|
|
189
|
+
// The resource's merged middleware metadata. It must resolve for a concrete
|
|
190
|
+
// request against the template, or the resource is reachable with its gate
|
|
191
|
+
// skipped.
|
|
192
|
+
middleware: [{ type: 'tag', tag: 'secure' }],
|
|
193
|
+
} as never
|
|
194
|
+
registerFunction('secureResourceFunc', async () => ({ ok: true }))
|
|
195
|
+
|
|
196
|
+
wireMCPResource({
|
|
197
|
+
uri: 'resource://secure/{id}',
|
|
198
|
+
title: 'Secure',
|
|
199
|
+
description: 'Secure',
|
|
200
|
+
func: { func: async () => ({ ok: true }) } as never,
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
await runMCPResource(
|
|
204
|
+
{ jsonrpc: '2.0', id: 'req-2', params: {} },
|
|
205
|
+
{ mcp: mcpWire as never },
|
|
206
|
+
'resource://secure/99'
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
assert.equal(
|
|
210
|
+
middlewareRan,
|
|
211
|
+
true,
|
|
212
|
+
'the templated resource middleware must run for a concrete uri'
|
|
213
|
+
)
|
|
214
|
+
})
|
|
169
215
|
})
|
|
170
216
|
|
|
171
217
|
describe('runMCPTool', () => {
|
|
@@ -371,6 +417,141 @@ describe('runMCPTool', () => {
|
|
|
371
417
|
})
|
|
372
418
|
})
|
|
373
419
|
|
|
420
|
+
describe("MCP carries the caller's HTTP request", () => {
|
|
421
|
+
// Without this, every auth middleware bails on its first line — each one
|
|
422
|
+
// opens with `if (!http?.request) return`, so an MCP call arrived at the
|
|
423
|
+
// function with no session, and a tool fronting a session-requiring function
|
|
424
|
+
// answered 'Authentication required' to every caller. Nothing else was
|
|
425
|
+
// missing: global middleware already runs for MCP, and the runner already
|
|
426
|
+
// builds a session service. Only the request was being dropped.
|
|
427
|
+
test('exposes http on the wire so session middleware can read it', async () => {
|
|
428
|
+
pikkuState(null, 'mcp', 'toolsMeta').whoami = {
|
|
429
|
+
name: 'whoami',
|
|
430
|
+
title: 'Who am I',
|
|
431
|
+
description: 'Who am I',
|
|
432
|
+
pikkuFuncId: 'whoamiFunc',
|
|
433
|
+
inputSchema: null,
|
|
434
|
+
outputSchema: 'MCPToolResponse',
|
|
435
|
+
} as never
|
|
436
|
+
|
|
437
|
+
let seenAuthorization: string | undefined
|
|
438
|
+
registerFunction('whoamiFunc', async (_services, _data, wire) => {
|
|
439
|
+
seenAuthorization = wire.http?.request?.header('authorization')
|
|
440
|
+
return [{ type: 'text', text: seenAuthorization ?? 'anonymous' }]
|
|
441
|
+
})
|
|
442
|
+
|
|
443
|
+
const http = {
|
|
444
|
+
request: {
|
|
445
|
+
header: (name: string) =>
|
|
446
|
+
name.toLowerCase() === 'authorization'
|
|
447
|
+
? 'Bearer token-abc'
|
|
448
|
+
: undefined,
|
|
449
|
+
},
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const response = await runMCPTool(
|
|
453
|
+
{ jsonrpc: '2.0', id: 'req-http', params: {} },
|
|
454
|
+
{ mcp: mcpWire as never, http: http as never },
|
|
455
|
+
'whoami'
|
|
456
|
+
)
|
|
457
|
+
|
|
458
|
+
assert.equal(seenAuthorization, 'Bearer token-abc')
|
|
459
|
+
assert.deepEqual(response, {
|
|
460
|
+
id: 'req-http',
|
|
461
|
+
result: [{ type: 'text', text: 'Bearer token-abc' }],
|
|
462
|
+
})
|
|
463
|
+
})
|
|
464
|
+
|
|
465
|
+
test('omitting http leaves the wire without one rather than inventing it', async () => {
|
|
466
|
+
pikkuState(null, 'mcp', 'toolsMeta').anon = {
|
|
467
|
+
name: 'anon',
|
|
468
|
+
title: 'Anon',
|
|
469
|
+
description: 'Anon',
|
|
470
|
+
pikkuFuncId: 'anonFunc',
|
|
471
|
+
inputSchema: null,
|
|
472
|
+
outputSchema: 'MCPToolResponse',
|
|
473
|
+
} as never
|
|
474
|
+
|
|
475
|
+
let sawHttp: unknown = 'unset'
|
|
476
|
+
registerFunction('anonFunc', async (_services, _data, wire) => {
|
|
477
|
+
sawHttp = wire.http
|
|
478
|
+
return [{ type: 'text', text: 'ok' }]
|
|
479
|
+
})
|
|
480
|
+
|
|
481
|
+
await runMCPTool(
|
|
482
|
+
{ jsonrpc: '2.0', id: 'req-anon', params: {} },
|
|
483
|
+
{ mcp: mcpWire as never },
|
|
484
|
+
'anon'
|
|
485
|
+
)
|
|
486
|
+
|
|
487
|
+
assert.equal(sawHttp, undefined)
|
|
488
|
+
})
|
|
489
|
+
})
|
|
490
|
+
|
|
491
|
+
describe('MCP tools fronting a session-requiring function', () => {
|
|
492
|
+
// The whole point of carrying the request: this is the template failure,
|
|
493
|
+
// reproduced. `listRoomsTool` and five others front a `pikkuFunc`, which
|
|
494
|
+
// requires a session, and answered 'Authentication required' to every caller
|
|
495
|
+
// because no request ever reached the middleware that would have set one.
|
|
496
|
+
test('receive the session set by global middleware from the request', async () => {
|
|
497
|
+
addGlobalMiddleware([
|
|
498
|
+
async (_services, wire, next) => {
|
|
499
|
+
const header = wire.http?.request?.header('authorization')
|
|
500
|
+
if (header === 'Bearer token-abc') {
|
|
501
|
+
wire.setSession?.({ userId: 'usr_1', name: 'Rafa' } as never)
|
|
502
|
+
}
|
|
503
|
+
await next()
|
|
504
|
+
},
|
|
505
|
+
] as never)
|
|
506
|
+
|
|
507
|
+
pikkuState(null, 'mcp', 'toolsMeta').listRooms = {
|
|
508
|
+
name: 'listRooms',
|
|
509
|
+
title: 'List rooms',
|
|
510
|
+
description: 'List rooms',
|
|
511
|
+
pikkuFuncId: 'listRoomsFunc',
|
|
512
|
+
inputSchema: null,
|
|
513
|
+
outputSchema: 'MCPToolResponse',
|
|
514
|
+
} as never
|
|
515
|
+
|
|
516
|
+
// sessionless: false — the distinction that made these tools fail.
|
|
517
|
+
addFunction(
|
|
518
|
+
'listRoomsFunc',
|
|
519
|
+
{
|
|
520
|
+
func: async (_services: any, _data: any, wire: any) => [
|
|
521
|
+
{ type: 'text', text: wire.session?.userId ?? 'anonymous' },
|
|
522
|
+
],
|
|
523
|
+
} as never,
|
|
524
|
+
null
|
|
525
|
+
)
|
|
526
|
+
pikkuState(null, 'function', 'meta').listRoomsFunc = {
|
|
527
|
+
name: 'listRoomsFunc',
|
|
528
|
+
sessionless: false,
|
|
529
|
+
permissions: [],
|
|
530
|
+
} as never
|
|
531
|
+
|
|
532
|
+
const response = await runMCPTool(
|
|
533
|
+
{ jsonrpc: '2.0', id: 'req-session', params: {} },
|
|
534
|
+
{
|
|
535
|
+
mcp: mcpWire as never,
|
|
536
|
+
http: {
|
|
537
|
+
request: {
|
|
538
|
+
header: (name: string) =>
|
|
539
|
+
name.toLowerCase() === 'authorization'
|
|
540
|
+
? 'Bearer token-abc'
|
|
541
|
+
: undefined,
|
|
542
|
+
},
|
|
543
|
+
} as never,
|
|
544
|
+
},
|
|
545
|
+
'listRooms'
|
|
546
|
+
)
|
|
547
|
+
|
|
548
|
+
assert.deepEqual(response, {
|
|
549
|
+
id: 'req-session',
|
|
550
|
+
result: [{ type: 'text', text: 'usr_1' }],
|
|
551
|
+
})
|
|
552
|
+
})
|
|
553
|
+
})
|
|
554
|
+
|
|
374
555
|
describe('runMCPPrompt', () => {
|
|
375
556
|
test('maps missing prompt registration to not found MCP errors', async () => {
|
|
376
557
|
pikkuState(null, 'mcp', 'promptsMeta').missingPrompt = {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { PikkuRawWire } from '../../types/core.types.js'
|
|
2
|
+
import type { PikkuHTTP } from '../http/http.types.js'
|
|
2
3
|
import type {
|
|
3
4
|
CoreMCPResource,
|
|
4
5
|
CoreMCPPrompt,
|
|
@@ -35,6 +36,20 @@ export class MCPError extends Error {
|
|
|
35
36
|
|
|
36
37
|
export type RunMCPEndpointParams<Tools extends string = any> = {
|
|
37
38
|
mcp?: PikkuMCP<Tools>
|
|
39
|
+
/**
|
|
40
|
+
* The HTTP request the MCP call arrived on, when it arrived over HTTP.
|
|
41
|
+
*
|
|
42
|
+
* Auth middleware reads the session off `wire.http.request` — every
|
|
43
|
+
* implementation opens with `if (!http?.request) return` — so without this an
|
|
44
|
+
* MCP call reaches the function unauthenticated no matter what middleware the
|
|
45
|
+
* app has registered, and a tool fronting a session-requiring function can
|
|
46
|
+
* only ever answer 'Authentication required'.
|
|
47
|
+
*
|
|
48
|
+
* Left undefined for transports that have no request to offer, such as stdio.
|
|
49
|
+
* Those remain anonymous, which is a property of the transport rather than a
|
|
50
|
+
* default chosen here.
|
|
51
|
+
*/
|
|
52
|
+
http?: PikkuHTTP
|
|
38
53
|
/** Defaults to enabled outside production. */
|
|
39
54
|
exposeErrors?: boolean
|
|
40
55
|
}
|
|
@@ -97,6 +112,12 @@ export async function runMCPResource(
|
|
|
97
112
|
const metas = pikkuState(null, 'mcp', 'resourcesMeta')
|
|
98
113
|
const endpoints = pikkuState(null, 'mcp', 'resources')
|
|
99
114
|
|
|
115
|
+
// The key the resource's meta (and its middleware) is stored under. For a
|
|
116
|
+
// templated resource this is the template, not the concrete request URI — so
|
|
117
|
+
// it must be carried forward, or the meta lookup downstream misses and the
|
|
118
|
+
// resource's declared middleware (including auth) is silently skipped.
|
|
119
|
+
let metaKey = uri
|
|
120
|
+
|
|
100
121
|
if (endpoints.has(uri)) {
|
|
101
122
|
endpoint = endpoints.get(uri)
|
|
102
123
|
pikkuFuncId = metas[uri]?.pikkuFuncId
|
|
@@ -113,6 +134,7 @@ export async function runMCPResource(
|
|
|
113
134
|
|
|
114
135
|
if (match) {
|
|
115
136
|
endpoint = value
|
|
137
|
+
metaKey = uriTemplate
|
|
116
138
|
pikkuFuncId = metas[uriTemplate]?.pikkuFuncId
|
|
117
139
|
|
|
118
140
|
for (let i = 0; i < paramNames.length; i++) {
|
|
@@ -134,7 +156,8 @@ export async function runMCPResource(
|
|
|
134
156
|
pikkuFuncId,
|
|
135
157
|
{ ...params, mcp: { ...params.mcp, uri } } as RunMCPEndpointParams<
|
|
136
158
|
keyof CoreMCPResource
|
|
137
|
-
|
|
159
|
+
>,
|
|
160
|
+
metaKey
|
|
138
161
|
)
|
|
139
162
|
}
|
|
140
163
|
|
|
@@ -177,7 +200,11 @@ async function runMCPPikkuFunc(
|
|
|
177
200
|
name: string,
|
|
178
201
|
mcp: CoreMCPResource | CoreMCPPrompt | undefined,
|
|
179
202
|
pikkuFuncId: string | undefined,
|
|
180
|
-
{ mcp: mcpWire, exposeErrors = !isProduction() }: RunMCPEndpointParams
|
|
203
|
+
{ mcp: mcpWire, http, exposeErrors = !isProduction() }: RunMCPEndpointParams,
|
|
204
|
+
// The key the endpoint's meta is stored under. Differs from `name` only for a
|
|
205
|
+
// templated resource, where `name` is the concrete URI and the meta lives
|
|
206
|
+
// under the template. Defaults to `name` for tools and prompts.
|
|
207
|
+
metaKey: string = name
|
|
181
208
|
): Promise<JsonRpcResponse> {
|
|
182
209
|
const singletonServices = getSingletonServices()
|
|
183
210
|
const createWireServices = getCreateWireServices()
|
|
@@ -208,16 +235,19 @@ async function runMCPPikkuFunc(
|
|
|
208
235
|
)
|
|
209
236
|
const wire: PikkuRawWire = {
|
|
210
237
|
mcp: mcpWire,
|
|
238
|
+
// Only when the transport supplied one. Setting `http: undefined`
|
|
239
|
+
// explicitly would be the same to a reader and different to a spread.
|
|
240
|
+
...(http ? { http } : {}),
|
|
211
241
|
...createMiddlewareSessionWireProps(mcpSessionService),
|
|
212
242
|
}
|
|
213
243
|
|
|
214
244
|
let meta: any
|
|
215
245
|
if (type === 'resource') {
|
|
216
|
-
meta = pikkuState(null, 'mcp', 'resourcesMeta')[
|
|
246
|
+
meta = pikkuState(null, 'mcp', 'resourcesMeta')[metaKey]
|
|
217
247
|
} else if (type === 'tool') {
|
|
218
|
-
meta = pikkuState(null, 'mcp', 'toolsMeta')[
|
|
248
|
+
meta = pikkuState(null, 'mcp', 'toolsMeta')[metaKey]
|
|
219
249
|
} else if (type === 'prompt') {
|
|
220
|
-
meta = pikkuState(null, 'mcp', 'promptsMeta')[
|
|
250
|
+
meta = pikkuState(null, 'mcp', 'promptsMeta')[metaKey]
|
|
221
251
|
}
|
|
222
252
|
|
|
223
253
|
let resolvedFuncName = pikkuFuncId
|
|
@@ -10,7 +10,6 @@ export type { RoleVerification } from './validate-personas.js'
|
|
|
10
10
|
export {
|
|
11
11
|
personaEnvironmentErrors,
|
|
12
12
|
personaEnvironmentRefusal,
|
|
13
|
-
resolvePersonaEnvironments,
|
|
14
13
|
} from './persona-environments.js'
|
|
15
14
|
export type {
|
|
16
15
|
PersonaEnvironment,
|
|
@@ -18,14 +17,8 @@ export type {
|
|
|
18
17
|
} from './persona-environments.js'
|
|
19
18
|
export {
|
|
20
19
|
personaEmail,
|
|
21
|
-
personaEmailLabel,
|
|
22
20
|
personaEmails,
|
|
23
21
|
} from './persona-email.js'
|
|
24
|
-
export {
|
|
25
|
-
allowedLinks,
|
|
26
|
-
applyMailboxAllowlist,
|
|
27
|
-
isAllowedSender,
|
|
28
|
-
} from './persona-mailbox.js'
|
|
29
22
|
export type {
|
|
30
23
|
MailboxAllowlist,
|
|
31
24
|
PersonaMailbox,
|
|
@@ -33,7 +26,6 @@ export type {
|
|
|
33
26
|
} from './persona-mailbox.js'
|
|
34
27
|
export type {
|
|
35
28
|
CorePersona,
|
|
36
|
-
CorePersonaAccount,
|
|
37
29
|
CorePersonas,
|
|
38
30
|
PersonaAccountMeta,
|
|
39
31
|
PersonaDefinitions,
|
|
@@ -17,26 +17,12 @@ export type {
|
|
|
17
17
|
export {
|
|
18
18
|
wireQueueWorker,
|
|
19
19
|
runQueueJob,
|
|
20
|
-
getQueueWorkers,
|
|
21
|
-
removeQueueWorker,
|
|
22
20
|
QueueJobDiscardedError,
|
|
23
21
|
QueueJobFailedError,
|
|
24
22
|
} from './queue-runner.js'
|
|
25
|
-
|
|
26
|
-
export {
|
|
27
|
-
QUEUE_IDENTITY_CLAIM_VERSION,
|
|
28
|
-
QUEUE_IDENTITY_INFO,
|
|
29
|
-
QUEUE_IDENTITY_SECRET_NAME,
|
|
30
|
-
getQueueIdentitySecret,
|
|
31
|
-
resolveQueueJobIdentity,
|
|
32
|
-
signQueueIdentity,
|
|
33
|
-
verifyQueueIdentity,
|
|
34
|
-
} from './queue-identity.js'
|
|
35
23
|
export type { QueueIdentityBinding } from './queue-identity.js'
|
|
36
24
|
|
|
37
25
|
export { SignedQueueService } from './signed-queue-service.js'
|
|
38
|
-
|
|
39
|
-
export { validateWorkerConfig } from './validate-worker-config.js'
|
|
40
26
|
export type { QueueConfigMapping } from './validate-worker-config.js'
|
|
41
27
|
|
|
42
28
|
export { registerQueueWorkers } from './register-queue-helper.js'
|
|
@@ -4,6 +4,8 @@ import type {
|
|
|
4
4
|
CoreSingletonServices,
|
|
5
5
|
} from '../../types/core.types.js'
|
|
6
6
|
import type { SecretService } from '../../services/secret-service.js'
|
|
7
|
+
import { ScopedSecretService } from '../../services/scoped-secret-service.js'
|
|
8
|
+
import { ScopedCredentialService } from '../../services/scoped-credential-service.js'
|
|
7
9
|
import type { VariablesService } from '../../services/variables-service.js'
|
|
8
10
|
|
|
9
11
|
export type AddonInstance = {
|
|
@@ -11,6 +13,10 @@ export type AddonInstance = {
|
|
|
11
13
|
secretOverrides?: Record<string, string>
|
|
12
14
|
variableOverrides?: Record<string, string>
|
|
13
15
|
credentialOverrides?: Record<string, string>
|
|
16
|
+
/** Set by the consuming app to opt this instance out of secret scoping. */
|
|
17
|
+
globalSecrets?: string
|
|
18
|
+
/** Set by the consuming app to opt this instance out of credential scoping. */
|
|
19
|
+
globalCredentials?: string
|
|
14
20
|
}
|
|
15
21
|
|
|
16
22
|
const aliasSecretService = (
|
|
@@ -111,9 +117,6 @@ export const getOrCreatePackageSingletonServices = async (
|
|
|
111
117
|
}
|
|
112
118
|
|
|
113
119
|
const factories = pikkuState(packageName, 'package', 'factories')
|
|
114
|
-
if (!factories || !factories.createSingletonServices) {
|
|
115
|
-
return parentServices
|
|
116
|
-
}
|
|
117
120
|
|
|
118
121
|
let existingServices = parentServices
|
|
119
122
|
if (addonInstance?.secretOverrides && parentServices.secrets) {
|
|
@@ -134,6 +137,32 @@ export const getOrCreatePackageSingletonServices = async (
|
|
|
134
137
|
),
|
|
135
138
|
}
|
|
136
139
|
}
|
|
140
|
+
if (!addonInstance?.globalSecrets && existingServices.secrets) {
|
|
141
|
+
existingServices = {
|
|
142
|
+
...existingServices,
|
|
143
|
+
secrets: new ScopedSecretService(
|
|
144
|
+
existingServices.secrets,
|
|
145
|
+
new Set(pikkuState(packageName, 'package', 'declaredSecrets') ?? [])
|
|
146
|
+
),
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (!addonInstance?.globalCredentials && existingServices.credentialService) {
|
|
150
|
+
existingServices = {
|
|
151
|
+
...existingServices,
|
|
152
|
+
credentialService: new ScopedCredentialService(
|
|
153
|
+
existingServices.credentialService,
|
|
154
|
+
new Set(
|
|
155
|
+
Object.keys(
|
|
156
|
+
pikkuState(packageName, 'package', 'credentialsMeta') ?? {}
|
|
157
|
+
)
|
|
158
|
+
)
|
|
159
|
+
),
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (!factories || !factories.createSingletonServices) {
|
|
164
|
+
return existingServices
|
|
165
|
+
}
|
|
137
166
|
|
|
138
167
|
let config: CoreConfig = existingServices.config
|
|
139
168
|
if (factories.createConfig) {
|
|
@@ -173,5 +202,7 @@ export const addonInstanceForNamespace = (
|
|
|
173
202
|
secretOverrides: cfg.secretOverrides,
|
|
174
203
|
variableOverrides: cfg.variableOverrides,
|
|
175
204
|
credentialOverrides: cfg.credentialOverrides,
|
|
205
|
+
globalSecrets: cfg.globalSecrets,
|
|
206
|
+
globalCredentials: cfg.globalCredentials,
|
|
176
207
|
}
|
|
177
208
|
}
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import { beforeEach, describe, test } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
|
|
4
|
+
import { pikkuState, resetPikkuState } from '../../pikku-state.js'
|
|
5
|
+
import { getOrCreatePackageSingletonServices } from './addon-runner.js'
|
|
6
|
+
import { wireAddon } from './wire-addon.js'
|
|
7
|
+
import type { SecretService } from '../../services/secret-service.js'
|
|
8
|
+
import type { CredentialService } from '../../services/credential-service.js'
|
|
9
|
+
import type { CoreSingletonServices } from '../../types/core.types.js'
|
|
10
|
+
|
|
11
|
+
const ADDON_PACKAGE = '@addon/example'
|
|
12
|
+
|
|
13
|
+
const createSecretService = (): SecretService =>
|
|
14
|
+
({
|
|
15
|
+
getSecret: async (key: string) => `value-of-${key}` as never,
|
|
16
|
+
hasSecret: async () => true,
|
|
17
|
+
setSecret: async () => {},
|
|
18
|
+
deleteSecret: async () => {},
|
|
19
|
+
getSecrets: async (keys: string[]) =>
|
|
20
|
+
Object.fromEntries(keys.map((k) => [k, `value-of-${k}`])) as never,
|
|
21
|
+
}) as SecretService
|
|
22
|
+
|
|
23
|
+
const createCredentialService = (): CredentialService =>
|
|
24
|
+
({
|
|
25
|
+
get: async (name: string) => `credential-of-${name}`,
|
|
26
|
+
set: async () => {},
|
|
27
|
+
delete: async () => {},
|
|
28
|
+
has: async () => true,
|
|
29
|
+
getAll: async () => ({ slack: 'a', stripe: 'b' }),
|
|
30
|
+
getUsersWithCredential: async () => ['user-1'],
|
|
31
|
+
getAllUsers: async () => ['user-1'],
|
|
32
|
+
}) as unknown as CredentialService
|
|
33
|
+
|
|
34
|
+
const createParentServices = () =>
|
|
35
|
+
({
|
|
36
|
+
config: {},
|
|
37
|
+
logger: {
|
|
38
|
+
debug: () => {},
|
|
39
|
+
info: () => {},
|
|
40
|
+
warn: () => {},
|
|
41
|
+
error: () => {},
|
|
42
|
+
},
|
|
43
|
+
secrets: createSecretService(),
|
|
44
|
+
credentialService: createCredentialService(),
|
|
45
|
+
}) as unknown as CoreSingletonServices
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Registers an addon package whose singleton factory hands back whatever
|
|
49
|
+
* secrets service the host decided it should see.
|
|
50
|
+
*/
|
|
51
|
+
const registerAddon = (
|
|
52
|
+
declaredSecrets: string[] | null,
|
|
53
|
+
declaredCredentials?: string[]
|
|
54
|
+
) => {
|
|
55
|
+
pikkuState(ADDON_PACKAGE, 'package', 'factories', {
|
|
56
|
+
createSingletonServices: (async (_config: unknown, parent: any) => ({
|
|
57
|
+
...parent,
|
|
58
|
+
secretsSeenByAddon: parent.secrets,
|
|
59
|
+
credentialsSeenByAddon: parent.credentialService,
|
|
60
|
+
})) as never,
|
|
61
|
+
})
|
|
62
|
+
if (declaredSecrets) {
|
|
63
|
+
pikkuState(ADDON_PACKAGE, 'package', 'declaredSecrets', declaredSecrets)
|
|
64
|
+
}
|
|
65
|
+
if (declaredCredentials) {
|
|
66
|
+
pikkuState(
|
|
67
|
+
ADDON_PACKAGE,
|
|
68
|
+
'package',
|
|
69
|
+
'credentialsMeta',
|
|
70
|
+
Object.fromEntries(
|
|
71
|
+
declaredCredentials.map((name) => [
|
|
72
|
+
name,
|
|
73
|
+
{ name, displayName: name, type: 'singleton' },
|
|
74
|
+
])
|
|
75
|
+
)
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const servicesHandedToAddon = async (namespace: string) =>
|
|
81
|
+
(await getOrCreatePackageSingletonServices(
|
|
82
|
+
ADDON_PACKAGE,
|
|
83
|
+
createParentServices(),
|
|
84
|
+
pikkuState(null, 'addons', 'packages').get(namespace) as never
|
|
85
|
+
)) as any
|
|
86
|
+
|
|
87
|
+
const secretsHandedToAddon = async (namespace: string) =>
|
|
88
|
+
(await servicesHandedToAddon(namespace)).secretsSeenByAddon as SecretService
|
|
89
|
+
|
|
90
|
+
const credentialsHandedToAddon = async (namespace: string) =>
|
|
91
|
+
(await servicesHandedToAddon(namespace))
|
|
92
|
+
.credentialsSeenByAddon as CredentialService
|
|
93
|
+
|
|
94
|
+
describe('addon secrets are scoped to what the addon declared', () => {
|
|
95
|
+
beforeEach(() => {
|
|
96
|
+
resetPikkuState()
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
test('a declared secret is readable', async () => {
|
|
100
|
+
registerAddon(['ADDON_KEY'])
|
|
101
|
+
wireAddon({
|
|
102
|
+
name: 'example',
|
|
103
|
+
package: ADDON_PACKAGE,
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
const secrets = await secretsHandedToAddon('example')
|
|
107
|
+
assert.equal(await secrets.getSecret('ADDON_KEY'), 'value-of-ADDON_KEY')
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
test('an undeclared secret is denied', async () => {
|
|
111
|
+
registerAddon(['ADDON_KEY'])
|
|
112
|
+
wireAddon({
|
|
113
|
+
name: 'example',
|
|
114
|
+
package: ADDON_PACKAGE,
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
const secrets = await secretsHandedToAddon('example')
|
|
118
|
+
await assert.rejects(
|
|
119
|
+
() => secrets.getSecret('SOMEONE_ELSES_KEY'),
|
|
120
|
+
/denied/i
|
|
121
|
+
)
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
test('a scoped addon cannot write secrets', async () => {
|
|
125
|
+
registerAddon(['ADDON_KEY'])
|
|
126
|
+
wireAddon({
|
|
127
|
+
name: 'example',
|
|
128
|
+
package: ADDON_PACKAGE,
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
const secrets = await secretsHandedToAddon('example')
|
|
132
|
+
await assert.rejects(() => secrets.setSecret('ADDON_KEY', 'x'))
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
test('an addon that declares nothing is scoped to nothing', async () => {
|
|
136
|
+
registerAddon([])
|
|
137
|
+
wireAddon({
|
|
138
|
+
name: 'example',
|
|
139
|
+
package: ADDON_PACKAGE,
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
const secrets = await secretsHandedToAddon('example')
|
|
143
|
+
await assert.rejects(() => secrets.getSecret('ADDON_KEY'), /denied/i)
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
test('globalSecrets, set by the host, hands over the whole service', async () => {
|
|
147
|
+
registerAddon(['ADDON_KEY'])
|
|
148
|
+
wireAddon({
|
|
149
|
+
name: 'example',
|
|
150
|
+
package: ADDON_PACKAGE,
|
|
151
|
+
globalSecrets: 'administers secrets named by the operator at runtime',
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
const secrets = await secretsHandedToAddon('example')
|
|
155
|
+
assert.equal(
|
|
156
|
+
await secrets.getSecret('SOMEONE_ELSES_KEY'),
|
|
157
|
+
'value-of-SOMEONE_ELSES_KEY'
|
|
158
|
+
)
|
|
159
|
+
await assert.doesNotReject(() => secrets.setSecret('ADDON_KEY', 'x'))
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
test('an addon with no singleton factory is still scoped', async () => {
|
|
163
|
+
pikkuState(ADDON_PACKAGE, 'package', 'declaredSecrets', ['ADDON_KEY'])
|
|
164
|
+
wireAddon({
|
|
165
|
+
name: 'example',
|
|
166
|
+
package: ADDON_PACKAGE,
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
const services = await getOrCreatePackageSingletonServices(
|
|
170
|
+
ADDON_PACKAGE,
|
|
171
|
+
createParentServices(),
|
|
172
|
+
pikkuState(null, 'addons', 'packages').get('example') as never
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
assert.equal(
|
|
176
|
+
await services.secrets.getSecret('ADDON_KEY'),
|
|
177
|
+
'value-of-ADDON_KEY'
|
|
178
|
+
)
|
|
179
|
+
await assert.rejects(
|
|
180
|
+
() => services.secrets.getSecret('SOMEONE_ELSES_KEY'),
|
|
181
|
+
/denied/i
|
|
182
|
+
)
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
test('scoping applies to the key the addon asks for, not the aliased one', async () => {
|
|
186
|
+
registerAddon(['ADDON_KEY'])
|
|
187
|
+
wireAddon({
|
|
188
|
+
name: 'example',
|
|
189
|
+
package: ADDON_PACKAGE,
|
|
190
|
+
secretOverrides: { ADDON_KEY: 'PROD_ADDON_KEY' },
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
const secrets = await secretsHandedToAddon('example')
|
|
194
|
+
assert.equal(
|
|
195
|
+
await secrets.getSecret('ADDON_KEY'),
|
|
196
|
+
'value-of-PROD_ADDON_KEY'
|
|
197
|
+
)
|
|
198
|
+
await assert.rejects(() => secrets.getSecret('PROD_ADDON_KEY'), /denied/i)
|
|
199
|
+
})
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
describe('addon credentials are scoped to what the addon declared', () => {
|
|
203
|
+
beforeEach(() => {
|
|
204
|
+
resetPikkuState()
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
test('a declared credential is reachable', async () => {
|
|
208
|
+
registerAddon(['ADDON_KEY'], ['slack'])
|
|
209
|
+
wireAddon({ name: 'example', package: ADDON_PACKAGE })
|
|
210
|
+
|
|
211
|
+
const credentials = await credentialsHandedToAddon('example')
|
|
212
|
+
assert.equal(await credentials.get('slack'), 'credential-of-slack')
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
test('an undeclared credential is denied', async () => {
|
|
216
|
+
registerAddon(['ADDON_KEY'], ['slack'])
|
|
217
|
+
wireAddon({ name: 'example', package: ADDON_PACKAGE })
|
|
218
|
+
|
|
219
|
+
const credentials = await credentialsHandedToAddon('example')
|
|
220
|
+
await assert.rejects(() => credentials.get('stripe'), /denied/i)
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
test('a scoped addon cannot enumerate the app users', async () => {
|
|
224
|
+
registerAddon(['ADDON_KEY'], ['slack'])
|
|
225
|
+
wireAddon({ name: 'example', package: ADDON_PACKAGE })
|
|
226
|
+
|
|
227
|
+
const credentials = await credentialsHandedToAddon('example')
|
|
228
|
+
await assert.rejects(() => credentials.getAllUsers(), /denied/i)
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
test('an addon with no singleton factory is still scoped', async () => {
|
|
232
|
+
pikkuState(ADDON_PACKAGE, 'package', 'credentialsMeta', {
|
|
233
|
+
slack: { name: 'slack', displayName: 'slack', type: 'singleton' },
|
|
234
|
+
} as never)
|
|
235
|
+
wireAddon({ name: 'example', package: ADDON_PACKAGE })
|
|
236
|
+
|
|
237
|
+
const services = await getOrCreatePackageSingletonServices(
|
|
238
|
+
ADDON_PACKAGE,
|
|
239
|
+
createParentServices(),
|
|
240
|
+
pikkuState(null, 'addons', 'packages').get('example') as never
|
|
241
|
+
)
|
|
242
|
+
const credentials = services.credentialService as CredentialService
|
|
243
|
+
|
|
244
|
+
assert.equal(await credentials.get('slack'), 'credential-of-slack')
|
|
245
|
+
await assert.rejects(() => credentials.get('stripe'), /denied/i)
|
|
246
|
+
await assert.rejects(() => credentials.getAllUsers(), /denied/i)
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
test('globalCredentials, set by the host, hands over the whole service', async () => {
|
|
250
|
+
registerAddon(['ADDON_KEY'], ['slack'])
|
|
251
|
+
wireAddon({
|
|
252
|
+
name: 'example',
|
|
253
|
+
package: ADDON_PACKAGE,
|
|
254
|
+
globalCredentials: 'links credentials an operator names at runtime',
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
const credentials = await credentialsHandedToAddon('example')
|
|
258
|
+
assert.equal(await credentials.get('stripe'), 'credential-of-stripe')
|
|
259
|
+
assert.deepEqual(await credentials.getAllUsers(), ['user-1'])
|
|
260
|
+
})
|
|
261
|
+
})
|
|
@@ -708,6 +708,7 @@ describe('multi-instance addons', () => {
|
|
|
708
708
|
return svc
|
|
709
709
|
},
|
|
710
710
|
} as never)
|
|
711
|
+
pikkuState('@addon/slack', 'package', 'declaredSecrets', ['slackToken'])
|
|
711
712
|
registerFunction(
|
|
712
713
|
'send',
|
|
713
714
|
async (services: any) => ({ token: services.token }),
|
|
@@ -753,6 +754,7 @@ describe('multi-instance addons', () => {
|
|
|
753
754
|
}
|
|
754
755
|
},
|
|
755
756
|
} as never)
|
|
757
|
+
pikkuState('@addon/slack', 'package', 'declaredSecrets', ['slackToken'])
|
|
756
758
|
|
|
757
759
|
registerFunction(
|
|
758
760
|
'leaf',
|