filegrc 0.11.0 → 0.12.0
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/model/index.js +9 -5
- package/model/v10.json +12122 -0
- package/model/v9.json +11647 -0
- package/package.json +1 -1
- package/src/agent.js +3 -0
- package/src/audit-populations.js +88 -0
- package/src/audit-preparation.js +7 -2
- package/src/cli.js +186 -13
- package/src/collection-review-integrity.js +118 -0
- package/src/collection-review.js +71 -7
- package/src/collection-scope.js +8 -0
- package/src/evidence-packet.js +328 -46
- package/src/files.js +364 -6
- package/src/git.js +1064 -149
- package/src/index.js +17 -1
- package/src/model-migration.js +221 -5
- package/src/obligations.js +834 -66
- package/src/policy-library/information-security-policy-v2.md +1 -1
- package/src/policy-library.js +85 -8
- package/src/program-path.js +11 -5
- package/src/program-readiness.js +84 -6
- package/src/reconciliation.js +332 -81
- package/src/reporting-route-integrity.js +542 -0
- package/src/reporting-route-sets.js +745 -0
- package/src/server.js +160 -47
- package/src/state.js +30 -8
- package/src/time.js +55 -0
- package/src/validate.js +978 -4
- package/src/web.js +491 -49
- package/src/workflow-history-integrity.js +872 -0
- package/src/workflow.js +36 -10
package/src/server.js
CHANGED
|
@@ -5,6 +5,7 @@ import { extname, join, resolve } from "node:path";
|
|
|
5
5
|
import { performance } from "node:perf_hooks";
|
|
6
6
|
import { getResourceDefinition } from "../model/index.js";
|
|
7
7
|
import { prepareAuditWorkspace } from "./audit-preparation.js";
|
|
8
|
+
import { saveAuditPopulation, scaffoldAuditPopulationCorrection } from "./audit-populations.js";
|
|
8
9
|
import { activateDocuments, activateGovernedContent } from "./document-activation.js";
|
|
9
10
|
import { createNextAuditCycle, planNextAuditCycle } from "./audit-transition.js";
|
|
10
11
|
import { applyApplicabilityReviewWithContext, planApplicabilityReview } from "./batch-review.js";
|
|
@@ -33,11 +34,22 @@ import {
|
|
|
33
34
|
} from "./git.js";
|
|
34
35
|
import { normalizeResourceMutation, serializeWorkspaceMutation } from "./mutation.js";
|
|
35
36
|
import {
|
|
37
|
+
approveReportingRouteSet,
|
|
38
|
+
assessReportingRouteSets,
|
|
39
|
+
cancelReportingRouteSet,
|
|
40
|
+
proposeReportingRouteSet,
|
|
41
|
+
scaffoldReportingRouteSet
|
|
42
|
+
} from "./reporting-route-sets.js";
|
|
43
|
+
import {
|
|
44
|
+
activateObligationRule,
|
|
36
45
|
completeObligationAction,
|
|
37
46
|
completeObligationEvent,
|
|
38
47
|
completeObligationOccurrence,
|
|
39
48
|
createObligationEvent,
|
|
40
|
-
planObligations
|
|
49
|
+
planObligations,
|
|
50
|
+
saveObligationOccurrence,
|
|
51
|
+
scaffoldObligationOccurrence,
|
|
52
|
+
scaffoldObligationRuleActivation
|
|
41
53
|
} from "./obligations.js";
|
|
42
54
|
import {
|
|
43
55
|
planExternalReviewerGovernance,
|
|
@@ -80,6 +92,7 @@ export function createFilegrcServer(input = process.cwd(), options = {}) {
|
|
|
80
92
|
return json(response, 403, { error: "The request host is not allowed." });
|
|
81
93
|
}
|
|
82
94
|
const url = new URL(request.url, "http://localhost");
|
|
95
|
+
const requestOptions = { ...options, programId: url.searchParams.get("programId") || undefined };
|
|
83
96
|
if (["POST", "PUT", "DELETE"].includes(request.method) && !sameOrigin(request)) {
|
|
84
97
|
return json(response, 403, { error: "Cross-origin writes are not allowed." });
|
|
85
98
|
}
|
|
@@ -87,6 +100,7 @@ export function createFilegrcServer(input = process.cwd(), options = {}) {
|
|
|
87
100
|
if (timingEnabled()) {
|
|
88
101
|
const { result, timings } = await collectTimings(() => measureTiming("state", () => createAppState(input, {
|
|
89
102
|
allowNonAuthoritativeWrites: options.allowNonAuthoritativeWrites,
|
|
103
|
+
programId: requestOptions.programId,
|
|
90
104
|
includeDetails: false
|
|
91
105
|
})));
|
|
92
106
|
console.error(`[filegrc timing] ${JSON.stringify({ operation: "state", ...timings })}`);
|
|
@@ -94,6 +108,7 @@ export function createFilegrcServer(input = process.cwd(), options = {}) {
|
|
|
94
108
|
}
|
|
95
109
|
return json(response, 200, await createAppState(input, {
|
|
96
110
|
allowNonAuthoritativeWrites: options.allowNonAuthoritativeWrites,
|
|
111
|
+
programId: requestOptions.programId,
|
|
97
112
|
includeDetails: false
|
|
98
113
|
}));
|
|
99
114
|
}
|
|
@@ -107,7 +122,10 @@ export function createFilegrcServer(input = process.cwd(), options = {}) {
|
|
|
107
122
|
};
|
|
108
123
|
stateSessions.set(token, session);
|
|
109
124
|
while (stateSessions.size > 8) stateSessions.delete(stateSessions.keys().next().value);
|
|
110
|
-
const state = await createAppBootstrap(loaded, {
|
|
125
|
+
const state = await createAppBootstrap(loaded, {
|
|
126
|
+
generatedAt: session.generatedAt,
|
|
127
|
+
programId: url.searchParams.get("programId") || undefined
|
|
128
|
+
});
|
|
111
129
|
state.stateToken = token;
|
|
112
130
|
return json(response, 200, state);
|
|
113
131
|
}
|
|
@@ -119,7 +137,7 @@ export function createFilegrcServer(input = process.cwd(), options = {}) {
|
|
|
119
137
|
const token = url.searchParams.get("token");
|
|
120
138
|
const session = stateSessions.get(token);
|
|
121
139
|
if (!session) return json(response, 409, { error: "The workspace state expired. Reload it and try again." });
|
|
122
|
-
const state = await loadStateSessionSection(session, section, options);
|
|
140
|
+
const state = await loadStateSessionSection(session, section, options, url.searchParams.get("programId") || undefined);
|
|
123
141
|
return json(response, 200, { stateToken: token, section, state });
|
|
124
142
|
}
|
|
125
143
|
if (request.method === "GET" && url.pathname === "/api/history") {
|
|
@@ -130,6 +148,7 @@ export function createFilegrcServer(input = process.cwd(), options = {}) {
|
|
|
130
148
|
if (request.method === "GET" && url.pathname === "/api/obligations") {
|
|
131
149
|
const loaded = await loadWorkspace(input);
|
|
132
150
|
return json(response, 200, planObligations(loaded.resources, {
|
|
151
|
+
programId: url.searchParams.get("programId") || undefined,
|
|
133
152
|
asOf: url.searchParams.get("asOf") || undefined,
|
|
134
153
|
from: url.searchParams.get("from") || undefined,
|
|
135
154
|
through: url.searchParams.get("through") || undefined,
|
|
@@ -163,10 +182,11 @@ export function createFilegrcServer(input = process.cwd(), options = {}) {
|
|
|
163
182
|
}
|
|
164
183
|
if (request.method === "POST" && url.pathname === "/api/reconciliation") {
|
|
165
184
|
const payload = await readJson(request);
|
|
166
|
-
return json(response, 201, await browserMutation(input,
|
|
185
|
+
return json(response, 201, await browserMutation(input, requestOptions, {
|
|
167
186
|
message: (result) => `Reconcile policy event: ${result.event?.title || payload.candidateId}`
|
|
168
187
|
}, () => applyReconciliation(input, {
|
|
169
188
|
...payload,
|
|
189
|
+
programId: payload.programId || requestOptions.programId,
|
|
170
190
|
confirmed: payload.confirmed === true
|
|
171
191
|
})));
|
|
172
192
|
}
|
|
@@ -175,7 +195,7 @@ export function createFilegrcServer(input = process.cwd(), options = {}) {
|
|
|
175
195
|
}
|
|
176
196
|
if (request.method === "POST" && url.pathname === "/api/external-reviewer-governance") {
|
|
177
197
|
const payload = await readJson(request);
|
|
178
|
-
return json(response, 201, await browserMutation(input,
|
|
198
|
+
return json(response, 201, await browserMutation(input, requestOptions, {
|
|
179
199
|
message: (result) => `Assign external independent reviewer: ${result.reviewerId}`
|
|
180
200
|
}, () => setupExternalReviewerGovernance(input, {
|
|
181
201
|
...payload,
|
|
@@ -187,7 +207,7 @@ export function createFilegrcServer(input = process.cwd(), options = {}) {
|
|
|
187
207
|
}
|
|
188
208
|
if (request.method === "POST" && url.pathname === "/api/audit-cycle") {
|
|
189
209
|
const payload = await readJson(request);
|
|
190
|
-
return json(response, 201, await browserMutation(input,
|
|
210
|
+
return json(response, 201, await browserMutation(input, requestOptions, {
|
|
191
211
|
message: (result) => `Create next audit cycle: ${result.audit?.title || payload.priorAuditId}`
|
|
192
212
|
}, () => createNextAuditCycle(input, {
|
|
193
213
|
...payload,
|
|
@@ -195,48 +215,88 @@ export function createFilegrcServer(input = process.cwd(), options = {}) {
|
|
|
195
215
|
})));
|
|
196
216
|
}
|
|
197
217
|
if (request.method === "POST" && url.pathname === "/api/applicability-review/preview") {
|
|
198
|
-
|
|
218
|
+
const payload = await readJson(request);
|
|
219
|
+
return json(response, 200, await planApplicabilityReview(input, {
|
|
220
|
+
...payload,
|
|
221
|
+
programId: url.searchParams.get("programId") || payload.programId
|
|
222
|
+
}));
|
|
199
223
|
}
|
|
200
224
|
if (request.method === "POST" && url.pathname === "/api/applicability-review") {
|
|
201
225
|
const payload = await readJson(request);
|
|
202
226
|
const { prefetchToken, ...reviewPayload } = payload;
|
|
203
|
-
return json(response, 201, await browserMutation(input,
|
|
227
|
+
return json(response, 201, await browserMutation(input, requestOptions, {
|
|
204
228
|
message: (result) => `Record ${result.reviewedIds?.length || 0} applicability decisions`,
|
|
205
229
|
fastResponse: prefersFastMutation(request),
|
|
206
230
|
prefetchToken
|
|
207
231
|
}, (_root, mutationContext) => applyApplicabilityReviewWithContext(input, {
|
|
208
232
|
...reviewPayload,
|
|
233
|
+
programId: url.searchParams.get("programId") || reviewPayload.programId,
|
|
209
234
|
confirmed: payload.confirmed === true
|
|
210
235
|
}, {
|
|
211
236
|
repositorySnapshot: mutationContext?.repositorySnapshot
|
|
212
237
|
})));
|
|
213
238
|
}
|
|
214
239
|
if (request.method === "POST" && url.pathname === "/api/collection-review/preview") {
|
|
215
|
-
|
|
240
|
+
const payload = await readJson(request);
|
|
241
|
+
return json(response, 200, await planCollectionReview(input, {
|
|
242
|
+
...payload,
|
|
243
|
+
programId: url.searchParams.get("programId") || payload.programId
|
|
244
|
+
}));
|
|
216
245
|
}
|
|
217
246
|
if (request.method === "POST" && url.pathname === "/api/collection-review") {
|
|
218
247
|
const payload = await readJson(request);
|
|
219
248
|
const { prefetchToken, ...reviewPayload } = payload;
|
|
220
|
-
return json(response, 201, await browserMutation(input,
|
|
249
|
+
return json(response, 201, await browserMutation(input, requestOptions, {
|
|
221
250
|
message: (result) => `Confirm ${result.assessment?.configuration?.title || payload.resourceType}`,
|
|
222
251
|
fastResponse: prefersFastMutation(request),
|
|
223
252
|
prefetchToken
|
|
224
253
|
}, (_root, mutationContext) => applyCollectionReview(input, {
|
|
225
254
|
...reviewPayload,
|
|
255
|
+
programId: url.searchParams.get("programId") || reviewPayload.programId,
|
|
226
256
|
scopeRevision: mutationContext?.repositorySnapshot?.currentCommit,
|
|
227
257
|
confirmed: payload.confirmed === true
|
|
228
258
|
})));
|
|
229
259
|
}
|
|
230
260
|
if (request.method === "POST" && url.pathname === "/api/obligation-events") {
|
|
231
261
|
const payload = await readJson(request);
|
|
232
|
-
return json(response, 201, await browserMutation(input,
|
|
262
|
+
return json(response, 201, await browserMutation(input, requestOptions, {
|
|
233
263
|
message: (result) => `Create policy event: ${result.event?.title || payload.title || payload.eventType}`
|
|
234
|
-
}, () => createObligationEvent(input, payload)));
|
|
264
|
+
}, () => createObligationEvent(input, { ...payload, programId: payload.programId || requestOptions.programId })));
|
|
265
|
+
}
|
|
266
|
+
if (request.method === "GET" && url.pathname === "/api/reporting-route-sets") {
|
|
267
|
+
return json(response, 200, await assessReportingRouteSets(input, {
|
|
268
|
+
programId: url.searchParams.get("programId") || requestOptions.programId,
|
|
269
|
+
at: url.searchParams.get("at") || undefined
|
|
270
|
+
}));
|
|
271
|
+
}
|
|
272
|
+
if (request.method === "POST" && url.pathname === "/api/reporting-route-sets/scaffold") {
|
|
273
|
+
return json(response, 200, scaffoldReportingRouteSet(await readJson(request)));
|
|
274
|
+
}
|
|
275
|
+
if (request.method === "POST" && url.pathname === "/api/reporting-route-sets/propose") {
|
|
276
|
+
const payload = await readJson(request);
|
|
277
|
+
if (!safeSegment(payload.routeSetId)) return json(response, 400, { error: "A safe Reporting Route Set ID is required." });
|
|
278
|
+
return json(response, 200, await browserMutation(input, requestOptions, {
|
|
279
|
+
message: () => `Propose reporting route set: ${payload.routeSetId}`
|
|
280
|
+
}, () => proposeReportingRouteSet(input, payload)));
|
|
281
|
+
}
|
|
282
|
+
if (request.method === "POST" && url.pathname === "/api/reporting-route-sets/approve") {
|
|
283
|
+
const payload = await readJson(request);
|
|
284
|
+
if (!safeSegment(payload.routeSetId)) return json(response, 400, { error: "A safe Reporting Route Set ID is required." });
|
|
285
|
+
return json(response, 200, await browserMutation(input, requestOptions, {
|
|
286
|
+
message: () => `Approve reporting route set: ${payload.routeSetId}`
|
|
287
|
+
}, () => approveReportingRouteSet(input, payload)));
|
|
288
|
+
}
|
|
289
|
+
if (request.method === "POST" && url.pathname === "/api/reporting-route-sets/cancel") {
|
|
290
|
+
const payload = await readJson(request);
|
|
291
|
+
if (!safeSegment(payload.routeSetId)) return json(response, 400, { error: "A safe Reporting Route Set ID is required." });
|
|
292
|
+
return json(response, 200, await browserMutation(input, requestOptions, {
|
|
293
|
+
message: () => `Cancel reporting route set: ${payload.routeSetId}`
|
|
294
|
+
}, () => cancelReportingRouteSet(input, payload)));
|
|
235
295
|
}
|
|
236
296
|
if (request.method === "POST" && url.pathname === "/api/obligation-completions") {
|
|
237
297
|
const payload = await readJson(request);
|
|
238
298
|
if (!safeSegment(payload.obligationId)) return json(response, 400, { error: "A safe obligation ID is required." });
|
|
239
|
-
const result = await browserMutation(input,
|
|
299
|
+
const result = await browserMutation(input, requestOptions, {
|
|
240
300
|
message: () => `Complete ${resourceTypeLabel(payload.record?.type)}: ${payload.record?.title || payload.obligationId}`
|
|
241
301
|
}, () => completeObligationOccurrence(input, {
|
|
242
302
|
obligationId: payload.obligationId,
|
|
@@ -246,12 +306,59 @@ export function createFilegrcServer(input = process.cwd(), options = {}) {
|
|
|
246
306
|
}));
|
|
247
307
|
return json(response, 201, result);
|
|
248
308
|
}
|
|
309
|
+
if (request.method === "POST" && url.pathname === "/api/obligation-occurrences/scaffold") {
|
|
310
|
+
const payload = await readJson(request);
|
|
311
|
+
if (!safeSegment(payload.obligationId)) return json(response, 400, { error: "A safe Obligation ID is required." });
|
|
312
|
+
return json(response, 200, await scaffoldObligationOccurrence(input, { ...payload, programId: payload.programId || requestOptions.programId }));
|
|
313
|
+
}
|
|
314
|
+
if (request.method === "POST" && url.pathname === "/api/audit-population-corrections/scaffold") {
|
|
315
|
+
const payload = await readJson(request);
|
|
316
|
+
if (!safeSegment(payload.populationId)) return json(response, 400, { error: "A safe Audit Population ID is required." });
|
|
317
|
+
return json(response, 200, await scaffoldAuditPopulationCorrection(input, payload));
|
|
318
|
+
}
|
|
319
|
+
if (request.method === "POST" && url.pathname === "/api/audit-population-corrections") {
|
|
320
|
+
const payload = await readJson(request);
|
|
321
|
+
if (!safeSegment(payload.record?.id)) return json(response, 400, { error: "A safe Audit Population correction ID is required." });
|
|
322
|
+
return json(response, 201, await browserMutation(input, requestOptions, {
|
|
323
|
+
message: () => `Correct audit population: ${payload.record.title || payload.record.id}`
|
|
324
|
+
}, () => saveAuditPopulation(input, {
|
|
325
|
+
record: payload.record,
|
|
326
|
+
content: payload.content,
|
|
327
|
+
expectedRevision: requireRevision(payload.revision, `audit-population/${payload.record.supersedesId}`)
|
|
328
|
+
})));
|
|
329
|
+
}
|
|
330
|
+
if (request.method === "POST" && url.pathname === "/api/obligation-occurrences") {
|
|
331
|
+
const payload = await readJson(request);
|
|
332
|
+
if (!safeSegment(payload.record?.id)) return json(response, 400, { error: "A safe occurrence ID is required." });
|
|
333
|
+
return json(response, 201, await browserMutation(input, requestOptions, {
|
|
334
|
+
message: () => `Save obligation occurrence: ${payload.record.title || payload.record.id}`
|
|
335
|
+
}, () => saveObligationOccurrence(input, {
|
|
336
|
+
record: payload.record,
|
|
337
|
+
programId: payload.programId || requestOptions.programId,
|
|
338
|
+
content: payload.content,
|
|
339
|
+
expectedRevision: payload.record?.supersedesId
|
|
340
|
+
? requireRevision(payload.revision, `obligation-occurrence/${payload.record.supersedesId}`)
|
|
341
|
+
: payload.revision
|
|
342
|
+
})));
|
|
343
|
+
}
|
|
344
|
+
if (request.method === "POST" && url.pathname === "/api/obligation-rule-activations/scaffold") {
|
|
345
|
+
const payload = await readJson(request);
|
|
346
|
+
if (!safeSegment(payload.ruleId)) return json(response, 400, { error: "A safe Obligation Rule ID is required." });
|
|
347
|
+
return json(response, 200, await scaffoldObligationRuleActivation(input, payload));
|
|
348
|
+
}
|
|
349
|
+
if (request.method === "POST" && url.pathname === "/api/obligation-rule-activations") {
|
|
350
|
+
const payload = await readJson(request);
|
|
351
|
+
if (!safeSegment(payload.ruleId)) return json(response, 400, { error: "A safe Obligation Rule ID is required." });
|
|
352
|
+
return json(response, 201, await browserMutation(input, requestOptions, {
|
|
353
|
+
message: () => `Activate obligation rule: ${payload.ruleId}`
|
|
354
|
+
}, () => activateObligationRule(input, payload)));
|
|
355
|
+
}
|
|
249
356
|
if (request.method === "POST" && url.pathname === "/api/action-completions") {
|
|
250
357
|
const payload = await readJson(request);
|
|
251
358
|
if (!safeSegment(payload.actionItemId)) {
|
|
252
359
|
return json(response, 400, { error: "A safe Action Item ID is required." });
|
|
253
360
|
}
|
|
254
|
-
const result = await browserMutation(input,
|
|
361
|
+
const result = await browserMutation(input, requestOptions, {
|
|
255
362
|
message: () => `Complete action item: ${payload.actionItemId}`
|
|
256
363
|
}, () => completeObligationAction(input, {
|
|
257
364
|
actionItemId: payload.actionItemId,
|
|
@@ -267,10 +374,11 @@ export function createFilegrcServer(input = process.cwd(), options = {}) {
|
|
|
267
374
|
if (!safeSegment(payload.eventId)) {
|
|
268
375
|
return json(response, 400, { error: "A safe Policy Event ID is required." });
|
|
269
376
|
}
|
|
270
|
-
const result = await browserMutation(input,
|
|
377
|
+
const result = await browserMutation(input, requestOptions, {
|
|
271
378
|
message: () => `Complete policy event: ${payload.eventId}`
|
|
272
379
|
}, () => completeObligationEvent(input, {
|
|
273
380
|
eventId: payload.eventId,
|
|
381
|
+
programId: payload.programId || requestOptions.programId,
|
|
274
382
|
completedOn: payload.completedOn,
|
|
275
383
|
expectedRevision: requireRevision(payload.revision, `obligation-event/${payload.eventId}`)
|
|
276
384
|
}));
|
|
@@ -288,7 +396,7 @@ export function createFilegrcServer(input = process.cwd(), options = {}) {
|
|
|
288
396
|
try {
|
|
289
397
|
const temporaryPath = join(temporaryDirectory, attachment);
|
|
290
398
|
await writeFile(temporaryPath, await readBytes(request));
|
|
291
|
-
const result = await browserMutation(input,
|
|
399
|
+
const result = await browserMutation(input, requestOptions, {
|
|
292
400
|
message: () => `Attach evidence file: ${attachment}`
|
|
293
401
|
}, () => addEvidenceAttachment(input, evidenceId, temporaryPath, {
|
|
294
402
|
name: attachment,
|
|
@@ -306,7 +414,7 @@ export function createFilegrcServer(input = process.cwd(), options = {}) {
|
|
|
306
414
|
return json(response, 400, { error: "Safe Evidence and attachment identifiers are required." });
|
|
307
415
|
}
|
|
308
416
|
const revision = requireRevision(url.searchParams.get("revision"), `evidence/${evidenceId}`);
|
|
309
|
-
const result = await browserMutation(input,
|
|
417
|
+
const result = await browserMutation(input, requestOptions, {
|
|
310
418
|
message: () => `Remove evidence file: ${attachment}`
|
|
311
419
|
}, () => removeEvidenceAttachment(input, evidenceId, attachment, {
|
|
312
420
|
expectedRevision: revision
|
|
@@ -317,7 +425,8 @@ export function createFilegrcServer(input = process.cwd(), options = {}) {
|
|
|
317
425
|
return json(response, 200, await prepareEvidencePacket(input, {
|
|
318
426
|
start: url.searchParams.get("start"),
|
|
319
427
|
end: url.searchParams.get("end"),
|
|
320
|
-
auditId: url.searchParams.get("auditId") || undefined
|
|
428
|
+
auditId: url.searchParams.get("auditId") || undefined,
|
|
429
|
+
programId: url.searchParams.get("programId") || undefined
|
|
321
430
|
}));
|
|
322
431
|
}
|
|
323
432
|
if (request.method === "POST" && url.pathname === "/api/evidence-packet") {
|
|
@@ -339,14 +448,14 @@ export function createFilegrcServer(input = process.cwd(), options = {}) {
|
|
|
339
448
|
}
|
|
340
449
|
if (request.method === "POST" && url.pathname === "/api/audit-preparation") {
|
|
341
450
|
const payload = await readJson(request);
|
|
342
|
-
return json(response, 201, await browserMutation(input,
|
|
451
|
+
return json(response, 201, await browserMutation(input, requestOptions, {
|
|
343
452
|
message: () => `Prepare audit: ${payload.auditId || "engagement"}`
|
|
344
453
|
}, () => prepareAuditWorkspace(input, payload)));
|
|
345
454
|
}
|
|
346
455
|
if (request.method === "POST" && url.pathname === "/api/setup") {
|
|
347
456
|
const payload = await readJson(request);
|
|
348
457
|
const completeSetup = async () => {
|
|
349
|
-
return browserMutation(input,
|
|
458
|
+
return browserMutation(input, requestOptions, {
|
|
350
459
|
message: (setupResult) => `${payload.draft === true ? "Save onboarding draft" : "Complete onboarding"} for ${setupResult.workspace.organizationName}`,
|
|
351
460
|
fastResponse: prefersFastMutation(request)
|
|
352
461
|
}, () => setupWorkspace(input, payload));
|
|
@@ -355,21 +464,21 @@ export function createFilegrcServer(input = process.cwd(), options = {}) {
|
|
|
355
464
|
}
|
|
356
465
|
if (request.method === "POST" && url.pathname === "/api/policy-activations") {
|
|
357
466
|
const payload = await readJson(request);
|
|
358
|
-
const result = await browserMutation(input,
|
|
467
|
+
const result = await browserMutation(input, requestOptions, {
|
|
359
468
|
message: (activation) => `Activate ${activation.policyIds.length} ${activation.policyIds.length === 1 ? "Policy" : "Policies"}`
|
|
360
469
|
}, () => activatePolicies(input, { ...payload, confirmed: true }));
|
|
361
470
|
return json(response, 200, result);
|
|
362
471
|
}
|
|
363
472
|
if (request.method === "POST" && url.pathname === "/api/document-activations") {
|
|
364
473
|
const payload = await readJson(request);
|
|
365
|
-
const result = await browserMutation(input,
|
|
474
|
+
const result = await browserMutation(input, requestOptions, {
|
|
366
475
|
message: (activation) => `Activate ${activation.documentIds.length} governed ${activation.documentIds.length === 1 ? "Document" : "Documents"}`
|
|
367
476
|
}, () => activateDocuments(input, { ...payload, confirmed: true }));
|
|
368
477
|
return json(response, 200, result);
|
|
369
478
|
}
|
|
370
479
|
if (request.method === "POST" && url.pathname === "/api/governed-content-activations") {
|
|
371
480
|
const payload = await readJson(request);
|
|
372
|
-
const result = await browserMutation(input,
|
|
481
|
+
const result = await browserMutation(input, requestOptions, {
|
|
373
482
|
message: (activation) => `Activate ${activation.resourceIds.length} governed-content ${activation.resourceIds.length === 1 ? "record" : "records"}`
|
|
374
483
|
}, () => activateGovernedContent(input, { ...payload, confirmed: true }));
|
|
375
484
|
return json(response, 200, result);
|
|
@@ -377,7 +486,7 @@ export function createFilegrcServer(input = process.cwd(), options = {}) {
|
|
|
377
486
|
if (request.method === "POST" && url.pathname === "/api/resources") {
|
|
378
487
|
const payload = normalizeResourceMutation(await readJson(request));
|
|
379
488
|
const { record } = payload;
|
|
380
|
-
const result = await browserMutation(input,
|
|
489
|
+
const result = await browserMutation(input, requestOptions, {
|
|
381
490
|
message: () => `Create ${resourceTypeLabel(record.type)}: ${record.title || record.id}`
|
|
382
491
|
}, () => createResource(input, record, { content: payload.content }));
|
|
383
492
|
return json(response, 201, result);
|
|
@@ -385,15 +494,15 @@ export function createFilegrcServer(input = process.cwd(), options = {}) {
|
|
|
385
494
|
if (request.method === "POST" && url.pathname === "/api/commit") {
|
|
386
495
|
await requireManualBrowserGit(input, options);
|
|
387
496
|
const payload = await readJson(request);
|
|
388
|
-
return json(response, 201, await manualGitResultWithState(input,
|
|
497
|
+
return json(response, 201, await manualGitResultWithState(input, requestOptions, () => commitAndPushWorkspace(input, payload.message)));
|
|
389
498
|
}
|
|
390
499
|
if (request.method === "POST" && url.pathname === "/api/git/pull") {
|
|
391
500
|
await requireManualBrowserGit(input, options);
|
|
392
|
-
return json(response, 200, await manualGitResultWithState(input,
|
|
501
|
+
return json(response, 200, await manualGitResultWithState(input, requestOptions, () => pullWorkspace(input)));
|
|
393
502
|
}
|
|
394
503
|
if (request.method === "POST" && url.pathname === "/api/git/push") {
|
|
395
504
|
await requireManualBrowserGit(input, options);
|
|
396
|
-
return json(response, 200, await manualGitResultWithState(input,
|
|
505
|
+
return json(response, 200, await manualGitResultWithState(input, requestOptions, () => pushWorkspace(input)));
|
|
397
506
|
}
|
|
398
507
|
if (request.method === "POST" && url.pathname === "/api/git/retry-sync") {
|
|
399
508
|
const result = await retryBrowserSync(input, {
|
|
@@ -431,7 +540,7 @@ export function createFilegrcServer(input = process.cwd(), options = {}) {
|
|
|
431
540
|
}
|
|
432
541
|
if (request.method === "PUT" && url.pathname === "/api/content") {
|
|
433
542
|
const payload = await readJson(request);
|
|
434
|
-
const result = await browserMutation(input,
|
|
543
|
+
const result = await browserMutation(input, requestOptions, {
|
|
435
544
|
message: () => `Update content: ${payload.path}`
|
|
436
545
|
}, () => updateContent(input, payload.path, payload.source, {
|
|
437
546
|
expectedRevision: requireRevision(payload.revision, `content/${payload.path}`)
|
|
@@ -455,7 +564,7 @@ export function createFilegrcServer(input = process.cwd(), options = {}) {
|
|
|
455
564
|
if (request.method === "PUT") {
|
|
456
565
|
const payload = normalizeResourceMutation(await readJson(request), { requireRevision: true });
|
|
457
566
|
const { record } = payload;
|
|
458
|
-
const result = await browserMutation(input,
|
|
567
|
+
const result = await browserMutation(input, requestOptions, {
|
|
459
568
|
message: () => `Update ${resourceTypeLabel(type)}: ${record.title || id}`
|
|
460
569
|
}, () => updateResource(input, type, id, record, {
|
|
461
570
|
content: payload.content,
|
|
@@ -467,7 +576,7 @@ export function createFilegrcServer(input = process.cwd(), options = {}) {
|
|
|
467
576
|
}
|
|
468
577
|
if (request.method === "DELETE") {
|
|
469
578
|
const revision = requireRevision(url.searchParams.get("revision"), `${type}/${id}`);
|
|
470
|
-
const result = await browserMutation(input,
|
|
579
|
+
const result = await browserMutation(input, requestOptions, {
|
|
471
580
|
message: () => `Delete ${resourceTypeLabel(type)}: ${id}`
|
|
472
581
|
}, () => deleteResource(input, type, id, { expectedRevision: revision }));
|
|
473
582
|
return json(response, 200, {
|
|
@@ -575,16 +684,16 @@ function listen(server, port, host) {
|
|
|
575
684
|
});
|
|
576
685
|
}
|
|
577
686
|
|
|
578
|
-
function browserMutation(input,
|
|
687
|
+
function browserMutation(input, requestOptions, mutationOptions, task) {
|
|
579
688
|
const run = () => serializeWorkspaceMutation(input, async (root) => {
|
|
580
689
|
const fastResponse = mutationOptions.fastResponse === true;
|
|
581
690
|
const workflowBefore = fastResponse
|
|
582
691
|
? null
|
|
583
|
-
: await measureTiming("workflow-before", () => assessWorkflow(root));
|
|
692
|
+
: await measureTiming("workflow-before", () => assessWorkflow(root, { programId: requestOptions.programId }));
|
|
584
693
|
const result = await measureTiming("mutation", () => runBrowserMutation(root, {
|
|
585
694
|
...mutationOptions,
|
|
586
|
-
allowNonAuthoritativeWrites:
|
|
587
|
-
backgroundPushDelayMs:
|
|
695
|
+
allowNonAuthoritativeWrites: requestOptions.allowNonAuthoritativeWrites === true,
|
|
696
|
+
backgroundPushDelayMs: requestOptions.backgroundPushDelayMs,
|
|
588
697
|
includeValidationProof: !fastResponse
|
|
589
698
|
}, task));
|
|
590
699
|
if (fastResponse) {
|
|
@@ -594,7 +703,8 @@ function browserMutation(input, options, mutationOptions, task) {
|
|
|
594
703
|
};
|
|
595
704
|
}
|
|
596
705
|
const state = await measureTiming("state", () => createAppState(root, {
|
|
597
|
-
allowNonAuthoritativeWrites:
|
|
706
|
+
allowNonAuthoritativeWrites: requestOptions.allowNonAuthoritativeWrites,
|
|
707
|
+
programId: requestOptions.programId,
|
|
598
708
|
includeDetails: false,
|
|
599
709
|
validationProof: result?.[BROWSER_VALIDATION]
|
|
600
710
|
}));
|
|
@@ -633,17 +743,18 @@ export function reconcileMutationSynchronization(synchronization, repository) {
|
|
|
633
743
|
};
|
|
634
744
|
}
|
|
635
745
|
|
|
636
|
-
function loadStateSessionSection(session, section, serverOptions) {
|
|
637
|
-
|
|
746
|
+
function loadStateSessionSection(session, section, serverOptions, programId) {
|
|
747
|
+
const cacheKey = `${section}:${programId || "default"}`;
|
|
748
|
+
if (session.promises.has(cacheKey)) return session.promises.get(cacheKey);
|
|
638
749
|
const dependencies = section === "workflow"
|
|
639
750
|
? Promise.all([
|
|
640
|
-
loadStateSessionSection(session, "repository", serverOptions),
|
|
641
|
-
loadStateSessionSection(session, "program", serverOptions),
|
|
642
|
-
loadStateSessionSection(session, "obligations", serverOptions),
|
|
643
|
-
loadStateSessionSection(session, "audits", serverOptions)
|
|
751
|
+
loadStateSessionSection(session, "repository", serverOptions, programId),
|
|
752
|
+
loadStateSessionSection(session, "program", serverOptions, programId),
|
|
753
|
+
loadStateSessionSection(session, "obligations", serverOptions, programId),
|
|
754
|
+
loadStateSessionSection(session, "audits", serverOptions, programId)
|
|
644
755
|
])
|
|
645
756
|
: section === "audits"
|
|
646
|
-
? Promise.all([loadStateSessionSection(session, "program", serverOptions)])
|
|
757
|
+
? Promise.all([loadStateSessionSection(session, "program", serverOptions, programId)])
|
|
647
758
|
: Promise.resolve([]);
|
|
648
759
|
const promise = dependencies.then((results) => {
|
|
649
760
|
const repository = section === "workflow" ? results[0] : null;
|
|
@@ -657,13 +768,14 @@ function loadStateSessionSection(session, section, serverOptions) {
|
|
|
657
768
|
auditPreparations: audits?.auditPreparations,
|
|
658
769
|
obligations: obligations?.obligations,
|
|
659
770
|
git: repository?.git,
|
|
660
|
-
validation: repository?.validation
|
|
771
|
+
validation: repository?.validation,
|
|
772
|
+
programId
|
|
661
773
|
});
|
|
662
774
|
}).catch((error) => {
|
|
663
|
-
session.promises.delete(
|
|
775
|
+
session.promises.delete(cacheKey);
|
|
664
776
|
throw error;
|
|
665
777
|
});
|
|
666
|
-
session.promises.set(
|
|
778
|
+
session.promises.set(cacheKey, promise);
|
|
667
779
|
return promise;
|
|
668
780
|
}
|
|
669
781
|
|
|
@@ -682,10 +794,11 @@ async function requireManualBrowserGit(input, options) {
|
|
|
682
794
|
}
|
|
683
795
|
}
|
|
684
796
|
|
|
685
|
-
async function manualGitResultWithState(input,
|
|
797
|
+
async function manualGitResultWithState(input, requestOptions, task) {
|
|
686
798
|
const result = await task();
|
|
687
799
|
const state = await createAppState(input, {
|
|
688
|
-
allowNonAuthoritativeWrites:
|
|
800
|
+
allowNonAuthoritativeWrites: requestOptions.allowNonAuthoritativeWrites,
|
|
801
|
+
programId: requestOptions.programId,
|
|
689
802
|
includeDetails: false
|
|
690
803
|
});
|
|
691
804
|
return { ...result, state };
|
package/src/state.js
CHANGED
|
@@ -15,6 +15,7 @@ import { fingerprintWorkspace, validateWorkspace } from "./validate.js";
|
|
|
15
15
|
import { assessWorkflow } from "./workflow.js";
|
|
16
16
|
import { measureTiming } from "./timing.js";
|
|
17
17
|
import { soc2RequirementApplicabilityConstraint } from "./soc2.js";
|
|
18
|
+
import { modelSupports } from "../model/index.js";
|
|
18
19
|
|
|
19
20
|
const renderedMarkdownCache = new Map();
|
|
20
21
|
const MAX_RENDERED_MARKDOWN_CACHE_ENTRIES = 1_000;
|
|
@@ -27,7 +28,8 @@ export async function createAppState(input = process.cwd(), options = {}) {
|
|
|
27
28
|
options.allowNonAuthoritativeWrites === true,
|
|
28
29
|
options.includeDetails !== false,
|
|
29
30
|
options.asOf ?? null,
|
|
30
|
-
options.now ?? null
|
|
31
|
+
options.now ?? null,
|
|
32
|
+
options.programId ?? null
|
|
31
33
|
]);
|
|
32
34
|
if (!options.validationProof && appStatePromises.has(key)) return appStatePromises.get(key);
|
|
33
35
|
const promise = serializeWorkspaceMutation(input, (root) => createAppStateUnlocked(root, options)).finally(() => {
|
|
@@ -48,6 +50,10 @@ export async function createAppBootstrap(input = process.cwd(), options = {}) {
|
|
|
48
50
|
timezone: "UTC"
|
|
49
51
|
};
|
|
50
52
|
const generatedAt = options.generatedAt ?? new Date().toISOString();
|
|
53
|
+
const programs = loaded.resources.filter((record) => record.type === "program" && record.status !== "retired");
|
|
54
|
+
const selectedProgram = options.programId
|
|
55
|
+
? resolveProgram(loaded, options.programId)
|
|
56
|
+
: programs[0] || null;
|
|
51
57
|
return {
|
|
52
58
|
generatedAt,
|
|
53
59
|
asOf: options.asOf ?? currentCalendarDate(workspace.timezone),
|
|
@@ -60,6 +66,7 @@ export async function createAppBootstrap(input = process.cwd(), options = {}) {
|
|
|
60
66
|
writesAllowed: false
|
|
61
67
|
},
|
|
62
68
|
workspace,
|
|
69
|
+
selectedProgramId: selectedProgram?.id || null,
|
|
63
70
|
model: loaded.model,
|
|
64
71
|
resources: loaded.entries.map((entry) => ({
|
|
65
72
|
record: structuredClone(entry.record),
|
|
@@ -129,14 +136,14 @@ export async function createAppStateSection(input, section, options = {}) {
|
|
|
129
136
|
};
|
|
130
137
|
}
|
|
131
138
|
if (section === "program") {
|
|
132
|
-
const programReadiness = options.programReadiness ?? await assessProgramReadiness(loaded, { asOf, generatedAt });
|
|
133
|
-
const activeProgram = resolveProgram(loaded);
|
|
139
|
+
const programReadiness = options.programReadiness ?? await assessProgramReadiness(loaded, { programId: options.programId, asOf, generatedAt });
|
|
140
|
+
const activeProgram = resolveProgram(loaded, options.programId);
|
|
134
141
|
return {
|
|
135
142
|
generatedAt,
|
|
136
143
|
asOf,
|
|
137
144
|
programReadiness,
|
|
138
145
|
collectionReviews: Object.fromEntries(
|
|
139
|
-
assessCollectionReviews(loaded).map((assessment) => [
|
|
146
|
+
assessCollectionReviews(loaded, { programId: activeProgram.id }).map((assessment) => [
|
|
140
147
|
assessment.resourceType,
|
|
141
148
|
{
|
|
142
149
|
resourceType: assessment.resourceType,
|
|
@@ -158,10 +165,12 @@ export async function createAppStateSection(input, section, options = {}) {
|
|
|
158
165
|
};
|
|
159
166
|
}
|
|
160
167
|
if (section === "obligations") {
|
|
168
|
+
const activeProgram = resolveProgram(loaded, options.programId);
|
|
161
169
|
return {
|
|
162
170
|
generatedAt,
|
|
163
171
|
asOf,
|
|
164
172
|
obligations: planObligations(loaded.resources, {
|
|
173
|
+
programId: activeProgram.id,
|
|
165
174
|
asOf,
|
|
166
175
|
now: options.now ?? generatedAt,
|
|
167
176
|
model: loaded.model
|
|
@@ -170,7 +179,11 @@ export async function createAppStateSection(input, section, options = {}) {
|
|
|
170
179
|
}
|
|
171
180
|
if (section === "audits") {
|
|
172
181
|
const programReadiness = options.programReadiness;
|
|
173
|
-
const
|
|
182
|
+
const activeProgram = resolveProgram(loaded, options.programId);
|
|
183
|
+
const audits = loaded.resources.filter((record) => (
|
|
184
|
+
record.type === "audit"
|
|
185
|
+
&& (!modelSupports(loaded.model, "program-scope") || record.programId === activeProgram.id)
|
|
186
|
+
));
|
|
174
187
|
return {
|
|
175
188
|
generatedAt,
|
|
176
189
|
asOf,
|
|
@@ -179,6 +192,7 @@ export async function createAppStateSection(input, section, options = {}) {
|
|
|
179
192
|
audit?.id || "none",
|
|
180
193
|
await assessAuditPreparation(loaded, {
|
|
181
194
|
auditId: audit?.id,
|
|
195
|
+
programId: audit?.programId || options.programId,
|
|
182
196
|
asOf,
|
|
183
197
|
generatedAt,
|
|
184
198
|
...(audit || !programReadiness ? {} : { programReadiness })
|
|
@@ -192,6 +206,7 @@ export async function createAppStateSection(input, section, options = {}) {
|
|
|
192
206
|
generatedAt,
|
|
193
207
|
asOf,
|
|
194
208
|
workflow: await assessWorkflow(loaded, {
|
|
209
|
+
programId: options.programId,
|
|
195
210
|
asOf,
|
|
196
211
|
evaluatedAt: generatedAt,
|
|
197
212
|
programReadiness: options.programReadiness,
|
|
@@ -249,15 +264,19 @@ async function createAppStateUnlocked(input, options) {
|
|
|
249
264
|
const asOf = options.asOf ?? currentCalendarDate(workspace.timezone);
|
|
250
265
|
const generatedAt = new Date().toISOString();
|
|
251
266
|
const programReadiness = await measureTiming("state-program-readiness", () => assessProgramReadiness(loaded, {
|
|
267
|
+
programId: options.programId,
|
|
252
268
|
asOf,
|
|
253
269
|
generatedAt
|
|
254
270
|
}));
|
|
255
|
-
const activeProgram = resolveProgram(loaded);
|
|
271
|
+
const activeProgram = resolveProgram(loaded, options.programId);
|
|
256
272
|
const applicabilityConstraints = Object.fromEntries(loaded.resources.flatMap((record) => {
|
|
257
273
|
const constraint = soc2RequirementApplicabilityConstraint(record, activeProgram, loaded.model.modelVersion);
|
|
258
274
|
return constraint ? [[record.id, constraint]] : [];
|
|
259
275
|
}));
|
|
260
|
-
const audits = loaded.resources.filter((record) =>
|
|
276
|
+
const audits = loaded.resources.filter((record) => (
|
|
277
|
+
record.type === "audit"
|
|
278
|
+
&& (!modelSupports(loaded.model, "program-scope") || record.programId === activeProgram.id)
|
|
279
|
+
));
|
|
261
280
|
const auditPreparations = await measureTiming("state-audit-preparation", async () => Object.fromEntries(await Promise.all(
|
|
262
281
|
(audits.length ? audits : [null]).map(async (audit) => {
|
|
263
282
|
const preparation = await assessAuditPreparation(loaded, {
|
|
@@ -270,11 +289,13 @@ async function createAppStateUnlocked(input, options) {
|
|
|
270
289
|
})
|
|
271
290
|
)));
|
|
272
291
|
const obligations = planObligations(entries, {
|
|
292
|
+
programId: activeProgram.id,
|
|
273
293
|
asOf,
|
|
274
294
|
now: options.now ?? generatedAt,
|
|
275
295
|
model: loaded.model
|
|
276
296
|
});
|
|
277
297
|
const workflow = await measureTiming("state-workflow", () => assessWorkflow(loaded, {
|
|
298
|
+
programId: activeProgram.id,
|
|
278
299
|
asOf,
|
|
279
300
|
evaluatedAt: generatedAt,
|
|
280
301
|
programReadiness,
|
|
@@ -286,7 +307,7 @@ async function createAppStateUnlocked(input, options) {
|
|
|
286
307
|
validation
|
|
287
308
|
}));
|
|
288
309
|
const collectionReviews = Object.fromEntries(
|
|
289
|
-
assessCollectionReviews(loaded).map((assessment) => [
|
|
310
|
+
assessCollectionReviews(loaded, { programId: activeProgram.id }).map((assessment) => [
|
|
290
311
|
assessment.resourceType,
|
|
291
312
|
{
|
|
292
313
|
resourceType: assessment.resourceType,
|
|
@@ -307,6 +328,7 @@ async function createAppStateUnlocked(input, options) {
|
|
|
307
328
|
readOnly: Boolean(options.readOnly || (repository.mode === "trunk" && !repository.writesAllowed)),
|
|
308
329
|
repository,
|
|
309
330
|
workspace,
|
|
331
|
+
selectedProgramId: activeProgram.id,
|
|
310
332
|
model: loaded.model,
|
|
311
333
|
resources: entries,
|
|
312
334
|
validation: {
|