@pikku/mongodb 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 +88 -0
- package/dist/src/mongodb-ai-storage-service.d.ts +1 -1
- package/dist/src/mongodb-ai-storage-service.js +3 -1
- package/dist/src/mongodb-secret-service.d.ts +4 -3
- package/dist/src/mongodb-secret-service.js +6 -3
- package/dist/src/mongodb-workflow-service.js +3 -0
- package/package.json +2 -2
- package/src/mongodb-ai-storage-service.ts +5 -3
- package/src/mongodb-secret-service.ts +11 -10
- package/src/mongodb-services.test.ts +4 -2
- package/src/mongodb-workflow-service.ts +3 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,93 @@
|
|
|
1
1
|
# @pikku/mongodb
|
|
2
2
|
|
|
3
|
+
## 0.12.15
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 6dada45: fix(workflow,ai-agent): make a run's owner, entry node and step function authoritative
|
|
8
|
+
|
|
9
|
+
A graph run may only start at a node the graph declared in `meta.entryNodeIds`, and
|
|
10
|
+
the generated `POST /workflow/:workflowName/graph/:nodeId` route that let an HTTP
|
|
11
|
+
caller pick the entry node is gone. `startNode` stays for `PikkuTriggerService`,
|
|
12
|
+
which names a declared entry node anyway.
|
|
13
|
+
|
|
14
|
+
`StepState` now records the `rpcName` the workflow dispatched a step with, and the
|
|
15
|
+
step claim rejects a queue message naming a different function with
|
|
16
|
+
`WorkflowStepFunctionMismatchError` before mutating any status — a step runs under
|
|
17
|
+
the run owner's identity and without the `expose` gate, so the message must not
|
|
18
|
+
choose what runs.
|
|
19
|
+
|
|
20
|
+
`approveStep` takes the caller's session, and the generated status routes and
|
|
21
|
+
streams assert the same `assertWorkflowRunOwner` check: a run started through a
|
|
22
|
+
session may only be read and approved by that session's user. A run with no
|
|
23
|
+
recorded owner (trigger, scheduler, unauthenticated route) has nobody to compare
|
|
24
|
+
against and is still gated by the entrypoint's own `auth`/`permissions`.
|
|
25
|
+
|
|
26
|
+
`AIRunStateService.resolveApproval` is now a compare-and-swap returning whether
|
|
27
|
+
_this_ caller made the claim, and both agent resume paths run a tool only for the
|
|
28
|
+
approvals they claimed — concurrent approvals of one tool call no longer all
|
|
29
|
+
execute it.
|
|
30
|
+
|
|
31
|
+
- Updated dependencies [41c1a95]
|
|
32
|
+
- Updated dependencies [ce96383]
|
|
33
|
+
- Updated dependencies [7e60867]
|
|
34
|
+
- Updated dependencies [f8f1244]
|
|
35
|
+
- Updated dependencies [dcf20cb]
|
|
36
|
+
- Updated dependencies [6512384]
|
|
37
|
+
- Updated dependencies [e3b4c14]
|
|
38
|
+
- Updated dependencies [efd0ed1]
|
|
39
|
+
- Updated dependencies [cba98fb]
|
|
40
|
+
- Updated dependencies [ce96383]
|
|
41
|
+
- Updated dependencies [f8f1244]
|
|
42
|
+
- Updated dependencies [f8f1244]
|
|
43
|
+
- Updated dependencies [6e93a35]
|
|
44
|
+
- Updated dependencies [6dada45]
|
|
45
|
+
- @pikku/core@0.12.80
|
|
46
|
+
|
|
47
|
+
## 0.12.14
|
|
48
|
+
|
|
49
|
+
### Patch Changes
|
|
50
|
+
|
|
51
|
+
- 78b29f0: `SecretService` now returns a `SecretValue<T>` rather than the bare value, so a
|
|
52
|
+
vault secret cannot reach a sink by accident.
|
|
53
|
+
|
|
54
|
+
`SecretValue` is nominally typed, which means it is not assignable to `string`
|
|
55
|
+
(or to any other concretely-typed field). Every sink with a real type — a
|
|
56
|
+
database column, an email body, a session payload — rejects it with no lint
|
|
57
|
+
rule involved. The sinks typed `any`, `unknown`, or a free generic — the logger,
|
|
58
|
+
queue payloads, webhook and email inputs, and a function's own output — are
|
|
59
|
+
guarded with `Safe<T>`, which collapses a `SecretValue` found anywhere inside
|
|
60
|
+
`T`, however deeply nested, to `never`.
|
|
61
|
+
|
|
62
|
+
Unwrap deliberately at the point the secret reaches the wire:
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
const secret = await secrets.getSecret('BETTER_AUTH_SECRET')
|
|
66
|
+
betterAuth({ secret: secret.reveal() })
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Two behaviours cover what types cannot see. Structured serialization redacts —
|
|
70
|
+
`JSON.stringify` and node's inspect both yield `[secret]`, so an audit or log
|
|
71
|
+
write stays honest without crashing the request. String coercion throws
|
|
72
|
+
`SecretCoercionError`, because a template literal is always a leak.
|
|
73
|
+
|
|
74
|
+
`AuditLog.write` is guarded the same way as the logger, since an audit event
|
|
75
|
+
carries `input` and `metadata` as `unknown` and nominality alone cannot stop a
|
|
76
|
+
secret landing in one.
|
|
77
|
+
|
|
78
|
+
`.reveal()` is the deliberate escape hatch, and what it hands back is an
|
|
79
|
+
ordinary string as far as every sink signature is concerned. **PKU953** closes
|
|
80
|
+
that gap: under `pikku all --security` the inspector reports a revealed secret
|
|
81
|
+
that flows into a logger, an audit, a queue, an email or a webhook — `console` included.
|
|
82
|
+
|
|
83
|
+
This also fixed a real one: `remote-addon-auth.ts` called `String(token)` on an
|
|
84
|
+
`unknown` and wrote the result straight into an `Authorization` header.
|
|
85
|
+
|
|
86
|
+
- Updated dependencies [62ea4cc]
|
|
87
|
+
- Updated dependencies [9dddff8]
|
|
88
|
+
- Updated dependencies [78b29f0]
|
|
89
|
+
- @pikku/core@0.12.76
|
|
90
|
+
|
|
3
91
|
## 0.12.13
|
|
4
92
|
|
|
5
93
|
### Patch Changes
|
|
@@ -36,6 +36,6 @@ export declare class MongoDBAIStorageService implements AIStorageService, AIRunS
|
|
|
36
36
|
run: AgentRunState;
|
|
37
37
|
approval: NonNullable<AgentRunState['pendingApprovals']>[number];
|
|
38
38
|
} | null>;
|
|
39
|
-
resolveApproval(toolCallId: string, status: 'approved' | 'denied'): Promise<
|
|
39
|
+
resolveApproval(toolCallId: string, status: 'approved' | 'denied'): Promise<boolean>;
|
|
40
40
|
close(): Promise<void>;
|
|
41
41
|
}
|
|
@@ -402,7 +402,9 @@ export class MongoDBAIStorageService {
|
|
|
402
402
|
return { run, approval };
|
|
403
403
|
}
|
|
404
404
|
async resolveApproval(toolCallId, status) {
|
|
405
|
-
|
|
405
|
+
// Only the caller that moves the tool call off 'pending' has claimed it.
|
|
406
|
+
const result = await this.toolCalls.updateOne({ _id: toolCallId, approvalStatus: 'pending' }, { $set: { approvalStatus: status } });
|
|
407
|
+
return result.modifiedCount > 0;
|
|
406
408
|
}
|
|
407
409
|
async close() { }
|
|
408
410
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type SecretValue } from '@pikku/core';
|
|
2
|
+
import type { SecretService, SecretValues } from '@pikku/core/services';
|
|
2
3
|
import type { Db } from 'mongodb';
|
|
3
4
|
export interface MongoDBSecretServiceConfig {
|
|
4
5
|
key: string;
|
|
@@ -25,10 +26,10 @@ export declare class MongoDBSecretService implements SecretService {
|
|
|
25
26
|
private logAudit;
|
|
26
27
|
private getKEKSalt;
|
|
27
28
|
private getKEK;
|
|
28
|
-
getSecret<T = string>(key: string): Promise<T
|
|
29
|
+
getSecret<T = string>(key: string): Promise<SecretValue<T>>;
|
|
29
30
|
hasSecret(key: string): Promise<boolean>;
|
|
30
31
|
setSecret(key: string, value: unknown): Promise<void>;
|
|
31
32
|
deleteSecret(key: string): Promise<void>;
|
|
32
|
-
getSecrets<T extends Record<string, unknown> = Record<string, unknown>>(keys: (keyof T & string)[]): Promise<T
|
|
33
|
+
getSecrets<T extends Record<string, unknown> = Record<string, unknown>>(keys: (keyof T & string)[]): Promise<Partial<SecretValues<T>>>;
|
|
33
34
|
rotateKEK(): Promise<number>;
|
|
34
35
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createSecretValue, isSecretValue } from '@pikku/core';
|
|
1
2
|
import { deriveKEK, generateKEKSalt, envelopeEncrypt, envelopeDecrypt, envelopeRewrap, } from '@pikku/core/crypto-utils';
|
|
2
3
|
export class MongoDBSecretService {
|
|
3
4
|
db;
|
|
@@ -74,14 +75,16 @@ export class MongoDBSecretService {
|
|
|
74
75
|
const kek = await this.getKEK(row.keyVersion);
|
|
75
76
|
const result = await envelopeDecrypt(kek, row.ciphertext, row.wrappedDek);
|
|
76
77
|
await this.logAudit(key, 'read');
|
|
77
|
-
return result;
|
|
78
|
+
return createSecretValue(result);
|
|
78
79
|
}
|
|
79
80
|
async hasSecret(key) {
|
|
80
81
|
const count = await this.secrets.countDocuments({ _id: key }, { limit: 1 });
|
|
81
82
|
return count > 0;
|
|
82
83
|
}
|
|
83
84
|
async setSecret(key, value) {
|
|
84
|
-
|
|
85
|
+
// Encrypting the wrapper would store its redaction, not the secret —
|
|
86
|
+
// writing a secret back to the vault is exactly what this method is for.
|
|
87
|
+
const { ciphertext, wrappedDEK } = await envelopeEncrypt(await this.getKEK(this.keyVersion), isSecretValue(value) ? value.reveal() : value);
|
|
85
88
|
const now = new Date();
|
|
86
89
|
await this.secrets.updateOne({ _id: key }, {
|
|
87
90
|
$set: {
|
|
@@ -109,7 +112,7 @@ export class MongoDBSecretService {
|
|
|
109
112
|
for (const row of rows) {
|
|
110
113
|
try {
|
|
111
114
|
const kek = await this.getKEK(row.keyVersion);
|
|
112
|
-
out[row._id] = await envelopeDecrypt(kek, row.ciphertext, row.wrappedDek);
|
|
115
|
+
out[row._id] = createSecretValue(await envelopeDecrypt(kek, row.ciphertext, row.wrappedDek));
|
|
113
116
|
}
|
|
114
117
|
catch (cause) {
|
|
115
118
|
throw new Error(`Failed to decrypt secret "${row._id}" (key_version ${row.keyVersion}): ` +
|
|
@@ -85,6 +85,7 @@ export class MongoDBWorkflowService extends PikkuWorkflowService {
|
|
|
85
85
|
return {
|
|
86
86
|
stepId,
|
|
87
87
|
status: 'pending',
|
|
88
|
+
rpcName,
|
|
88
89
|
result: undefined,
|
|
89
90
|
error: undefined,
|
|
90
91
|
attemptCount: 1,
|
|
@@ -109,6 +110,7 @@ export class MongoDBWorkflowService extends PikkuWorkflowService {
|
|
|
109
110
|
return {
|
|
110
111
|
stepId: row._id,
|
|
111
112
|
status: row.status,
|
|
113
|
+
rpcName: row.rpcName ?? null,
|
|
112
114
|
result: row.result ?? undefined,
|
|
113
115
|
error: row.error ?? undefined,
|
|
114
116
|
attemptCount,
|
|
@@ -238,6 +240,7 @@ export class MongoDBWorkflowService extends PikkuWorkflowService {
|
|
|
238
240
|
return {
|
|
239
241
|
stepId: row._id,
|
|
240
242
|
status: row.status,
|
|
243
|
+
rpcName: row.rpcName ?? null,
|
|
241
244
|
result: row.result ?? undefined,
|
|
242
245
|
error: row.error ?? undefined,
|
|
243
246
|
attemptCount,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pikku/mongodb",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.15",
|
|
4
4
|
"description": "MongoDB services for Pikku — workflow state, agent runs, secrets and channel stores",
|
|
5
5
|
"author": "yasser.fadl@gmail.com",
|
|
6
6
|
"license": "MIT",
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"prepublishOnly": "yarn build"
|
|
19
19
|
},
|
|
20
20
|
"peerDependencies": {
|
|
21
|
-
"@pikku/core": "^0.12.
|
|
21
|
+
"@pikku/core": "^0.12.80"
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
24
|
"mongodb": "^6.12.0"
|
|
@@ -565,11 +565,13 @@ export class MongoDBAIStorageService
|
|
|
565
565
|
async resolveApproval(
|
|
566
566
|
toolCallId: string,
|
|
567
567
|
status: 'approved' | 'denied'
|
|
568
|
-
): Promise<
|
|
569
|
-
|
|
570
|
-
|
|
568
|
+
): Promise<boolean> {
|
|
569
|
+
// Only the caller that moves the tool call off 'pending' has claimed it.
|
|
570
|
+
const result = await this.toolCalls.updateOne(
|
|
571
|
+
{ _id: toolCallId, approvalStatus: 'pending' },
|
|
571
572
|
{ $set: { approvalStatus: status } }
|
|
572
573
|
)
|
|
574
|
+
return result.modifiedCount > 0
|
|
573
575
|
}
|
|
574
576
|
|
|
575
577
|
public async close(): Promise<void> {}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { createSecretValue, isSecretValue, type SecretValue } from '@pikku/core'
|
|
2
|
+
import type { SecretService, SecretValues } from '@pikku/core/services'
|
|
2
3
|
import type { Db, Collection } from 'mongodb'
|
|
3
4
|
import {
|
|
4
5
|
deriveKEK,
|
|
@@ -124,14 +125,14 @@ export class MongoDBSecretService implements SecretService {
|
|
|
124
125
|
return kek
|
|
125
126
|
}
|
|
126
127
|
|
|
127
|
-
async getSecret<T = string>(key: string): Promise<T
|
|
128
|
+
async getSecret<T = string>(key: string): Promise<SecretValue<T>> {
|
|
128
129
|
const row = await this.secrets.findOne({ _id: key })
|
|
129
130
|
if (!row) throw new Error('Requested secret not found')
|
|
130
131
|
|
|
131
132
|
const kek = await this.getKEK(row.keyVersion)
|
|
132
133
|
const result = await envelopeDecrypt<T>(kek, row.ciphertext, row.wrappedDek)
|
|
133
134
|
await this.logAudit(key, 'read')
|
|
134
|
-
return result
|
|
135
|
+
return createSecretValue(result)
|
|
135
136
|
}
|
|
136
137
|
|
|
137
138
|
async hasSecret(key: string): Promise<boolean> {
|
|
@@ -140,9 +141,11 @@ export class MongoDBSecretService implements SecretService {
|
|
|
140
141
|
}
|
|
141
142
|
|
|
142
143
|
async setSecret(key: string, value: unknown): Promise<void> {
|
|
144
|
+
// Encrypting the wrapper would store its redaction, not the secret —
|
|
145
|
+
// writing a secret back to the vault is exactly what this method is for.
|
|
143
146
|
const { ciphertext, wrappedDEK } = await envelopeEncrypt(
|
|
144
147
|
await this.getKEK(this.keyVersion),
|
|
145
|
-
value
|
|
148
|
+
isSecretValue(value) ? value.reveal() : value
|
|
146
149
|
)
|
|
147
150
|
const now = new Date()
|
|
148
151
|
|
|
@@ -173,7 +176,7 @@ export class MongoDBSecretService implements SecretService {
|
|
|
173
176
|
|
|
174
177
|
async getSecrets<T extends Record<string, unknown> = Record<string, unknown>>(
|
|
175
178
|
keys: (keyof T & string)[]
|
|
176
|
-
): Promise<T
|
|
179
|
+
): Promise<Partial<SecretValues<T>>> {
|
|
177
180
|
const rows = await this.secrets
|
|
178
181
|
.find({ _id: { $in: keys } } as any)
|
|
179
182
|
.toArray()
|
|
@@ -181,10 +184,8 @@ export class MongoDBSecretService implements SecretService {
|
|
|
181
184
|
for (const row of rows) {
|
|
182
185
|
try {
|
|
183
186
|
const kek = await this.getKEK(row.keyVersion)
|
|
184
|
-
out[row._id] =
|
|
185
|
-
kek,
|
|
186
|
-
row.ciphertext,
|
|
187
|
-
row.wrappedDek
|
|
187
|
+
out[row._id] = createSecretValue(
|
|
188
|
+
await envelopeDecrypt(kek, row.ciphertext, row.wrappedDek)
|
|
188
189
|
)
|
|
189
190
|
} catch (cause) {
|
|
190
191
|
throw new Error(
|
|
@@ -194,7 +195,7 @@ export class MongoDBSecretService implements SecretService {
|
|
|
194
195
|
)
|
|
195
196
|
}
|
|
196
197
|
}
|
|
197
|
-
return out as T
|
|
198
|
+
return out as Partial<SecretValues<T>>
|
|
198
199
|
}
|
|
199
200
|
|
|
200
201
|
async rotateKEK(): Promise<number> {
|
|
@@ -171,7 +171,8 @@ function registerTests(name: string, getDb: () => Db) {
|
|
|
171
171
|
'batch-b': string
|
|
172
172
|
}>(['batch-a', 'batch-b'])
|
|
173
173
|
|
|
174
|
-
assert.deepEqual(out
|
|
174
|
+
assert.deepEqual(out['batch-a']!.reveal(), { v: 1 })
|
|
175
|
+
assert.equal(out['batch-b']!.reveal(), 'two')
|
|
175
176
|
})
|
|
176
177
|
|
|
177
178
|
test('getSecrets omits keys that were never stored', async () => {
|
|
@@ -181,7 +182,8 @@ function registerTests(name: string, getDb: () => Db) {
|
|
|
181
182
|
|
|
182
183
|
const out = await service.getSecrets(['present', 'never-stored'])
|
|
183
184
|
|
|
184
|
-
assert.deepEqual(out,
|
|
185
|
+
assert.deepEqual(Object.keys(out), ['present'])
|
|
186
|
+
assert.equal(out.present!.reveal(), 'here')
|
|
185
187
|
})
|
|
186
188
|
})
|
|
187
189
|
}
|
|
@@ -203,6 +203,7 @@ export class MongoDBWorkflowService extends PikkuWorkflowService {
|
|
|
203
203
|
return {
|
|
204
204
|
stepId,
|
|
205
205
|
status: 'pending',
|
|
206
|
+
rpcName,
|
|
206
207
|
result: undefined,
|
|
207
208
|
error: undefined,
|
|
208
209
|
attemptCount: 1,
|
|
@@ -233,6 +234,7 @@ export class MongoDBWorkflowService extends PikkuWorkflowService {
|
|
|
233
234
|
return {
|
|
234
235
|
stepId: row._id,
|
|
235
236
|
status: row.status as StepState['status'],
|
|
237
|
+
rpcName: row.rpcName ?? null,
|
|
236
238
|
result: row.result ?? undefined,
|
|
237
239
|
error: row.error ?? undefined,
|
|
238
240
|
attemptCount,
|
|
@@ -426,6 +428,7 @@ export class MongoDBWorkflowService extends PikkuWorkflowService {
|
|
|
426
428
|
return {
|
|
427
429
|
stepId: row._id,
|
|
428
430
|
status: row.status as StepState['status'],
|
|
431
|
+
rpcName: row.rpcName ?? null,
|
|
429
432
|
result: row.result ?? undefined,
|
|
430
433
|
error: row.error ?? undefined,
|
|
431
434
|
attemptCount,
|