@vellumai/credential-executor 0.4.55
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/Dockerfile +55 -0
- package/bun.lock +37 -0
- package/package.json +32 -0
- package/src/__tests__/command-executor.test.ts +1333 -0
- package/src/__tests__/command-validator.test.ts +708 -0
- package/src/__tests__/command-workspace.test.ts +997 -0
- package/src/__tests__/grant-store.test.ts +467 -0
- package/src/__tests__/http-executor.test.ts +1251 -0
- package/src/__tests__/http-policy.test.ts +970 -0
- package/src/__tests__/local-materializers.test.ts +826 -0
- package/src/__tests__/managed-materializers.test.ts +961 -0
- package/src/__tests__/toolstore.test.ts +539 -0
- package/src/__tests__/transport.test.ts +388 -0
- package/src/audit/store.ts +188 -0
- package/src/commands/auth-adapters.ts +169 -0
- package/src/commands/executor.ts +840 -0
- package/src/commands/output-scan.ts +157 -0
- package/src/commands/profiles.ts +282 -0
- package/src/commands/validator.ts +438 -0
- package/src/commands/workspace.ts +512 -0
- package/src/grants/index.ts +17 -0
- package/src/grants/persistent-store.ts +247 -0
- package/src/grants/rpc-handlers.ts +269 -0
- package/src/grants/temporary-store.ts +219 -0
- package/src/http/audit.ts +84 -0
- package/src/http/executor.ts +540 -0
- package/src/http/path-template.ts +179 -0
- package/src/http/policy.ts +256 -0
- package/src/http/response-filter.ts +233 -0
- package/src/index.ts +106 -0
- package/src/main.ts +263 -0
- package/src/managed-main.ts +420 -0
- package/src/materializers/local.ts +300 -0
- package/src/materializers/managed-platform.ts +270 -0
- package/src/paths.ts +137 -0
- package/src/server.ts +636 -0
- package/src/subjects/local.ts +177 -0
- package/src/subjects/managed.ts +290 -0
- package/src/toolstore/integrity.ts +94 -0
- package/src/toolstore/manifest.ts +154 -0
- package/src/toolstore/publish.ts +342 -0
- package/tsconfig.json +20 -0
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CES persistent grant store.
|
|
3
|
+
*
|
|
4
|
+
* Stores canonical validated grants by stable ID in a `grants.json` file
|
|
5
|
+
* inside the CES-private data root. This file is never co-mingled with
|
|
6
|
+
* assistant trust files or credential metadata.
|
|
7
|
+
*
|
|
8
|
+
* Design principles:
|
|
9
|
+
* - **Fail closed**: If the store file is unreadable or malformed, all
|
|
10
|
+
* reads return empty and all writes throw. The CES must never fall back
|
|
11
|
+
* to a permissive default when the persistent state is corrupt.
|
|
12
|
+
* - **Atomic writes**: Uses rename-over-tmp to prevent partial writes.
|
|
13
|
+
* - **Deduplication**: Grants are keyed by a canonical hash (the `id`
|
|
14
|
+
* field) — adding a grant with an existing ID is a no-op.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
chmodSync,
|
|
19
|
+
existsSync,
|
|
20
|
+
mkdirSync,
|
|
21
|
+
readFileSync,
|
|
22
|
+
renameSync,
|
|
23
|
+
writeFileSync,
|
|
24
|
+
} from "node:fs";
|
|
25
|
+
import { dirname, join } from "node:path";
|
|
26
|
+
import { randomUUID } from "node:crypto";
|
|
27
|
+
|
|
28
|
+
import { getCesGrantsDir } from "../paths.js";
|
|
29
|
+
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// Types
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
/** A canonical persistent grant stored on disk. */
|
|
35
|
+
export interface PersistentGrant {
|
|
36
|
+
/** Stable canonical hash identifying this grant. */
|
|
37
|
+
id: string;
|
|
38
|
+
/** The tool or command pattern this grant authorises. */
|
|
39
|
+
tool: string;
|
|
40
|
+
/** Glob pattern scoping the grant. */
|
|
41
|
+
pattern: string;
|
|
42
|
+
/** Scope constraint (directory path or "everywhere"). */
|
|
43
|
+
scope: string;
|
|
44
|
+
/** When the grant was created (epoch ms). */
|
|
45
|
+
createdAt: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** On-disk format for the grants file. */
|
|
49
|
+
interface GrantsFile {
|
|
50
|
+
version: number;
|
|
51
|
+
grants: PersistentGrant[];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const GRANTS_FILE_VERSION = 1;
|
|
55
|
+
const GRANTS_FILENAME = "grants.json";
|
|
56
|
+
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
// Store implementation
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
|
|
61
|
+
export class PersistentGrantStore {
|
|
62
|
+
private readonly filePath: string;
|
|
63
|
+
private cache: PersistentGrant[] | null = null;
|
|
64
|
+
/** Set to true when the store detects corruption; blocks all operations. */
|
|
65
|
+
private corrupt = false;
|
|
66
|
+
|
|
67
|
+
constructor(grantsDir?: string) {
|
|
68
|
+
const dir = grantsDir ?? getCesGrantsDir();
|
|
69
|
+
this.filePath = join(dir, GRANTS_FILENAME);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// -----------------------------------------------------------------------
|
|
73
|
+
// Public API
|
|
74
|
+
// -----------------------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Initialise the store, ensuring the parent directory exists and the
|
|
78
|
+
* grants file is readable. If the file does not exist it is created
|
|
79
|
+
* with an empty grant list.
|
|
80
|
+
*
|
|
81
|
+
* Throws if the directory cannot be created or an existing file is
|
|
82
|
+
* malformed (fail-closed).
|
|
83
|
+
*/
|
|
84
|
+
init(): void {
|
|
85
|
+
const dir = dirname(this.filePath);
|
|
86
|
+
if (!existsSync(dir)) {
|
|
87
|
+
mkdirSync(dir, { recursive: true });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (!existsSync(this.filePath)) {
|
|
91
|
+
this.writeToDisk([]);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Validate the existing file is readable and well-formed.
|
|
96
|
+
// If it isn't, mark corrupt and throw (fail closed).
|
|
97
|
+
this.loadFromDisk();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Return all persisted grants.
|
|
102
|
+
*
|
|
103
|
+
* Returns an empty array if the store has never been initialised
|
|
104
|
+
* (no file on disk). Throws if the store is corrupt.
|
|
105
|
+
*/
|
|
106
|
+
getAll(): PersistentGrant[] {
|
|
107
|
+
this.assertNotCorrupt();
|
|
108
|
+
if (this.cache !== null) return [...this.cache];
|
|
109
|
+
if (!existsSync(this.filePath)) return [];
|
|
110
|
+
return [...this.loadFromDisk()];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Look up a grant by its canonical ID.
|
|
115
|
+
*
|
|
116
|
+
* Returns `undefined` if not found. Throws if the store is corrupt.
|
|
117
|
+
*/
|
|
118
|
+
getById(id: string): PersistentGrant | undefined {
|
|
119
|
+
return this.getAll().find((g) => g.id === id);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Add a grant. If a grant with the same `id` already exists, this is
|
|
124
|
+
* a no-op (idempotent deduplication by canonical hash).
|
|
125
|
+
*
|
|
126
|
+
* Returns `true` if the grant was newly added, `false` if it was a
|
|
127
|
+
* duplicate.
|
|
128
|
+
*/
|
|
129
|
+
add(grant: PersistentGrant): boolean {
|
|
130
|
+
this.assertNotCorrupt();
|
|
131
|
+
const grants = this.loadFromDisk();
|
|
132
|
+
if (grants.some((g) => g.id === grant.id)) {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
grants.push(grant);
|
|
136
|
+
this.writeToDisk(grants);
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Remove a grant by its canonical ID.
|
|
142
|
+
*
|
|
143
|
+
* Returns `true` if the grant was found and removed, `false` otherwise.
|
|
144
|
+
*/
|
|
145
|
+
remove(id: string): boolean {
|
|
146
|
+
this.assertNotCorrupt();
|
|
147
|
+
const grants = this.loadFromDisk();
|
|
148
|
+
const index = grants.findIndex((g) => g.id === id);
|
|
149
|
+
if (index === -1) return false;
|
|
150
|
+
grants.splice(index, 1);
|
|
151
|
+
this.writeToDisk(grants);
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Check whether a grant with the given ID exists.
|
|
157
|
+
*/
|
|
158
|
+
has(id: string): boolean {
|
|
159
|
+
return this.getById(id) !== undefined;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Remove all grants and reset the store to an empty state.
|
|
164
|
+
*/
|
|
165
|
+
clear(): void {
|
|
166
|
+
this.assertNotCorrupt();
|
|
167
|
+
this.writeToDisk([]);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// -----------------------------------------------------------------------
|
|
171
|
+
// Internals
|
|
172
|
+
// -----------------------------------------------------------------------
|
|
173
|
+
|
|
174
|
+
private assertNotCorrupt(): void {
|
|
175
|
+
if (this.corrupt) {
|
|
176
|
+
throw new Error(
|
|
177
|
+
"CES persistent grant store is corrupt — refusing to operate (fail closed)",
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
private loadFromDisk(): PersistentGrant[] {
|
|
183
|
+
try {
|
|
184
|
+
const raw = readFileSync(this.filePath, "utf-8");
|
|
185
|
+
const data = JSON.parse(raw) as unknown;
|
|
186
|
+
|
|
187
|
+
if (
|
|
188
|
+
typeof data !== "object" ||
|
|
189
|
+
data === null ||
|
|
190
|
+
!("version" in data) ||
|
|
191
|
+
!("grants" in data)
|
|
192
|
+
) {
|
|
193
|
+
this.corrupt = true;
|
|
194
|
+
throw new Error(
|
|
195
|
+
"CES grants file is malformed: missing version or grants field",
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const file = data as GrantsFile;
|
|
200
|
+
|
|
201
|
+
if (file.version !== GRANTS_FILE_VERSION) {
|
|
202
|
+
this.corrupt = true;
|
|
203
|
+
throw new Error(
|
|
204
|
+
`CES grants file has unsupported version ${file.version} (expected ${GRANTS_FILE_VERSION})`,
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (!Array.isArray(file.grants)) {
|
|
209
|
+
this.corrupt = true;
|
|
210
|
+
throw new Error("CES grants file is malformed: grants is not an array");
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
this.cache = file.grants;
|
|
214
|
+
return [...file.grants];
|
|
215
|
+
} catch (err) {
|
|
216
|
+
if (this.corrupt) throw err;
|
|
217
|
+
// Any other read/parse error → fail closed
|
|
218
|
+
this.corrupt = true;
|
|
219
|
+
throw new Error(
|
|
220
|
+
`CES persistent grant store failed to read ${this.filePath}: ${err instanceof Error ? err.message : String(err)}`,
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
private writeToDisk(grants: PersistentGrant[]): void {
|
|
226
|
+
const dir = dirname(this.filePath);
|
|
227
|
+
if (!existsSync(dir)) {
|
|
228
|
+
mkdirSync(dir, { recursive: true });
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const data: GrantsFile = {
|
|
232
|
+
version: GRANTS_FILE_VERSION,
|
|
233
|
+
grants,
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
const tmpPath = join(dir, `.tmp-${randomUUID()}`);
|
|
237
|
+
writeFileSync(tmpPath, JSON.stringify(data, null, 2), {
|
|
238
|
+
mode: 0o600,
|
|
239
|
+
});
|
|
240
|
+
renameSync(tmpPath, this.filePath);
|
|
241
|
+
// Enforce owner-only permissions even if the file already existed
|
|
242
|
+
// with wider permissions.
|
|
243
|
+
chmodSync(this.filePath, 0o600);
|
|
244
|
+
|
|
245
|
+
this.cache = grants;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CES RPC handlers for grant and audit management.
|
|
3
|
+
*
|
|
4
|
+
* Implements the server-side handlers for:
|
|
5
|
+
* - `record_grant` — Record a grant decision after guardian approval.
|
|
6
|
+
* - `list_grants` — List grants filtered by session, handle, or status.
|
|
7
|
+
* - `revoke_grant` — Revoke a specific grant by its stable ID.
|
|
8
|
+
* - `list_audit_records` — List audit records with filtering and pagination.
|
|
9
|
+
*
|
|
10
|
+
* All handlers operate strictly on CES-owned state and never expose raw
|
|
11
|
+
* secret material, raw tokens, or raw headers/bodies. Grant records returned
|
|
12
|
+
* to the assistant contain only metadata (handle, proposal type, status,
|
|
13
|
+
* timestamps).
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type {
|
|
17
|
+
ListGrants,
|
|
18
|
+
ListGrantsResponse,
|
|
19
|
+
RecordGrant,
|
|
20
|
+
RecordGrantResponse,
|
|
21
|
+
RevokeGrant,
|
|
22
|
+
RevokeGrantResponse,
|
|
23
|
+
ListAuditRecords,
|
|
24
|
+
ListAuditRecordsResponse,
|
|
25
|
+
PersistentGrantRecord,
|
|
26
|
+
} from "@vellumai/ces-contracts";
|
|
27
|
+
|
|
28
|
+
import type { PersistentGrantStore, PersistentGrant } from "./persistent-store.js";
|
|
29
|
+
import type { TemporaryGrantStore } from "./temporary-store.js";
|
|
30
|
+
import type { AuditStore } from "../audit/store.js";
|
|
31
|
+
import type { RpcMethodHandler } from "../server.js";
|
|
32
|
+
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// Grant → PersistentGrantRecord projection
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Project a CES internal PersistentGrant into the wire-format
|
|
39
|
+
* PersistentGrantRecord. The internal store uses a simpler schema;
|
|
40
|
+
* the wire format includes additional status/lifecycle fields.
|
|
41
|
+
*
|
|
42
|
+
* Since the persistent store does not track lifecycle states (expiry,
|
|
43
|
+
* revocation, consumption), all persisted grants are considered "active".
|
|
44
|
+
*/
|
|
45
|
+
function projectGrant(
|
|
46
|
+
grant: PersistentGrant,
|
|
47
|
+
sessionId: string,
|
|
48
|
+
): PersistentGrantRecord {
|
|
49
|
+
return {
|
|
50
|
+
grantId: grant.id,
|
|
51
|
+
sessionId,
|
|
52
|
+
credentialHandle: grant.scope,
|
|
53
|
+
proposalType: grant.tool as "http" | "command",
|
|
54
|
+
proposalHash: grant.id,
|
|
55
|
+
allowedPurposes: [grant.pattern],
|
|
56
|
+
status: "active",
|
|
57
|
+
grantedBy: "user",
|
|
58
|
+
createdAt: new Date(grant.createdAt).toISOString(),
|
|
59
|
+
expiresAt: null,
|
|
60
|
+
consumedAt: null,
|
|
61
|
+
revokedAt: null,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
// record_grant handler
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
|
|
69
|
+
export interface RecordGrantHandlerDeps {
|
|
70
|
+
persistentGrantStore: PersistentGrantStore;
|
|
71
|
+
temporaryGrantStore: TemporaryGrantStore;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Create an RPC handler for the `record_grant` method.
|
|
76
|
+
*
|
|
77
|
+
* Receives a `TemporaryGrantDecision` from the approval bridge and persists
|
|
78
|
+
* it as a `PersistentGrant` (for approved decisions) or returns a success
|
|
79
|
+
* acknowledgement (for denied decisions). The handler also adds an
|
|
80
|
+
* in-memory temporary grant so the caller can immediately retry the
|
|
81
|
+
* original tool invocation.
|
|
82
|
+
*
|
|
83
|
+
* For approved decisions with a TTL of "PT10M", the grant is stored as
|
|
84
|
+
* a timed temporary grant. Otherwise it is persisted as a permanent grant.
|
|
85
|
+
*/
|
|
86
|
+
export function createRecordGrantHandler(
|
|
87
|
+
deps: RecordGrantHandlerDeps,
|
|
88
|
+
): RpcMethodHandler<RecordGrant, RecordGrantResponse> {
|
|
89
|
+
return (request) => {
|
|
90
|
+
const { decision, sessionId } = request;
|
|
91
|
+
|
|
92
|
+
// Denied decisions are acknowledged but produce no grant record.
|
|
93
|
+
if (decision.decision === "denied") {
|
|
94
|
+
return { success: true };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Build a PersistentGrant from the decision.
|
|
98
|
+
const proposal = decision.proposal;
|
|
99
|
+
const now = Date.now();
|
|
100
|
+
const grantId = decision.proposalHash;
|
|
101
|
+
|
|
102
|
+
const persistentGrant: PersistentGrant = {
|
|
103
|
+
id: grantId,
|
|
104
|
+
tool: proposal.type,
|
|
105
|
+
pattern:
|
|
106
|
+
proposal.type === "http"
|
|
107
|
+
? `${proposal.method} ${proposal.url}`
|
|
108
|
+
: proposal.command,
|
|
109
|
+
scope: proposal.credentialHandle,
|
|
110
|
+
createdAt: now,
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
// Persist the grant.
|
|
114
|
+
deps.persistentGrantStore.add(persistentGrant);
|
|
115
|
+
|
|
116
|
+
// Also record a temporary grant so the caller can use it immediately.
|
|
117
|
+
// Map TTL to the appropriate temporary grant kind.
|
|
118
|
+
if (decision.ttl === "PT10M") {
|
|
119
|
+
deps.temporaryGrantStore.add("allow_10m", decision.proposalHash);
|
|
120
|
+
} else {
|
|
121
|
+
deps.temporaryGrantStore.add("allow_once", decision.proposalHash);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Compute expiry from TTL if present.
|
|
125
|
+
let expiresAt: string | null = null;
|
|
126
|
+
if (decision.ttl === "PT10M") {
|
|
127
|
+
expiresAt = new Date(now + 10 * 60 * 1000).toISOString();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const grantRecord: PersistentGrantRecord = {
|
|
131
|
+
grantId,
|
|
132
|
+
sessionId,
|
|
133
|
+
credentialHandle: proposal.credentialHandle,
|
|
134
|
+
proposalType: proposal.type,
|
|
135
|
+
proposalHash: decision.proposalHash,
|
|
136
|
+
allowedPurposes:
|
|
137
|
+
proposal.type === "http"
|
|
138
|
+
? proposal.allowedUrlPatterns ?? [`${proposal.method} ${proposal.url}`]
|
|
139
|
+
: proposal.allowedCommandPatterns ?? [proposal.command],
|
|
140
|
+
status: "active",
|
|
141
|
+
grantedBy: decision.decidedBy,
|
|
142
|
+
createdAt: new Date(now).toISOString(),
|
|
143
|
+
expiresAt,
|
|
144
|
+
consumedAt: null,
|
|
145
|
+
revokedAt: null,
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
success: true,
|
|
150
|
+
grant: grantRecord,
|
|
151
|
+
};
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ---------------------------------------------------------------------------
|
|
156
|
+
// list_grants handler
|
|
157
|
+
// ---------------------------------------------------------------------------
|
|
158
|
+
|
|
159
|
+
export interface ListGrantsHandlerDeps {
|
|
160
|
+
persistentGrantStore: PersistentGrantStore;
|
|
161
|
+
/** Default session ID for grants that don't track session. */
|
|
162
|
+
sessionId: string;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Create an RPC handler for the `list_grants` method.
|
|
167
|
+
*
|
|
168
|
+
* Lists all persistent grants, optionally filtered by session ID,
|
|
169
|
+
* credential handle, or status. Returns wire-format PersistentGrantRecords
|
|
170
|
+
* that never include raw secret material.
|
|
171
|
+
*/
|
|
172
|
+
export function createListGrantsHandler(
|
|
173
|
+
deps: ListGrantsHandlerDeps,
|
|
174
|
+
): RpcMethodHandler<ListGrants, ListGrantsResponse> {
|
|
175
|
+
return (request) => {
|
|
176
|
+
const allGrants = deps.persistentGrantStore.getAll();
|
|
177
|
+
const projected = allGrants.map((g) => projectGrant(g, deps.sessionId));
|
|
178
|
+
|
|
179
|
+
let filtered = projected;
|
|
180
|
+
|
|
181
|
+
if (request.sessionId) {
|
|
182
|
+
filtered = filtered.filter((g) => g.sessionId === request.sessionId);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (request.credentialHandle) {
|
|
186
|
+
filtered = filtered.filter(
|
|
187
|
+
(g) => g.credentialHandle === request.credentialHandle,
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (request.status) {
|
|
192
|
+
filtered = filtered.filter((g) => g.status === request.status);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return { grants: filtered };
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ---------------------------------------------------------------------------
|
|
200
|
+
// revoke_grant handler
|
|
201
|
+
// ---------------------------------------------------------------------------
|
|
202
|
+
|
|
203
|
+
export interface RevokeGrantHandlerDeps {
|
|
204
|
+
persistentGrantStore: PersistentGrantStore;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Create an RPC handler for the `revoke_grant` method.
|
|
209
|
+
*
|
|
210
|
+
* Removes a grant from the persistent store by its stable ID. Returns
|
|
211
|
+
* success/failure. The reason field is logged but not persisted (the
|
|
212
|
+
* persistent store does not track revocation metadata).
|
|
213
|
+
*/
|
|
214
|
+
export function createRevokeGrantHandler(
|
|
215
|
+
deps: RevokeGrantHandlerDeps,
|
|
216
|
+
): RpcMethodHandler<RevokeGrant, RevokeGrantResponse> {
|
|
217
|
+
return (request) => {
|
|
218
|
+
const removed = deps.persistentGrantStore.remove(request.grantId);
|
|
219
|
+
|
|
220
|
+
if (!removed) {
|
|
221
|
+
return {
|
|
222
|
+
success: false,
|
|
223
|
+
error: {
|
|
224
|
+
code: "GRANT_NOT_FOUND",
|
|
225
|
+
message: `No grant found with ID "${request.grantId}"`,
|
|
226
|
+
},
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return { success: true };
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// ---------------------------------------------------------------------------
|
|
235
|
+
// list_audit_records handler
|
|
236
|
+
// ---------------------------------------------------------------------------
|
|
237
|
+
|
|
238
|
+
export interface ListAuditRecordsHandlerDeps {
|
|
239
|
+
auditStore: AuditStore;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Create an RPC handler for the `list_audit_records` method.
|
|
244
|
+
*
|
|
245
|
+
* Lists audit records with optional filtering by session, credential
|
|
246
|
+
* handle, or grant ID. Supports limit and cursor-based pagination.
|
|
247
|
+
*
|
|
248
|
+
* Audit records never contain raw secrets, raw tokens, or raw
|
|
249
|
+
* headers/bodies — they are token-free summaries generated at
|
|
250
|
+
* execution time.
|
|
251
|
+
*/
|
|
252
|
+
export function createListAuditRecordsHandler(
|
|
253
|
+
deps: ListAuditRecordsHandlerDeps,
|
|
254
|
+
): RpcMethodHandler<ListAuditRecords, ListAuditRecordsResponse> {
|
|
255
|
+
return (request) => {
|
|
256
|
+
const result = deps.auditStore.list({
|
|
257
|
+
sessionId: request.sessionId,
|
|
258
|
+
credentialHandle: request.credentialHandle,
|
|
259
|
+
grantId: request.grantId,
|
|
260
|
+
limit: request.limit,
|
|
261
|
+
cursor: request.cursor,
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
return {
|
|
265
|
+
records: result.records,
|
|
266
|
+
nextCursor: result.nextCursor,
|
|
267
|
+
};
|
|
268
|
+
};
|
|
269
|
+
}
|