@pegma/support-desk-application 0.1.0 → 0.2.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/README.md +228 -7
- package/dist/assistant-turn-capacity.d.ts +13 -0
- package/dist/assistant-turn-capacity.d.ts.map +1 -0
- package/dist/assistant-turn-capacity.js +111 -0
- package/dist/assistant-turn-capacity.js.map +1 -0
- package/dist/assistant-turn-records.d.ts +68 -0
- package/dist/assistant-turn-records.d.ts.map +1 -0
- package/dist/assistant-turn-records.js +418 -0
- package/dist/assistant-turn-records.js.map +1 -0
- package/dist/assistant-turn-types.d.ts +133 -0
- package/dist/assistant-turn-types.d.ts.map +1 -0
- package/dist/assistant-turn-types.js +2 -0
- package/dist/assistant-turn-types.js.map +1 -0
- package/dist/assistant-turns.d.ts +42 -0
- package/dist/assistant-turns.d.ts.map +1 -0
- package/dist/assistant-turns.js +640 -0
- package/dist/assistant-turns.js.map +1 -0
- package/dist/engineering-evidence.d.ts +20 -0
- package/dist/engineering-evidence.d.ts.map +1 -0
- package/dist/engineering-evidence.js +899 -0
- package/dist/engineering-evidence.js.map +1 -0
- package/dist/engineering-followup-internal.d.ts +45 -0
- package/dist/engineering-followup-internal.d.ts.map +1 -0
- package/dist/engineering-followup-internal.js +2 -0
- package/dist/engineering-followup-internal.js.map +1 -0
- package/dist/engineering-followup-records.d.ts +209 -0
- package/dist/engineering-followup-records.d.ts.map +1 -0
- package/dist/engineering-followup-records.js +1434 -0
- package/dist/engineering-followup-records.js.map +1 -0
- package/dist/engineering-followup-source.d.ts +168 -0
- package/dist/engineering-followup-source.d.ts.map +1 -0
- package/dist/engineering-followup-source.js +1144 -0
- package/dist/engineering-followup-source.js.map +1 -0
- package/dist/engineering-followup-types.d.ts +364 -0
- package/dist/engineering-followup-types.d.ts.map +1 -0
- package/dist/engineering-followup-types.js +2 -0
- package/dist/engineering-followup-types.js.map +1 -0
- package/dist/engineering-followup-work.d.ts +70 -0
- package/dist/engineering-followup-work.d.ts.map +1 -0
- package/dist/engineering-followup-work.js +1153 -0
- package/dist/engineering-followup-work.js.map +1 -0
- package/dist/engineering-internal.d.ts +38 -0
- package/dist/engineering-internal.d.ts.map +1 -0
- package/dist/engineering-internal.js +11 -0
- package/dist/engineering-internal.js.map +1 -0
- package/dist/engineering-policy.d.ts +47 -0
- package/dist/engineering-policy.d.ts.map +1 -0
- package/dist/engineering-policy.js +579 -0
- package/dist/engineering-policy.js.map +1 -0
- package/dist/engineering-reconciliation.d.ts +6 -0
- package/dist/engineering-reconciliation.d.ts.map +1 -0
- package/dist/engineering-reconciliation.js +464 -0
- package/dist/engineering-reconciliation.js.map +1 -0
- package/dist/engineering-records.d.ts +227 -0
- package/dist/engineering-records.d.ts.map +1 -0
- package/dist/engineering-records.js +1272 -0
- package/dist/engineering-records.js.map +1 -0
- package/dist/engineering-types.d.ts +505 -0
- package/dist/engineering-types.d.ts.map +1 -0
- package/dist/engineering-types.js +2 -0
- package/dist/engineering-types.js.map +1 -0
- package/dist/engineering-work.d.ts +32 -0
- package/dist/engineering-work.d.ts.map +1 -0
- package/dist/engineering-work.js +1757 -0
- package/dist/engineering-work.js.map +1 -0
- package/dist/index.d.ts +609 -14
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3568 -204
- package/dist/index.js.map +1 -1
- package/dist/resolution-learning-records.d.ts +154 -0
- package/dist/resolution-learning-records.d.ts.map +1 -0
- package/dist/resolution-learning-records.js +657 -0
- package/dist/resolution-learning-records.js.map +1 -0
- package/dist/resolution-learning-types.d.ts +356 -0
- package/dist/resolution-learning-types.d.ts.map +1 -0
- package/dist/resolution-learning-types.js +2 -0
- package/dist/resolution-learning-types.js.map +1 -0
- package/dist/resolution-learning-worker.d.ts +21 -0
- package/dist/resolution-learning-worker.d.ts.map +1 -0
- package/dist/resolution-learning-worker.js +1027 -0
- package/dist/resolution-learning-worker.js.map +1 -0
- package/dist/resolution-learning.d.ts +38 -0
- package/dist/resolution-learning.d.ts.map +1 -0
- package/dist/resolution-learning.js +860 -0
- package/dist/resolution-learning.js.map +1 -0
- package/package.json +8 -8
|
@@ -0,0 +1,899 @@
|
|
|
1
|
+
import { hasPermission } from "@pegma/authorization-core";
|
|
2
|
+
import { SupportDeskAuthorizationError, SupportDeskConflictError, SupportDeskLimitError, SupportDeskNotFoundError, supportPermissions, } from "./index.js";
|
|
3
|
+
import { fields, fingerprint, integer, timestamp, text, } from "./assistant-turn-records.js";
|
|
4
|
+
import { checkEngineeringRecordCapacity, advanceEngineeringFollowupRevocationEpoch, createEngineeringSource, engineeringCommandKey, engineeringSourceKey, engineeringWorkKey, safeEngineeringSnapshot as snapshot, validateEngineeringEvidence, validateEngineeringEvidenceInput, validateEngineeringProduct, } from "./engineering-records.js";
|
|
5
|
+
import { projectEngineeringRows } from "./engineering-internal.js";
|
|
6
|
+
class EngineeringTrustUnavailable extends Error {
|
|
7
|
+
}
|
|
8
|
+
/** One original clock/elapsed-time budget covers every awaited trust port. */
|
|
9
|
+
function trustWindow(clock, milliseconds) {
|
|
10
|
+
const started = performance.now();
|
|
11
|
+
const initial = timestamp(clock.now(), "now");
|
|
12
|
+
const controller = new AbortController();
|
|
13
|
+
const now = () => Math.max(timestamp(clock.now(), "now"), initial + performance.now() - started);
|
|
14
|
+
const remaining = () => initial + milliseconds - now();
|
|
15
|
+
const ensure = () => {
|
|
16
|
+
if (controller.signal.aborted || remaining() <= 0)
|
|
17
|
+
throw new EngineeringTrustUnavailable();
|
|
18
|
+
};
|
|
19
|
+
return {
|
|
20
|
+
now,
|
|
21
|
+
close: () => controller.abort(),
|
|
22
|
+
async within(action) {
|
|
23
|
+
ensure();
|
|
24
|
+
let timer;
|
|
25
|
+
try {
|
|
26
|
+
const value = await Promise.race([
|
|
27
|
+
Promise.resolve().then(() => {
|
|
28
|
+
ensure();
|
|
29
|
+
return action(controller.signal);
|
|
30
|
+
}),
|
|
31
|
+
new Promise((_resolve, reject) => {
|
|
32
|
+
timer = setTimeout(() => {
|
|
33
|
+
controller.abort();
|
|
34
|
+
reject(new EngineeringTrustUnavailable());
|
|
35
|
+
}, Math.max(1, Math.ceil(remaining())));
|
|
36
|
+
}),
|
|
37
|
+
]);
|
|
38
|
+
ensure();
|
|
39
|
+
return value;
|
|
40
|
+
}
|
|
41
|
+
finally {
|
|
42
|
+
if (timer !== undefined)
|
|
43
|
+
clearTimeout(timer);
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function permission(access, name, actorId) {
|
|
49
|
+
text(access.principalId, "principalId");
|
|
50
|
+
if (actorId !== undefined && access.principalId !== actorId)
|
|
51
|
+
throw new SupportDeskConflictError("Engineering actor changed");
|
|
52
|
+
for (const required of [name, supportPermissions.queueRead])
|
|
53
|
+
if (!hasPermission(access, required))
|
|
54
|
+
throw new SupportDeskAuthorizationError(required);
|
|
55
|
+
}
|
|
56
|
+
function result(source) {
|
|
57
|
+
return {
|
|
58
|
+
ticketId: source.ticketId,
|
|
59
|
+
sourceGeneration: source.sourceGeneration,
|
|
60
|
+
state: source.state,
|
|
61
|
+
security: source.security,
|
|
62
|
+
...(source.heldReason === undefined
|
|
63
|
+
? {}
|
|
64
|
+
: { heldReason: source.heldReason }),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
function bounded(row) {
|
|
68
|
+
checkEngineeringRecordCapacity(row);
|
|
69
|
+
}
|
|
70
|
+
export async function readEngineeringSourceVersion(records, ticketId) {
|
|
71
|
+
const source = await records.getVersioned(engineeringSourceKey(ticketId));
|
|
72
|
+
if (source === null)
|
|
73
|
+
return null;
|
|
74
|
+
if (source.value.kind !== "engineering_source")
|
|
75
|
+
throw new SupportDeskConflictError("Engineering source key contains another record");
|
|
76
|
+
return { value: source.value, version: source.version };
|
|
77
|
+
}
|
|
78
|
+
function sourceAction(source, next) {
|
|
79
|
+
bounded(next);
|
|
80
|
+
return { action: "putIfUnchanged", version: source.version, value: next };
|
|
81
|
+
}
|
|
82
|
+
function withoutClaim(work) {
|
|
83
|
+
const { claim: _claim, leaseUntil: _lease, retryAt: _retry, ...rest } = work;
|
|
84
|
+
return rest;
|
|
85
|
+
}
|
|
86
|
+
function reconciliationAfterSourceChange(destination) {
|
|
87
|
+
return destination.reconciliation === "confirmed" &&
|
|
88
|
+
["accepted", "confirmed_rejected"].includes(destination.outcome)
|
|
89
|
+
? "confirmed"
|
|
90
|
+
: "pending";
|
|
91
|
+
}
|
|
92
|
+
/** Sensitivity survives workflow-status changes and retained-body transitions. */
|
|
93
|
+
function retainedWorkSecurity(work) {
|
|
94
|
+
if (work.status === "restricted" ||
|
|
95
|
+
work.security === "restricted" ||
|
|
96
|
+
work.candidate?.security === "restricted")
|
|
97
|
+
return "restricted";
|
|
98
|
+
if (work.security === "uncertain" || work.candidate?.security === "uncertain")
|
|
99
|
+
return "uncertain";
|
|
100
|
+
return "ordinary";
|
|
101
|
+
}
|
|
102
|
+
function invalidatedWork(work, reason) {
|
|
103
|
+
const { approval: _approval, ...rest } = withoutClaim(work);
|
|
104
|
+
const status = work.status === "accepted" || work.status === "confirmed_rejected"
|
|
105
|
+
? work.status
|
|
106
|
+
: work.dispatch === undefined
|
|
107
|
+
? "superseded"
|
|
108
|
+
: "unknown";
|
|
109
|
+
return {
|
|
110
|
+
...rest,
|
|
111
|
+
security: retainedWorkSecurity(work),
|
|
112
|
+
status,
|
|
113
|
+
...(work.dispatch === undefined ? {} : { stage: "reconcile" }),
|
|
114
|
+
heldReason: reason,
|
|
115
|
+
workGeneration: work.workGeneration + 1,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
/** Persisted enrollment participates even when new engineering is disabled. */
|
|
119
|
+
export async function planEngineeringTicketMutation(records, before, after, hint) {
|
|
120
|
+
const source = await readEngineeringSourceVersion(records, after.id);
|
|
121
|
+
const reopened = ["resolved", "closed"].includes(before.value.ticket.status) &&
|
|
122
|
+
!["resolved", "closed"].includes(after.status);
|
|
123
|
+
if (source === null || (!hint.visibleChange && !reopened))
|
|
124
|
+
return { actions: [], additionalRows: 0 };
|
|
125
|
+
const next = {
|
|
126
|
+
...source.value,
|
|
127
|
+
sourceGeneration: source.value.sourceGeneration + 1,
|
|
128
|
+
workGeneration: source.value.workGeneration + 1,
|
|
129
|
+
state: source.value.state === "withdrawn"
|
|
130
|
+
? "withdrawn"
|
|
131
|
+
: source.value.state === "restricted"
|
|
132
|
+
? "restricted"
|
|
133
|
+
: "held",
|
|
134
|
+
heldReason: source.value.state === "withdrawn"
|
|
135
|
+
? (source.value.heldReason ?? "withdrawn")
|
|
136
|
+
: reopened
|
|
137
|
+
? "reopened"
|
|
138
|
+
: "visible_source_changed",
|
|
139
|
+
updatedAt: after.updatedAt,
|
|
140
|
+
reconciliationPending: source.value.destinations.some((destination) => reconciliationAfterSourceChange(destination) !== "confirmed"),
|
|
141
|
+
destinations: source.value.destinations.map((destination) => ({
|
|
142
|
+
...destination,
|
|
143
|
+
reconciliation: reconciliationAfterSourceChange(destination),
|
|
144
|
+
})),
|
|
145
|
+
};
|
|
146
|
+
const actions = [sourceAction(source, next)];
|
|
147
|
+
// Older generations already fail the source fence. Only this currently live
|
|
148
|
+
// generation can retain a claim or approval invalidated by the ticket change.
|
|
149
|
+
const work = await records.getVersioned(engineeringWorkKey(after.id, source.value.sourceGeneration));
|
|
150
|
+
if (work !== null) {
|
|
151
|
+
if (work.value.kind !== "engineering_work")
|
|
152
|
+
throw new SupportDeskConflictError();
|
|
153
|
+
const changed = invalidatedWork(work.value, next.heldReason);
|
|
154
|
+
bounded(changed);
|
|
155
|
+
actions.push({
|
|
156
|
+
action: "putIfUnchanged",
|
|
157
|
+
version: work.version,
|
|
158
|
+
value: changed,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
return { actions, additionalRows: 0 };
|
|
162
|
+
}
|
|
163
|
+
function opaqueAcknowledgement(ack) {
|
|
164
|
+
if (ack === undefined)
|
|
165
|
+
return undefined;
|
|
166
|
+
const { url: _url, ...identity } = ack;
|
|
167
|
+
return identity;
|
|
168
|
+
}
|
|
169
|
+
function scrubbedWork(work, reason) {
|
|
170
|
+
const request = work.withdrawnRequest ??
|
|
171
|
+
(work.proposal === undefined
|
|
172
|
+
? undefined
|
|
173
|
+
: {
|
|
174
|
+
operation: work.proposal.request.operation,
|
|
175
|
+
repositoryId: work.proposal.request.repository.repositoryId,
|
|
176
|
+
repositoryMetadataHash: work.proposal.repositoryMetadataHash,
|
|
177
|
+
exportMarker: work.proposal.request.bundle.exportMarker,
|
|
178
|
+
...(work.proposal.request.operation === "link_existing_issue"
|
|
179
|
+
? {
|
|
180
|
+
issueId: work.proposal.request.issueId,
|
|
181
|
+
issueNumber: work.proposal.request.issueNumber,
|
|
182
|
+
}
|
|
183
|
+
: {}),
|
|
184
|
+
});
|
|
185
|
+
const acknowledgement = opaqueAcknowledgement(work.acknowledgement);
|
|
186
|
+
const next = {
|
|
187
|
+
kind: work.kind,
|
|
188
|
+
partition: work.partition,
|
|
189
|
+
id: work.id,
|
|
190
|
+
ticketId: work.ticketId,
|
|
191
|
+
sourceGeneration: work.sourceGeneration,
|
|
192
|
+
sourceBindingHash: work.sourceBindingHash,
|
|
193
|
+
workGeneration: work.workGeneration + 1,
|
|
194
|
+
acceptedTicketRevision: work.acceptedTicketRevision,
|
|
195
|
+
createdAt: work.createdAt,
|
|
196
|
+
deadline: work.deadline,
|
|
197
|
+
security: retainedWorkSecurity(work),
|
|
198
|
+
status: work.status === "accepted" || work.status === "confirmed_rejected"
|
|
199
|
+
? work.status
|
|
200
|
+
: work.dispatch === undefined
|
|
201
|
+
? "held"
|
|
202
|
+
: "unknown",
|
|
203
|
+
stage: work.dispatch === undefined ? work.stage : "reconcile",
|
|
204
|
+
modelAttempts: [],
|
|
205
|
+
claimGeneration: work.claimGeneration,
|
|
206
|
+
proposalCount: work.proposalCount,
|
|
207
|
+
heldReason: reason,
|
|
208
|
+
terminalReserveRows: work.terminalReserveRows,
|
|
209
|
+
usageTombstones: work.usageTombstones ??
|
|
210
|
+
work.modelAttempts.map((attempt) => ({
|
|
211
|
+
stepId: attempt.stepId,
|
|
212
|
+
requestHash: attempt.requestHash,
|
|
213
|
+
usageReservationId: attempt.binding.usageReservationId,
|
|
214
|
+
accounting: attempt.accounting,
|
|
215
|
+
})),
|
|
216
|
+
...(work.stepId === undefined ? {} : { stepId: work.stepId }),
|
|
217
|
+
...(work.requestHash === undefined
|
|
218
|
+
? {}
|
|
219
|
+
: { requestHash: work.requestHash }),
|
|
220
|
+
...(work.dispatch === undefined ? {} : { dispatch: work.dispatch }),
|
|
221
|
+
...(acknowledgement === undefined ? {} : { acknowledgement }),
|
|
222
|
+
...(request === undefined ? {} : { withdrawnRequest: request }),
|
|
223
|
+
...(work.lastCompletion === undefined
|
|
224
|
+
? {}
|
|
225
|
+
: { lastCompletion: work.lastCompletion }),
|
|
226
|
+
...(work.lastFinish === undefined ? {} : { lastFinish: work.lastFinish }),
|
|
227
|
+
...(work.lastDispatchResult === undefined
|
|
228
|
+
? {}
|
|
229
|
+
: { lastDispatchResult: work.lastDispatchResult }),
|
|
230
|
+
};
|
|
231
|
+
bounded(next);
|
|
232
|
+
return next;
|
|
233
|
+
}
|
|
234
|
+
function requiresSecurityReview(source, security) {
|
|
235
|
+
return (security === "ordinary" &&
|
|
236
|
+
(source.state === "restricted" ||
|
|
237
|
+
(source.security !== "ordinary" &&
|
|
238
|
+
(source.evidence !== undefined || source.generationCount > 0))));
|
|
239
|
+
}
|
|
240
|
+
function privateRead(access, security, actorId) {
|
|
241
|
+
permission(access, supportPermissions.engineeringRead, actorId);
|
|
242
|
+
if (security !== "ordinary")
|
|
243
|
+
permission(access, supportPermissions.engineeringRestrictedRead, actorId);
|
|
244
|
+
}
|
|
245
|
+
export function createEngineeringEvidenceServices(deps) {
|
|
246
|
+
const { records, clock, options } = deps;
|
|
247
|
+
const load = async (ticketId) => {
|
|
248
|
+
text(ticketId, "ticketId");
|
|
249
|
+
const ticket = await records.getVersioned({
|
|
250
|
+
partition: ticketId,
|
|
251
|
+
id: "ticket",
|
|
252
|
+
});
|
|
253
|
+
if (ticket?.value.kind !== "ticket")
|
|
254
|
+
throw new SupportDeskNotFoundError();
|
|
255
|
+
const source = await readEngineeringSourceVersion(records, ticketId);
|
|
256
|
+
if (source === null)
|
|
257
|
+
throw new SupportDeskNotFoundError();
|
|
258
|
+
const rows = await records.list(ticketId);
|
|
259
|
+
if (rows.length > deps.limits.maxRecordsPerTicket)
|
|
260
|
+
throw new SupportDeskLimitError("ticket_partition", deps.limits.maxRecordsPerTicket);
|
|
261
|
+
return {
|
|
262
|
+
ticket: { value: ticket.value, version: ticket.version },
|
|
263
|
+
source,
|
|
264
|
+
rows,
|
|
265
|
+
};
|
|
266
|
+
};
|
|
267
|
+
function validateCommand(command) {
|
|
268
|
+
for (const name of ["ticketId", "commandId", "correlationId"])
|
|
269
|
+
text(command[name], name);
|
|
270
|
+
}
|
|
271
|
+
function receipt(command, hash, value) {
|
|
272
|
+
return {
|
|
273
|
+
action: "insert",
|
|
274
|
+
value: {
|
|
275
|
+
kind: "engineering_receipt",
|
|
276
|
+
partition: command.ticketId,
|
|
277
|
+
id: engineeringCommandKey(command.ticketId, command.commandId).id,
|
|
278
|
+
fingerprint: hash,
|
|
279
|
+
result: value,
|
|
280
|
+
},
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
async function replay(command, hash) {
|
|
284
|
+
const stored = await records.get(engineeringCommandKey(command.ticketId, command.commandId));
|
|
285
|
+
if (stored === null)
|
|
286
|
+
return null;
|
|
287
|
+
if (stored.kind !== "engineering_receipt" ||
|
|
288
|
+
stored.fingerprint !== hash ||
|
|
289
|
+
!("state" in stored.result) ||
|
|
290
|
+
!("security" in stored.result))
|
|
291
|
+
throw new SupportDeskConflictError("Engineering command identity reused");
|
|
292
|
+
if ((await readEngineeringSourceVersion(records, command.ticketId)) === null)
|
|
293
|
+
throw new SupportDeskNotFoundError();
|
|
294
|
+
return snapshot(stored.result);
|
|
295
|
+
}
|
|
296
|
+
async function replayAuthorized(access, name, actorId, command, hash) {
|
|
297
|
+
permission(access, name, actorId);
|
|
298
|
+
const previous = await replay(command, hash);
|
|
299
|
+
permission(access, name, actorId);
|
|
300
|
+
return previous;
|
|
301
|
+
}
|
|
302
|
+
const guardedTicket = (loaded) => ({
|
|
303
|
+
action: "putIfUnchanged",
|
|
304
|
+
value: loaded.ticket.value,
|
|
305
|
+
version: loaded.ticket.version,
|
|
306
|
+
});
|
|
307
|
+
async function invalidate(access, input, privacy) {
|
|
308
|
+
const name = privacy
|
|
309
|
+
? supportPermissions.engineeringWithdraw
|
|
310
|
+
: supportPermissions.engineeringEvidenceSubmit;
|
|
311
|
+
permission(access, name);
|
|
312
|
+
const actorId = access.principalId;
|
|
313
|
+
const command = snapshot(fields(input, "engineering invalidation", [
|
|
314
|
+
"ticketId",
|
|
315
|
+
"commandId",
|
|
316
|
+
"correlationId",
|
|
317
|
+
"reason",
|
|
318
|
+
]));
|
|
319
|
+
validateCommand(command);
|
|
320
|
+
if (!(privacy
|
|
321
|
+
? ["privacy_withdrawal", "source_access_revoked", "source_deleted"]
|
|
322
|
+
: [
|
|
323
|
+
"evidence_revoked",
|
|
324
|
+
"product_binding_revoked",
|
|
325
|
+
"check_policy_revoked",
|
|
326
|
+
]).includes(command.reason))
|
|
327
|
+
throw new TypeError("Invalid engineering invalidation reason");
|
|
328
|
+
const hash = await fingerprint({
|
|
329
|
+
operation: privacy ? "withdraw" : "invalidate",
|
|
330
|
+
actorId,
|
|
331
|
+
command,
|
|
332
|
+
});
|
|
333
|
+
for (let attempt = 0; attempt < deps.maxConflictAttempts; attempt += 1) {
|
|
334
|
+
const previous = await replayAuthorized(access, name, actorId, command, hash);
|
|
335
|
+
if (previous !== null)
|
|
336
|
+
return previous;
|
|
337
|
+
const loaded = await load(command.ticketId);
|
|
338
|
+
if (loaded.source.value.state === "withdrawn") {
|
|
339
|
+
permission(access, name, actorId);
|
|
340
|
+
return result(loaded.source.value);
|
|
341
|
+
}
|
|
342
|
+
const source = loaded.source.value;
|
|
343
|
+
const now = clock.now();
|
|
344
|
+
const { evidence: _evidence, product: _product, proofIdentity: _proof, evidenceFingerprint: _evidenceHash, productContextFingerprint: _productHash, sourceBindingHash: _binding, acceptedTicketRevision: _revision, lastVisibleMessageId: _visible, pendingEvidenceReference: _pending, ...minimal } = source;
|
|
345
|
+
const next = {
|
|
346
|
+
...(privacy ? minimal : source),
|
|
347
|
+
state: privacy
|
|
348
|
+
? "withdrawn"
|
|
349
|
+
: source.security === "ordinary"
|
|
350
|
+
? "held"
|
|
351
|
+
: "restricted",
|
|
352
|
+
heldReason: command.reason,
|
|
353
|
+
sourceGeneration: source.sourceGeneration + 1,
|
|
354
|
+
followupRevocationEpoch: advanceEngineeringFollowupRevocationEpoch(source),
|
|
355
|
+
workGeneration: source.workGeneration + 1,
|
|
356
|
+
updatedAt: now,
|
|
357
|
+
destinations: source.destinations.map((entry) => {
|
|
358
|
+
const acknowledgement = privacy
|
|
359
|
+
? opaqueAcknowledgement(entry.acknowledgement)
|
|
360
|
+
: entry.acknowledgement;
|
|
361
|
+
const { acknowledgement: _ack, lastReconciliation: last, ...identity } = entry;
|
|
362
|
+
return {
|
|
363
|
+
...identity,
|
|
364
|
+
reconciliation: reconciliationAfterSourceChange(entry),
|
|
365
|
+
...(acknowledgement === undefined ? {} : { acknowledgement }),
|
|
366
|
+
...(last === undefined
|
|
367
|
+
? {}
|
|
368
|
+
: {
|
|
369
|
+
lastReconciliation: privacy
|
|
370
|
+
? {
|
|
371
|
+
fingerprint: last.fingerprint,
|
|
372
|
+
outcome: last.outcome,
|
|
373
|
+
observedAt: last.observedAt,
|
|
374
|
+
}
|
|
375
|
+
: last,
|
|
376
|
+
}),
|
|
377
|
+
};
|
|
378
|
+
}),
|
|
379
|
+
reconciliationPending: source.destinations.some((destination) => reconciliationAfterSourceChange(destination) !== "confirmed"),
|
|
380
|
+
};
|
|
381
|
+
const followup = await deps.planFollowupSourceChange({
|
|
382
|
+
rows: loaded.rows,
|
|
383
|
+
previousSource: source,
|
|
384
|
+
nextSource: next,
|
|
385
|
+
reason: privacy ? "withdrawal" : "explicit_revocation",
|
|
386
|
+
});
|
|
387
|
+
const plannedRows = projectEngineeringRows(loaded.rows, followup.nextWorkRows);
|
|
388
|
+
if (privacy)
|
|
389
|
+
deps.withdrawalAdmission(loaded.ticket.value.ticket, plannedRows, 2);
|
|
390
|
+
else
|
|
391
|
+
deps.admission(loaded.ticket.value.ticket, plannedRows, 2);
|
|
392
|
+
const actions = [
|
|
393
|
+
guardedTicket(loaded),
|
|
394
|
+
sourceAction(loaded.source, next),
|
|
395
|
+
...followup.actions,
|
|
396
|
+
];
|
|
397
|
+
const workRows = loaded.rows.filter((row) => row.kind === "engineering_work");
|
|
398
|
+
if (workRows.length > 8)
|
|
399
|
+
throw new SupportDeskConflictError("Engineering history exceeds its bound");
|
|
400
|
+
for (const row of workRows) {
|
|
401
|
+
const work = await records.getVersioned({
|
|
402
|
+
partition: command.ticketId,
|
|
403
|
+
id: row.id,
|
|
404
|
+
});
|
|
405
|
+
if (work?.value.kind !== "engineering_work")
|
|
406
|
+
throw new SupportDeskConflictError();
|
|
407
|
+
const changed = privacy
|
|
408
|
+
? scrubbedWork(work.value, command.reason)
|
|
409
|
+
: invalidatedWork(work.value, command.reason);
|
|
410
|
+
bounded(changed);
|
|
411
|
+
actions.push({
|
|
412
|
+
action: "putIfUnchanged",
|
|
413
|
+
version: work.version,
|
|
414
|
+
value: changed,
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
actions.push(deps.audit(command, loaded.ticket.value.ticket, actorId, now, privacy
|
|
418
|
+
? "support.engineering.withdrawn"
|
|
419
|
+
: "support.engineering.evidence_invalidated"), receipt(command, hash, result(next)));
|
|
420
|
+
permission(access, name, actorId);
|
|
421
|
+
if ((await records.transact(command.ticketId, actions)).committed) {
|
|
422
|
+
permission(access, name, actorId);
|
|
423
|
+
return result(next);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
const previous = await replayAuthorized(access, name, actorId, command, hash);
|
|
427
|
+
if (previous !== null)
|
|
428
|
+
return previous;
|
|
429
|
+
throw new SupportDeskConflictError();
|
|
430
|
+
}
|
|
431
|
+
const application = {
|
|
432
|
+
async enrollEngineeringSource(access, input) {
|
|
433
|
+
const name = supportPermissions.engineeringEvidenceSubmit;
|
|
434
|
+
permission(access, name);
|
|
435
|
+
const actorId = access.principalId;
|
|
436
|
+
const command = snapshot(fields(input, "engineering enrollment", [
|
|
437
|
+
"ticketId",
|
|
438
|
+
"commandId",
|
|
439
|
+
"correlationId",
|
|
440
|
+
"expectedRevision",
|
|
441
|
+
]));
|
|
442
|
+
validateCommand(command);
|
|
443
|
+
integer(command.expectedRevision, "expectedRevision", 1);
|
|
444
|
+
const hash = await fingerprint({ operation: "enroll", actorId, command });
|
|
445
|
+
for (let attempt = 0; attempt < deps.maxConflictAttempts; attempt += 1) {
|
|
446
|
+
const previous = await replayAuthorized(access, name, actorId, command, hash);
|
|
447
|
+
if (previous !== null)
|
|
448
|
+
return previous;
|
|
449
|
+
const ticket = await records.getVersioned({
|
|
450
|
+
partition: command.ticketId,
|
|
451
|
+
id: "ticket",
|
|
452
|
+
});
|
|
453
|
+
if (ticket?.value.kind !== "ticket")
|
|
454
|
+
throw new SupportDeskNotFoundError();
|
|
455
|
+
const existing = await readEngineeringSourceVersion(records, command.ticketId);
|
|
456
|
+
if (existing !== null) {
|
|
457
|
+
permission(access, name, actorId);
|
|
458
|
+
return result(existing.value);
|
|
459
|
+
}
|
|
460
|
+
if (!options.enabled)
|
|
461
|
+
throw new SupportDeskConflictError("Engineering enrollment disabled");
|
|
462
|
+
if (ticket.value.ticket.revision !== command.expectedRevision)
|
|
463
|
+
throw new SupportDeskConflictError("Engineering ticket revision changed");
|
|
464
|
+
const rows = await records.list(command.ticketId);
|
|
465
|
+
const created = await createEngineeringSource(ticket.value.ticket);
|
|
466
|
+
deps.admission(ticket.value.ticket, [...rows, created], 2);
|
|
467
|
+
const now = clock.now();
|
|
468
|
+
const actions = [
|
|
469
|
+
{
|
|
470
|
+
action: "putIfUnchanged",
|
|
471
|
+
version: ticket.version,
|
|
472
|
+
value: ticket.value,
|
|
473
|
+
},
|
|
474
|
+
{ action: "insert", value: created },
|
|
475
|
+
deps.audit(command, ticket.value.ticket, actorId, now, "support.engineering.enrolled"),
|
|
476
|
+
receipt(command, hash, result(created)),
|
|
477
|
+
];
|
|
478
|
+
permission(access, name, actorId);
|
|
479
|
+
if ((await records.transact(command.ticketId, actions)).committed) {
|
|
480
|
+
permission(access, name, actorId);
|
|
481
|
+
return result(created);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
const previous = await replayAuthorized(access, name, actorId, command, hash);
|
|
485
|
+
if (previous !== null)
|
|
486
|
+
return previous;
|
|
487
|
+
throw new SupportDeskConflictError();
|
|
488
|
+
},
|
|
489
|
+
async submitEngineeringEvidence(access, input) {
|
|
490
|
+
const name = supportPermissions.engineeringEvidenceSubmit;
|
|
491
|
+
permission(access, name);
|
|
492
|
+
const actorId = access.principalId;
|
|
493
|
+
const raw = fields(input, "engineering evidence command", [
|
|
494
|
+
"ticketId",
|
|
495
|
+
"commandId",
|
|
496
|
+
"correlationId",
|
|
497
|
+
"expectedRevision",
|
|
498
|
+
"evidence",
|
|
499
|
+
"security",
|
|
500
|
+
]);
|
|
501
|
+
// Bound the closed mismatch before copying any caller-owned object graph.
|
|
502
|
+
validateCommand(raw);
|
|
503
|
+
integer(raw.expectedRevision, "expectedRevision", 1);
|
|
504
|
+
validateEngineeringEvidenceInput(raw.evidence);
|
|
505
|
+
if (!["ordinary", "restricted", "uncertain"].includes(raw.security))
|
|
506
|
+
throw new TypeError("Invalid engineering security classification");
|
|
507
|
+
const command = snapshot(raw);
|
|
508
|
+
validateCommand(command);
|
|
509
|
+
integer(command.expectedRevision, "expectedRevision", 1);
|
|
510
|
+
validateEngineeringEvidenceInput(command.evidence);
|
|
511
|
+
if (!["ordinary", "restricted", "uncertain"].includes(command.security))
|
|
512
|
+
throw new TypeError("Invalid engineering security classification");
|
|
513
|
+
const hash = await fingerprint({
|
|
514
|
+
operation: "evidence",
|
|
515
|
+
actorId,
|
|
516
|
+
command,
|
|
517
|
+
});
|
|
518
|
+
for (let attempt = 0; attempt < deps.maxConflictAttempts; attempt += 1) {
|
|
519
|
+
const previous = await replayAuthorized(access, name, actorId, command, hash);
|
|
520
|
+
if (previous !== null)
|
|
521
|
+
return previous;
|
|
522
|
+
const loaded = await load(command.ticketId);
|
|
523
|
+
const source = loaded.source.value;
|
|
524
|
+
const ticket = loaded.ticket.value.ticket;
|
|
525
|
+
if (source.state === "withdrawn") {
|
|
526
|
+
permission(access, name, actorId);
|
|
527
|
+
return result(source);
|
|
528
|
+
}
|
|
529
|
+
if (!options.enabled)
|
|
530
|
+
throw new SupportDeskConflictError("Engineering processing disabled");
|
|
531
|
+
if (ticket.revision !== command.expectedRevision)
|
|
532
|
+
throw new SupportDeskConflictError("Engineering ticket revision changed");
|
|
533
|
+
const clearing = requiresSecurityReview(source, command.security);
|
|
534
|
+
if (clearing)
|
|
535
|
+
permission(access, supportPermissions.engineeringSecurityReview, actorId);
|
|
536
|
+
const visible = loaded.rows.filter((row) => row.kind === "message" && row.message.visibility === "customer");
|
|
537
|
+
const latest = [...visible].sort((left, right) => right.ordinal - left.ordinal)[0];
|
|
538
|
+
if (latest === undefined ||
|
|
539
|
+
command.evidence.visibleMessageIds.some((id) => !visible.some((message) => message.message.id === id &&
|
|
540
|
+
message.ordinal <= ticket.revision)))
|
|
541
|
+
throw new SupportDeskConflictError("Engineering evidence must identify actual visible messages");
|
|
542
|
+
const now = clock.now();
|
|
543
|
+
const trust = {
|
|
544
|
+
ticketId: ticket.id,
|
|
545
|
+
ticketRevision: ticket.revision,
|
|
546
|
+
sourceGeneration: source.sourceGeneration,
|
|
547
|
+
actorId,
|
|
548
|
+
acceptedAt: now,
|
|
549
|
+
};
|
|
550
|
+
const mismatch = {
|
|
551
|
+
expectedBehavior: command.evidence.expectedBehavior,
|
|
552
|
+
actualBehavior: command.evidence.actualBehavior,
|
|
553
|
+
reproductionSteps: command.evidence.reproductionSteps,
|
|
554
|
+
visibleMessageIds: command.evidence.visibleMessageIds,
|
|
555
|
+
};
|
|
556
|
+
const window = trustWindow(clock, options.trustTimeoutMilliseconds);
|
|
557
|
+
let product = null;
|
|
558
|
+
let evidence = null;
|
|
559
|
+
let heldReason = "product_context_unavailable";
|
|
560
|
+
try {
|
|
561
|
+
if (options.productContext !== undefined) {
|
|
562
|
+
const resolved = await window.within((signal) => options.productContext.resolve(snapshot(trust), signal));
|
|
563
|
+
if (resolved !== null) {
|
|
564
|
+
product = snapshot(resolved);
|
|
565
|
+
validateEngineeringProduct(product);
|
|
566
|
+
if (timestamp(product.verifiedAt, "product.verifiedAt") >
|
|
567
|
+
window.now() ||
|
|
568
|
+
timestamp(product.validUntil, "product.validUntil") <=
|
|
569
|
+
window.now())
|
|
570
|
+
product = null;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
if (product !== null) {
|
|
574
|
+
if (command.evidence.kind === "staff_verified_failure") {
|
|
575
|
+
evidence = {
|
|
576
|
+
...mismatch,
|
|
577
|
+
kind: command.evidence.kind,
|
|
578
|
+
verificationReference: command.evidence.verificationReference,
|
|
579
|
+
actorId,
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
else {
|
|
583
|
+
heldReason = "failed_test_unverified";
|
|
584
|
+
if (options.testReceipts !== undefined) {
|
|
585
|
+
const receiptId = command.evidence.receipt;
|
|
586
|
+
const test = await window.within((signal) => options.testReceipts.verify(receiptId, snapshot({
|
|
587
|
+
...trust,
|
|
588
|
+
product: product,
|
|
589
|
+
evidence: mismatch,
|
|
590
|
+
}), signal));
|
|
591
|
+
if (test !== null) {
|
|
592
|
+
const proposed = {
|
|
593
|
+
...mismatch,
|
|
594
|
+
kind: "failed_test",
|
|
595
|
+
actorId,
|
|
596
|
+
test: snapshot(test),
|
|
597
|
+
};
|
|
598
|
+
validateEngineeringEvidence(proposed);
|
|
599
|
+
if (test.productBinding === product.binding &&
|
|
600
|
+
timestamp(test.executedAt, "test.executedAt") <=
|
|
601
|
+
timestamp(test.verifiedAt, "test.verifiedAt") &&
|
|
602
|
+
timestamp(test.verifiedAt, "test.verifiedAt") <=
|
|
603
|
+
window.now() &&
|
|
604
|
+
timestamp(test.validUntil, "test.validUntil") > window.now())
|
|
605
|
+
evidence = proposed;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
if (evidence !== null)
|
|
610
|
+
validateEngineeringEvidence(evidence);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
catch {
|
|
614
|
+
product = null;
|
|
615
|
+
evidence = null;
|
|
616
|
+
}
|
|
617
|
+
finally {
|
|
618
|
+
window.close();
|
|
619
|
+
}
|
|
620
|
+
const stableProduct = product === null
|
|
621
|
+
? null
|
|
622
|
+
: {
|
|
623
|
+
productId: product.productId,
|
|
624
|
+
version: product.version,
|
|
625
|
+
buildId: product.buildId,
|
|
626
|
+
binding: product.binding,
|
|
627
|
+
contextVersion: product.contextVersion,
|
|
628
|
+
};
|
|
629
|
+
const stableEvidence = evidence === null
|
|
630
|
+
? null
|
|
631
|
+
: evidence.kind === "staff_verified_failure"
|
|
632
|
+
? evidence
|
|
633
|
+
: {
|
|
634
|
+
...mismatch,
|
|
635
|
+
kind: evidence.kind,
|
|
636
|
+
actorId,
|
|
637
|
+
executionId: evidence.test.executionId,
|
|
638
|
+
runnerId: evidence.test.runnerId,
|
|
639
|
+
testId: evidence.test.testId,
|
|
640
|
+
productBinding: evidence.test.productBinding,
|
|
641
|
+
};
|
|
642
|
+
const evidenceFingerprint = await fingerprint(stableEvidence);
|
|
643
|
+
const productContextFingerprint = await fingerprint(stableProduct);
|
|
644
|
+
const proofIdentity = await fingerprint({
|
|
645
|
+
evidence: stableEvidence,
|
|
646
|
+
product: stableProduct,
|
|
647
|
+
lastVisibleMessageId: latest.message.id,
|
|
648
|
+
security: command.security,
|
|
649
|
+
});
|
|
650
|
+
const trusted = product !== null &&
|
|
651
|
+
evidence !== null &&
|
|
652
|
+
timestamp(product.validUntil, "product.validUntil") > window.now() &&
|
|
653
|
+
(evidence.kind !== "failed_test" ||
|
|
654
|
+
timestamp(evidence.test.validUntil, "test.validUntil") >
|
|
655
|
+
window.now());
|
|
656
|
+
let next = source;
|
|
657
|
+
let work;
|
|
658
|
+
if (!(trusted &&
|
|
659
|
+
["eligible", "restricted"].includes(source.state) &&
|
|
660
|
+
source.security === command.security &&
|
|
661
|
+
!clearing &&
|
|
662
|
+
source.proofIdentity === proofIdentity)) {
|
|
663
|
+
const { heldReason: _held, pendingEvidenceReference: _pending, evidence: _evidence, product: _product, proofIdentity: _proof, evidenceFingerprint: _evidenceHash, productContextFingerprint: _productHash, sourceBindingHash: _binding, acceptedTicketRevision: _accepted, lastVisibleMessageId: _visible, ...base } = source;
|
|
664
|
+
const sourceGeneration = source.sourceGeneration + 1;
|
|
665
|
+
const sourceBindingHash = await fingerprint({
|
|
666
|
+
ticketId: ticket.id,
|
|
667
|
+
sourceGeneration,
|
|
668
|
+
evidenceFingerprint,
|
|
669
|
+
productContextFingerprint,
|
|
670
|
+
proofIdentity,
|
|
671
|
+
});
|
|
672
|
+
next = {
|
|
673
|
+
...base,
|
|
674
|
+
sourceGeneration,
|
|
675
|
+
workGeneration: source.workGeneration + 1,
|
|
676
|
+
security: command.security,
|
|
677
|
+
updatedAt: now,
|
|
678
|
+
state: trusted
|
|
679
|
+
? command.security === "ordinary"
|
|
680
|
+
? "eligible"
|
|
681
|
+
: "restricted"
|
|
682
|
+
: command.security === "ordinary"
|
|
683
|
+
? "held"
|
|
684
|
+
: "restricted",
|
|
685
|
+
...(trusted
|
|
686
|
+
? {
|
|
687
|
+
proofIdentity,
|
|
688
|
+
evidenceFingerprint,
|
|
689
|
+
productContextFingerprint,
|
|
690
|
+
sourceBindingHash,
|
|
691
|
+
acceptedTicketRevision: ticket.revision,
|
|
692
|
+
lastVisibleMessageId: latest.message.id,
|
|
693
|
+
evidence: evidence,
|
|
694
|
+
product: product,
|
|
695
|
+
generationCount: source.generationCount + 1,
|
|
696
|
+
}
|
|
697
|
+
: { heldReason, pendingEvidenceReference: command.commandId }),
|
|
698
|
+
destinations: source.destinations.map((destination) => ({
|
|
699
|
+
...destination,
|
|
700
|
+
reconciliation: reconciliationAfterSourceChange(destination),
|
|
701
|
+
})),
|
|
702
|
+
reconciliationPending: source.destinations.some((destination) => reconciliationAfterSourceChange(destination) !== "confirmed"),
|
|
703
|
+
};
|
|
704
|
+
if (trusted && source.generationCount >= options.maxGenerations) {
|
|
705
|
+
next = {
|
|
706
|
+
...next,
|
|
707
|
+
generationCount: source.generationCount,
|
|
708
|
+
state: "held",
|
|
709
|
+
heldReason: "engineering_capacity",
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
else if (trusted && command.security === "ordinary") {
|
|
713
|
+
work = {
|
|
714
|
+
kind: "engineering_work",
|
|
715
|
+
partition: ticket.id,
|
|
716
|
+
id: engineeringWorkKey(ticket.id, sourceGeneration).id,
|
|
717
|
+
ticketId: ticket.id,
|
|
718
|
+
sourceGeneration,
|
|
719
|
+
sourceBindingHash,
|
|
720
|
+
workGeneration: 1,
|
|
721
|
+
acceptedTicketRevision: ticket.revision,
|
|
722
|
+
createdAt: now,
|
|
723
|
+
deadline: new Date(timestamp(now, "now") + options.maxWorkMilliseconds).toISOString(),
|
|
724
|
+
security: command.security,
|
|
725
|
+
evidence: evidence,
|
|
726
|
+
product: product,
|
|
727
|
+
status: "pending",
|
|
728
|
+
stage: "model",
|
|
729
|
+
modelAttempts: [],
|
|
730
|
+
claimGeneration: 0,
|
|
731
|
+
proposalCount: 0,
|
|
732
|
+
terminalReserveRows: 0,
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
const securityRestriction = next.security !== "ordinary" &&
|
|
737
|
+
(next.sourceGeneration !== source.sourceGeneration ||
|
|
738
|
+
next.security !== source.security ||
|
|
739
|
+
source.state !== "restricted");
|
|
740
|
+
if (securityRestriction)
|
|
741
|
+
next = {
|
|
742
|
+
...next,
|
|
743
|
+
followupRevocationEpoch: advanceEngineeringFollowupRevocationEpoch(source),
|
|
744
|
+
};
|
|
745
|
+
else if (trusted && next.followupRevocationEpoch === undefined)
|
|
746
|
+
next = { ...next, followupRevocationEpoch: 0 };
|
|
747
|
+
const followup = securityRestriction
|
|
748
|
+
? await deps.planFollowupSourceChange({
|
|
749
|
+
rows: loaded.rows,
|
|
750
|
+
previousSource: source,
|
|
751
|
+
nextSource: next,
|
|
752
|
+
reason: "security_restriction",
|
|
753
|
+
})
|
|
754
|
+
: { actions: [], nextWorkRows: [] };
|
|
755
|
+
const plannedRows = projectEngineeringRows(loaded.rows, followup.nextWorkRows);
|
|
756
|
+
try {
|
|
757
|
+
bounded(next);
|
|
758
|
+
if (work !== undefined) {
|
|
759
|
+
bounded(work);
|
|
760
|
+
deps.admission(ticket, [...plannedRows, work], 2);
|
|
761
|
+
}
|
|
762
|
+
else
|
|
763
|
+
deps.admission(ticket, plannedRows, 2);
|
|
764
|
+
}
|
|
765
|
+
catch (error) {
|
|
766
|
+
if (!(error instanceof SupportDeskLimitError) || work === undefined)
|
|
767
|
+
throw error;
|
|
768
|
+
work = undefined;
|
|
769
|
+
next = {
|
|
770
|
+
...next,
|
|
771
|
+
state: "held",
|
|
772
|
+
heldReason: "engineering_capacity",
|
|
773
|
+
generationCount: source.generationCount,
|
|
774
|
+
};
|
|
775
|
+
bounded(next);
|
|
776
|
+
deps.admission(ticket, plannedRows, 2);
|
|
777
|
+
}
|
|
778
|
+
const actions = [
|
|
779
|
+
guardedTicket(loaded),
|
|
780
|
+
sourceAction(loaded.source, next),
|
|
781
|
+
...followup.actions,
|
|
782
|
+
];
|
|
783
|
+
if (work !== undefined)
|
|
784
|
+
actions.push({ action: "insert", value: work });
|
|
785
|
+
actions.push(deps.audit(command, ticket, actorId, now, "support.engineering.evidence_recorded"), receipt(command, hash, result(next)));
|
|
786
|
+
permission(access, name, actorId);
|
|
787
|
+
if (clearing)
|
|
788
|
+
permission(access, supportPermissions.engineeringSecurityReview, actorId);
|
|
789
|
+
// Hashing/planning can await after the trust response. Never commit a
|
|
790
|
+
// newly eligible proof whose original product/test attestation expired.
|
|
791
|
+
if (trusted &&
|
|
792
|
+
(timestamp(product.validUntil, "product.validUntil") <=
|
|
793
|
+
window.now() ||
|
|
794
|
+
(evidence.kind === "failed_test" &&
|
|
795
|
+
timestamp(evidence.test.validUntil, "test.validUntil") <=
|
|
796
|
+
window.now())))
|
|
797
|
+
throw new SupportDeskConflictError("Engineering evidence expired before commit");
|
|
798
|
+
if ((await records.transact(command.ticketId, actions)).committed) {
|
|
799
|
+
permission(access, name, actorId);
|
|
800
|
+
if (clearing)
|
|
801
|
+
permission(access, supportPermissions.engineeringSecurityReview, actorId);
|
|
802
|
+
return result(next);
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
const previous = await replayAuthorized(access, name, actorId, command, hash);
|
|
806
|
+
if (previous !== null)
|
|
807
|
+
return previous;
|
|
808
|
+
throw new SupportDeskConflictError();
|
|
809
|
+
},
|
|
810
|
+
invalidateEngineeringSource: (access, command) => invalidate(access, command, false),
|
|
811
|
+
withdrawEngineeringSource: (access, command) => invalidate(access, command, true),
|
|
812
|
+
async readEngineeringSource(access, ticketId) {
|
|
813
|
+
const actorId = access.principalId;
|
|
814
|
+
permission(access, supportPermissions.engineeringRead, actorId);
|
|
815
|
+
text(ticketId, "ticketId");
|
|
816
|
+
for (let attempt = 0; attempt < deps.maxConflictAttempts; attempt += 1) {
|
|
817
|
+
const source = await readEngineeringSourceVersion(records, ticketId);
|
|
818
|
+
permission(access, supportPermissions.engineeringRead, actorId);
|
|
819
|
+
if (source === null)
|
|
820
|
+
throw new SupportDeskNotFoundError();
|
|
821
|
+
privateRead(access, source.value.security, actorId);
|
|
822
|
+
if (source.value.state === "restricted")
|
|
823
|
+
permission(access, supportPermissions.engineeringRestrictedRead, actorId);
|
|
824
|
+
if (source.value.state !== "withdrawn") {
|
|
825
|
+
const ticket = await records.get({
|
|
826
|
+
partition: ticketId,
|
|
827
|
+
id: "ticket",
|
|
828
|
+
});
|
|
829
|
+
permission(access, supportPermissions.engineeringRead, actorId);
|
|
830
|
+
if (ticket?.kind !== "ticket")
|
|
831
|
+
throw new SupportDeskNotFoundError();
|
|
832
|
+
}
|
|
833
|
+
const latest = await readEngineeringSourceVersion(records, ticketId);
|
|
834
|
+
permission(access, supportPermissions.engineeringRead, actorId);
|
|
835
|
+
if (latest?.version !== source.version)
|
|
836
|
+
continue;
|
|
837
|
+
privateRead(access, source.value.security, actorId);
|
|
838
|
+
if (source.value.state === "restricted")
|
|
839
|
+
permission(access, supportPermissions.engineeringRestrictedRead, actorId);
|
|
840
|
+
return snapshot(source.value);
|
|
841
|
+
}
|
|
842
|
+
throw new SupportDeskConflictError();
|
|
843
|
+
},
|
|
844
|
+
async readEngineeringWork(access, key) {
|
|
845
|
+
const actorId = access.principalId;
|
|
846
|
+
permission(access, supportPermissions.engineeringRead, actorId);
|
|
847
|
+
const input = snapshot(fields(key, "engineering work key", ["ticketId", "sourceGeneration"]));
|
|
848
|
+
text(input.ticketId, "ticketId");
|
|
849
|
+
integer(input.sourceGeneration, "sourceGeneration", 1);
|
|
850
|
+
for (let attempt = 0; attempt < deps.maxConflictAttempts; attempt += 1) {
|
|
851
|
+
const source = await readEngineeringSourceVersion(records, input.ticketId);
|
|
852
|
+
permission(access, supportPermissions.engineeringRead, actorId);
|
|
853
|
+
if (source === null)
|
|
854
|
+
throw new SupportDeskNotFoundError();
|
|
855
|
+
privateRead(access, source.value.security, actorId);
|
|
856
|
+
if (source.value.state === "restricted")
|
|
857
|
+
permission(access, supportPermissions.engineeringRestrictedRead, actorId);
|
|
858
|
+
const work = await records.getVersioned(engineeringWorkKey(input.ticketId, input.sourceGeneration));
|
|
859
|
+
permission(access, supportPermissions.engineeringRead, actorId);
|
|
860
|
+
if (work?.value.kind !== "engineering_work")
|
|
861
|
+
throw new SupportDeskNotFoundError();
|
|
862
|
+
privateRead(access, source.value.security, actorId);
|
|
863
|
+
privateRead(access, work.value.security, actorId);
|
|
864
|
+
if (source.value.state === "restricted" ||
|
|
865
|
+
work.value.status === "restricted" ||
|
|
866
|
+
(work.value.candidate !== undefined &&
|
|
867
|
+
work.value.candidate.security !== "ordinary"))
|
|
868
|
+
permission(access, supportPermissions.engineeringRestrictedRead, actorId);
|
|
869
|
+
if (source.value.state !== "withdrawn") {
|
|
870
|
+
const ticket = await records.get({
|
|
871
|
+
partition: input.ticketId,
|
|
872
|
+
id: "ticket",
|
|
873
|
+
});
|
|
874
|
+
permission(access, supportPermissions.engineeringRead, actorId);
|
|
875
|
+
if (ticket?.kind !== "ticket")
|
|
876
|
+
throw new SupportDeskNotFoundError();
|
|
877
|
+
}
|
|
878
|
+
const latest = await readEngineeringSourceVersion(records, input.ticketId);
|
|
879
|
+
permission(access, supportPermissions.engineeringRead, actorId);
|
|
880
|
+
const latestWork = await records.getVersioned(engineeringWorkKey(input.ticketId, input.sourceGeneration));
|
|
881
|
+
permission(access, supportPermissions.engineeringRead, actorId);
|
|
882
|
+
if (latest?.version !== source.version ||
|
|
883
|
+
latestWork?.version !== work.version)
|
|
884
|
+
continue;
|
|
885
|
+
privateRead(access, source.value.security, actorId);
|
|
886
|
+
privateRead(access, work.value.security, actorId);
|
|
887
|
+
if (source.value.state === "restricted" ||
|
|
888
|
+
work.value.status === "restricted" ||
|
|
889
|
+
(work.value.candidate !== undefined &&
|
|
890
|
+
work.value.candidate.security !== "ordinary"))
|
|
891
|
+
permission(access, supportPermissions.engineeringRestrictedRead, actorId);
|
|
892
|
+
return snapshot(work.value);
|
|
893
|
+
}
|
|
894
|
+
throw new SupportDeskConflictError();
|
|
895
|
+
},
|
|
896
|
+
};
|
|
897
|
+
return { application, load };
|
|
898
|
+
}
|
|
899
|
+
//# sourceMappingURL=engineering-evidence.js.map
|