@anthusai/papyrus 1.4.0-next.6 → 1.4.0-next.7
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/amplify/data/resource.js +57 -0
- package/amplify/data/resource.ts +62 -0
- package/amplify/data/schema.js +57 -0
- package/amplify/data/schema.ts +62 -0
- package/amplify/functions/category-action/handler.ts +171 -2
- package/amplify/functions/shared/curation-delegation.ts +67 -0
- package/amplify/functions/shared/reference-curation-metadata.ts +5 -0
- package/components/administration-delegations-panel.tsx +204 -0
- package/components/board-card-excerpts.tsx +43 -27
- package/components/curation-delegation-client.ts +81 -0
- package/components/news-desk-taxonomy-client.ts +7 -0
- package/components/newsroom-organizer.css +22 -0
- package/components/newsroom-references-view.tsx +56 -21
- package/components/reference-cyclotron-status.tsx +5 -5
- package/components/reference-cyclotron-suggestion.tsx +143 -0
- package/components/reference-live-agreement.tsx +62 -0
- package/components/topic-steering-workspace.tsx +49 -3
- package/lib/curation-delegation-helpers.ts +59 -0
- package/lib/cyclotron-suggestions.ts +263 -0
- package/lib/local-preview-seed.ts +119 -1
- package/lib/relevance-decisions.ts +29 -0
- package/package.json +7 -1
- package/routes.manifest.json +1 -1
package/amplify/data/resource.js
CHANGED
|
@@ -211,6 +211,15 @@ const schema = a.schema({
|
|
|
211
211
|
.returns(a.ref("SteeringActionResult"))
|
|
212
212
|
.authorization((allow) => [allow.groups(categoryWriteGroups)])
|
|
213
213
|
.handler(a.handler.function(categoryAction)),
|
|
214
|
+
CurationDelegationResult: a.customType({
|
|
215
|
+
ok: a.boolean().required(),
|
|
216
|
+
delegationId: a.id().required(),
|
|
217
|
+
token: a.string(),
|
|
218
|
+
agentLabel: a.string().required(),
|
|
219
|
+
grantorLabel: a.string(),
|
|
220
|
+
expiresAt: a.datetime().required(),
|
|
221
|
+
revokedAt: a.datetime(),
|
|
222
|
+
}),
|
|
214
223
|
ReferenceCurationActionResult: a.customType({
|
|
215
224
|
ok: a.boolean().required(),
|
|
216
225
|
action: a.string().required(),
|
|
@@ -295,6 +304,54 @@ const schema = a.schema({
|
|
|
295
304
|
allow.groups(categoryWriteGroups),
|
|
296
305
|
])
|
|
297
306
|
.handler(a.handler.function(categoryAction)),
|
|
307
|
+
createCurationDelegation: a
|
|
308
|
+
.mutation()
|
|
309
|
+
.arguments({
|
|
310
|
+
agentLabel: a.string().required(),
|
|
311
|
+
hours: a.integer(),
|
|
312
|
+
})
|
|
313
|
+
.returns(a.ref("CurationDelegationResult"))
|
|
314
|
+
.authorization((allow) => [
|
|
315
|
+
allow.groups(categoryWriteGroups),
|
|
316
|
+
])
|
|
317
|
+
.handler(a.handler.function(categoryAction)),
|
|
318
|
+
revokeCurationDelegation: a
|
|
319
|
+
.mutation()
|
|
320
|
+
.arguments({
|
|
321
|
+
delegationId: a.id().required(),
|
|
322
|
+
})
|
|
323
|
+
.returns(a.ref("CurationDelegationResult"))
|
|
324
|
+
.authorization((allow) => [
|
|
325
|
+
allow.groups(categoryWriteGroups),
|
|
326
|
+
])
|
|
327
|
+
.handler(a.handler.function(categoryAction)),
|
|
328
|
+
listCurationDelegations: a
|
|
329
|
+
.query()
|
|
330
|
+
.returns(a.ref("CurationDelegationResult").array())
|
|
331
|
+
.authorization((allow) => [
|
|
332
|
+
allow.groups(categoryWriteGroups),
|
|
333
|
+
])
|
|
334
|
+
.handler(a.handler.function(categoryAction)),
|
|
335
|
+
recordDelegatedCuration: a
|
|
336
|
+
.mutation()
|
|
337
|
+
.arguments({
|
|
338
|
+
referenceId: a.id().required(),
|
|
339
|
+
action: a.string().required(),
|
|
340
|
+
delegationId: a.id().required(),
|
|
341
|
+
delegationToken: a.string().required(),
|
|
342
|
+
agentLabel: a.string().required(),
|
|
343
|
+
onBehalfOf: a.string().required(),
|
|
344
|
+
instruction: a.string().required(),
|
|
345
|
+
note: a.string(),
|
|
346
|
+
reasonCode: a.string(),
|
|
347
|
+
decisionRelationId: a.id(),
|
|
348
|
+
shareable: a.boolean(),
|
|
349
|
+
})
|
|
350
|
+
.returns(a.ref("ReferenceCurationActionResult"))
|
|
351
|
+
.authorization((allow) => [
|
|
352
|
+
allow.authenticated("identityPool"),
|
|
353
|
+
])
|
|
354
|
+
.handler(a.handler.function(categoryAction)),
|
|
298
355
|
setReferenceQualityRating: a
|
|
299
356
|
.mutation()
|
|
300
357
|
.arguments({
|
package/amplify/data/resource.ts
CHANGED
|
@@ -228,6 +228,16 @@ const schema = a.schema({
|
|
|
228
228
|
.authorization((allow) => [allow.groups(categoryWriteGroups)])
|
|
229
229
|
.handler(a.handler.function(categoryAction)),
|
|
230
230
|
|
|
231
|
+
CurationDelegationResult: a.customType({
|
|
232
|
+
ok: a.boolean().required(),
|
|
233
|
+
delegationId: a.id().required(),
|
|
234
|
+
token: a.string(),
|
|
235
|
+
agentLabel: a.string().required(),
|
|
236
|
+
grantorLabel: a.string(),
|
|
237
|
+
expiresAt: a.datetime().required(),
|
|
238
|
+
revokedAt: a.datetime(),
|
|
239
|
+
}),
|
|
240
|
+
|
|
231
241
|
ReferenceCurationActionResult: a.customType({
|
|
232
242
|
ok: a.boolean().required(),
|
|
233
243
|
action: a.string().required(),
|
|
@@ -321,6 +331,58 @@ const schema = a.schema({
|
|
|
321
331
|
])
|
|
322
332
|
.handler(a.handler.function(categoryAction)),
|
|
323
333
|
|
|
334
|
+
createCurationDelegation: a
|
|
335
|
+
.mutation()
|
|
336
|
+
.arguments({
|
|
337
|
+
agentLabel: a.string().required(),
|
|
338
|
+
hours: a.integer(),
|
|
339
|
+
})
|
|
340
|
+
.returns(a.ref("CurationDelegationResult"))
|
|
341
|
+
.authorization((allow) => [
|
|
342
|
+
allow.groups(categoryWriteGroups),
|
|
343
|
+
])
|
|
344
|
+
.handler(a.handler.function(categoryAction)),
|
|
345
|
+
|
|
346
|
+
revokeCurationDelegation: a
|
|
347
|
+
.mutation()
|
|
348
|
+
.arguments({
|
|
349
|
+
delegationId: a.id().required(),
|
|
350
|
+
})
|
|
351
|
+
.returns(a.ref("CurationDelegationResult"))
|
|
352
|
+
.authorization((allow) => [
|
|
353
|
+
allow.groups(categoryWriteGroups),
|
|
354
|
+
])
|
|
355
|
+
.handler(a.handler.function(categoryAction)),
|
|
356
|
+
|
|
357
|
+
listCurationDelegations: a
|
|
358
|
+
.query()
|
|
359
|
+
.returns(a.ref("CurationDelegationResult").array())
|
|
360
|
+
.authorization((allow) => [
|
|
361
|
+
allow.groups(categoryWriteGroups),
|
|
362
|
+
])
|
|
363
|
+
.handler(a.handler.function(categoryAction)),
|
|
364
|
+
|
|
365
|
+
recordDelegatedCuration: a
|
|
366
|
+
.mutation()
|
|
367
|
+
.arguments({
|
|
368
|
+
referenceId: a.id().required(),
|
|
369
|
+
action: a.string().required(),
|
|
370
|
+
delegationId: a.id().required(),
|
|
371
|
+
delegationToken: a.string().required(),
|
|
372
|
+
agentLabel: a.string().required(),
|
|
373
|
+
onBehalfOf: a.string().required(),
|
|
374
|
+
instruction: a.string().required(),
|
|
375
|
+
note: a.string(),
|
|
376
|
+
reasonCode: a.string(),
|
|
377
|
+
decisionRelationId: a.id(),
|
|
378
|
+
shareable: a.boolean(),
|
|
379
|
+
})
|
|
380
|
+
.returns(a.ref("ReferenceCurationActionResult"))
|
|
381
|
+
.authorization((allow) => [
|
|
382
|
+
allow.authenticated("identityPool"),
|
|
383
|
+
])
|
|
384
|
+
.handler(a.handler.function(categoryAction)),
|
|
385
|
+
|
|
324
386
|
setReferenceQualityRating: a
|
|
325
387
|
.mutation()
|
|
326
388
|
.arguments({
|
package/amplify/data/schema.js
CHANGED
|
@@ -203,6 +203,15 @@ const baseDefinitions = {
|
|
|
203
203
|
.returns(a.ref("SteeringActionResult"))
|
|
204
204
|
.authorization((allow) => [allow.groups(categoryWriteGroups)])
|
|
205
205
|
.handler(a.handler.function(categoryAction)),
|
|
206
|
+
CurationDelegationResult: a.customType({
|
|
207
|
+
ok: a.boolean().required(),
|
|
208
|
+
delegationId: a.id().required(),
|
|
209
|
+
token: a.string(),
|
|
210
|
+
agentLabel: a.string().required(),
|
|
211
|
+
grantorLabel: a.string(),
|
|
212
|
+
expiresAt: a.datetime().required(),
|
|
213
|
+
revokedAt: a.datetime(),
|
|
214
|
+
}),
|
|
206
215
|
ReferenceCurationActionResult: a.customType({
|
|
207
216
|
ok: a.boolean().required(),
|
|
208
217
|
action: a.string().required(),
|
|
@@ -253,6 +262,54 @@ const baseDefinitions = {
|
|
|
253
262
|
allow.groups(categoryWriteGroups),
|
|
254
263
|
])
|
|
255
264
|
.handler(a.handler.function(categoryAction)),
|
|
265
|
+
createCurationDelegation: a
|
|
266
|
+
.mutation()
|
|
267
|
+
.arguments({
|
|
268
|
+
agentLabel: a.string().required(),
|
|
269
|
+
hours: a.integer(),
|
|
270
|
+
})
|
|
271
|
+
.returns(a.ref("CurationDelegationResult"))
|
|
272
|
+
.authorization((allow) => [
|
|
273
|
+
allow.groups(categoryWriteGroups),
|
|
274
|
+
])
|
|
275
|
+
.handler(a.handler.function(categoryAction)),
|
|
276
|
+
revokeCurationDelegation: a
|
|
277
|
+
.mutation()
|
|
278
|
+
.arguments({
|
|
279
|
+
delegationId: a.id().required(),
|
|
280
|
+
})
|
|
281
|
+
.returns(a.ref("CurationDelegationResult"))
|
|
282
|
+
.authorization((allow) => [
|
|
283
|
+
allow.groups(categoryWriteGroups),
|
|
284
|
+
])
|
|
285
|
+
.handler(a.handler.function(categoryAction)),
|
|
286
|
+
listCurationDelegations: a
|
|
287
|
+
.query()
|
|
288
|
+
.returns(a.ref("CurationDelegationResult").array())
|
|
289
|
+
.authorization((allow) => [
|
|
290
|
+
allow.groups(categoryWriteGroups),
|
|
291
|
+
])
|
|
292
|
+
.handler(a.handler.function(categoryAction)),
|
|
293
|
+
recordDelegatedCuration: a
|
|
294
|
+
.mutation()
|
|
295
|
+
.arguments({
|
|
296
|
+
referenceId: a.id().required(),
|
|
297
|
+
action: a.string().required(),
|
|
298
|
+
delegationId: a.id().required(),
|
|
299
|
+
delegationToken: a.string().required(),
|
|
300
|
+
agentLabel: a.string().required(),
|
|
301
|
+
onBehalfOf: a.string().required(),
|
|
302
|
+
instruction: a.string().required(),
|
|
303
|
+
note: a.string(),
|
|
304
|
+
reasonCode: a.string(),
|
|
305
|
+
decisionRelationId: a.id(),
|
|
306
|
+
shareable: a.boolean(),
|
|
307
|
+
})
|
|
308
|
+
.returns(a.ref("ReferenceCurationActionResult"))
|
|
309
|
+
.authorization((allow) => [
|
|
310
|
+
allow.authenticated("identityPool"),
|
|
311
|
+
])
|
|
312
|
+
.handler(a.handler.function(categoryAction)),
|
|
256
313
|
setReferenceQualityRating: a
|
|
257
314
|
.mutation()
|
|
258
315
|
.arguments({
|
package/amplify/data/schema.ts
CHANGED
|
@@ -222,6 +222,16 @@ const baseDefinitions = {
|
|
|
222
222
|
.authorization((allow) => [allow.groups(categoryWriteGroups)])
|
|
223
223
|
.handler(a.handler.function(categoryAction)),
|
|
224
224
|
|
|
225
|
+
CurationDelegationResult: a.customType({
|
|
226
|
+
ok: a.boolean().required(),
|
|
227
|
+
delegationId: a.id().required(),
|
|
228
|
+
token: a.string(),
|
|
229
|
+
agentLabel: a.string().required(),
|
|
230
|
+
grantorLabel: a.string(),
|
|
231
|
+
expiresAt: a.datetime().required(),
|
|
232
|
+
revokedAt: a.datetime(),
|
|
233
|
+
}),
|
|
234
|
+
|
|
225
235
|
ReferenceCurationActionResult: a.customType({
|
|
226
236
|
ok: a.boolean().required(),
|
|
227
237
|
action: a.string().required(),
|
|
@@ -277,6 +287,58 @@ const baseDefinitions = {
|
|
|
277
287
|
])
|
|
278
288
|
.handler(a.handler.function(categoryAction)),
|
|
279
289
|
|
|
290
|
+
createCurationDelegation: a
|
|
291
|
+
.mutation()
|
|
292
|
+
.arguments({
|
|
293
|
+
agentLabel: a.string().required(),
|
|
294
|
+
hours: a.integer(),
|
|
295
|
+
})
|
|
296
|
+
.returns(a.ref("CurationDelegationResult"))
|
|
297
|
+
.authorization((allow) => [
|
|
298
|
+
allow.groups(categoryWriteGroups),
|
|
299
|
+
])
|
|
300
|
+
.handler(a.handler.function(categoryAction)),
|
|
301
|
+
|
|
302
|
+
revokeCurationDelegation: a
|
|
303
|
+
.mutation()
|
|
304
|
+
.arguments({
|
|
305
|
+
delegationId: a.id().required(),
|
|
306
|
+
})
|
|
307
|
+
.returns(a.ref("CurationDelegationResult"))
|
|
308
|
+
.authorization((allow) => [
|
|
309
|
+
allow.groups(categoryWriteGroups),
|
|
310
|
+
])
|
|
311
|
+
.handler(a.handler.function(categoryAction)),
|
|
312
|
+
|
|
313
|
+
listCurationDelegations: a
|
|
314
|
+
.query()
|
|
315
|
+
.returns(a.ref("CurationDelegationResult").array())
|
|
316
|
+
.authorization((allow) => [
|
|
317
|
+
allow.groups(categoryWriteGroups),
|
|
318
|
+
])
|
|
319
|
+
.handler(a.handler.function(categoryAction)),
|
|
320
|
+
|
|
321
|
+
recordDelegatedCuration: a
|
|
322
|
+
.mutation()
|
|
323
|
+
.arguments({
|
|
324
|
+
referenceId: a.id().required(),
|
|
325
|
+
action: a.string().required(),
|
|
326
|
+
delegationId: a.id().required(),
|
|
327
|
+
delegationToken: a.string().required(),
|
|
328
|
+
agentLabel: a.string().required(),
|
|
329
|
+
onBehalfOf: a.string().required(),
|
|
330
|
+
instruction: a.string().required(),
|
|
331
|
+
note: a.string(),
|
|
332
|
+
reasonCode: a.string(),
|
|
333
|
+
decisionRelationId: a.id(),
|
|
334
|
+
shareable: a.boolean(),
|
|
335
|
+
})
|
|
336
|
+
.returns(a.ref("ReferenceCurationActionResult"))
|
|
337
|
+
.authorization((allow) => [
|
|
338
|
+
allow.authenticated("identityPool"),
|
|
339
|
+
])
|
|
340
|
+
.handler(a.handler.function(categoryAction)),
|
|
341
|
+
|
|
280
342
|
setReferenceQualityRating: a
|
|
281
343
|
.mutation()
|
|
282
344
|
.arguments({
|
|
@@ -5,9 +5,22 @@ import { generateClient } from "aws-amplify/data";
|
|
|
5
5
|
import type { Schema } from "../../data/resource";
|
|
6
6
|
import { putJsonModelPayload, putTextModelPayload, readJsonModelPayload } from "../shared/model-payloads";
|
|
7
7
|
import { curationActorKind, referenceCurationMetadata } from "../shared/reference-curation-metadata";
|
|
8
|
+
import {
|
|
9
|
+
CURATION_DELEGATION_OWNER_TYPE,
|
|
10
|
+
CURATION_DELEGATION_PAYLOAD_KIND,
|
|
11
|
+
delegationExpiry,
|
|
12
|
+
delegationRefusalMessage,
|
|
13
|
+
delegationVerdict,
|
|
14
|
+
newDelegationToken,
|
|
15
|
+
type CurationDelegation,
|
|
16
|
+
} from "../shared/curation-delegation";
|
|
8
17
|
|
|
9
18
|
type ReviewHandler = Schema["reviewSteeringProposal"]["functionHandler"];
|
|
10
19
|
type ReferenceCurationHandler = Schema["reviewReferenceCuration"]["functionHandler"];
|
|
20
|
+
type CreateCurationDelegationHandler = Schema["createCurationDelegation"]["functionHandler"];
|
|
21
|
+
type RevokeCurationDelegationHandler = Schema["revokeCurationDelegation"]["functionHandler"];
|
|
22
|
+
type ListCurationDelegationsHandler = Schema["listCurationDelegations"]["functionHandler"];
|
|
23
|
+
type RecordDelegatedCurationHandler = Schema["recordDelegatedCuration"]["functionHandler"];
|
|
11
24
|
type ReferenceQualityHandler = Schema["setReferenceQualityRating"]["functionHandler"];
|
|
12
25
|
type ReferenceNoteHandler = Schema["createReferenceNote"]["functionHandler"];
|
|
13
26
|
type MoveReferenceCorpusHandler = Schema["moveReferenceCorpus"]["functionHandler"];
|
|
@@ -23,6 +36,10 @@ type ReviewReferenceTopicLabelHandler = Schema["reviewReferenceTopicLabel"]["fun
|
|
|
23
36
|
type CategoryActionEvent =
|
|
24
37
|
| Parameters<ReviewHandler>[0]
|
|
25
38
|
| Parameters<ReferenceCurationHandler>[0]
|
|
39
|
+
| Parameters<CreateCurationDelegationHandler>[0]
|
|
40
|
+
| Parameters<RevokeCurationDelegationHandler>[0]
|
|
41
|
+
| Parameters<ListCurationDelegationsHandler>[0]
|
|
42
|
+
| Parameters<RecordDelegatedCurationHandler>[0]
|
|
26
43
|
| Parameters<ReferenceQualityHandler>[0]
|
|
27
44
|
| Parameters<ReferenceNoteHandler>[0]
|
|
28
45
|
| Parameters<MoveReferenceCorpusHandler>[0]
|
|
@@ -88,6 +105,10 @@ export const handler = async (event: CategoryActionEvent) => {
|
|
|
88
105
|
}
|
|
89
106
|
if (fieldName === "reviewSteeringProposal") return reviewSteeringProposal(event as Parameters<ReviewHandler>[0]);
|
|
90
107
|
if (fieldName === "reviewReferenceCuration") return reviewReferenceCuration(event as Parameters<ReferenceCurationHandler>[0]);
|
|
108
|
+
if (fieldName === "createCurationDelegation") return createCurationDelegation(event as Parameters<CreateCurationDelegationHandler>[0]);
|
|
109
|
+
if (fieldName === "revokeCurationDelegation") return revokeCurationDelegation(event as Parameters<RevokeCurationDelegationHandler>[0]);
|
|
110
|
+
if (fieldName === "listCurationDelegations") return listCurationDelegations(event as Parameters<ListCurationDelegationsHandler>[0]);
|
|
111
|
+
if (fieldName === "recordDelegatedCuration") return recordDelegatedCuration(event as Parameters<RecordDelegatedCurationHandler>[0]);
|
|
91
112
|
if (fieldName === "setReferenceQualityRating") return setReferenceQualityRating(event as Parameters<ReferenceQualityHandler>[0]);
|
|
92
113
|
if (fieldName === "createReferenceNote") return createReferenceNote(event as Parameters<ReferenceNoteHandler>[0]);
|
|
93
114
|
if (fieldName === "moveReferenceCorpus") return moveReferenceCorpus(event as Parameters<MoveReferenceCorpusHandler>[0]);
|
|
@@ -162,7 +183,10 @@ async function reviewSteeringProposal(event: Parameters<ReviewHandler>[0]) {
|
|
|
162
183
|
};
|
|
163
184
|
}
|
|
164
185
|
|
|
165
|
-
async function reviewReferenceCuration(
|
|
186
|
+
async function reviewReferenceCuration(
|
|
187
|
+
event: Parameters<ReferenceCurationHandler>[0],
|
|
188
|
+
delegation: { delegationId: string; relayedBy: string; instruction: string } | null = null,
|
|
189
|
+
) {
|
|
166
190
|
const client = await getDataClient();
|
|
167
191
|
const referenceId = normalizeRequiredString(event.arguments.referenceId, "referenceId");
|
|
168
192
|
const action = normalizeReferenceCurationAction(event.arguments.action);
|
|
@@ -234,7 +258,8 @@ async function reviewReferenceCuration(event: Parameters<ReferenceCurationHandle
|
|
|
234
258
|
referenceLineageId,
|
|
235
259
|
decisionRelationId: normalizeOptionalString(event.arguments.decisionRelationId),
|
|
236
260
|
shareable: event.arguments.shareable ?? null,
|
|
237
|
-
actorKind: curationActorKind(event.identity),
|
|
261
|
+
actorKind: delegation ? "human" : curationActorKind(event.identity),
|
|
262
|
+
delegation,
|
|
238
263
|
}),
|
|
239
264
|
{ filename: "metadata.json", now },
|
|
240
265
|
);
|
|
@@ -301,6 +326,150 @@ async function reviewReferenceCuration(event: Parameters<ReferenceCurationHandle
|
|
|
301
326
|
};
|
|
302
327
|
}
|
|
303
328
|
|
|
329
|
+
function requireSignedInPerson(event: { identity?: unknown }): { sub: string; label: string; isAdmin: boolean } {
|
|
330
|
+
if (curationActorKind(event.identity) !== "human") {
|
|
331
|
+
throw new Error("Only a person signed in to the Newsroom can manage curation delegations.");
|
|
332
|
+
}
|
|
333
|
+
const identity = event.identity as { sub?: unknown; claims?: Record<string, unknown> };
|
|
334
|
+
const claims = identity.claims ?? {};
|
|
335
|
+
const groups = Array.isArray(claims["cognito:groups"]) ? (claims["cognito:groups"] as unknown[]) : [];
|
|
336
|
+
return {
|
|
337
|
+
sub: normalizeRequiredString(claims.sub ?? identity.sub, "identity.sub"),
|
|
338
|
+
label: normalizeOptionalString(claims.email) ?? normalizeOptionalString(claims.name) ?? "Newsroom editor",
|
|
339
|
+
isAdmin: groups.includes("admin"),
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
async function readCurationDelegation(client: DataClient, delegationId: string): Promise<CurationDelegation | null> {
|
|
344
|
+
const stored = await readJsonModelPayload(client as any, "knowledgeRawPayload", delegationId, "raw_payload", CURATION_DELEGATION_PAYLOAD_KIND);
|
|
345
|
+
return stored ? (stored as unknown as CurationDelegation) : null;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
async function writeCurationDelegation(client: DataClient, delegation: CurationDelegation, now: string, created: boolean) {
|
|
349
|
+
const input = {
|
|
350
|
+
id: delegation.delegationId,
|
|
351
|
+
ownerType: CURATION_DELEGATION_OWNER_TYPE,
|
|
352
|
+
ownerId: delegation.grantorSub,
|
|
353
|
+
payloadKind: CURATION_DELEGATION_PAYLOAD_KIND,
|
|
354
|
+
createdAt: delegation.createdAt,
|
|
355
|
+
updatedAt: now,
|
|
356
|
+
};
|
|
357
|
+
if (created) {
|
|
358
|
+
await requireDataResult(client.models.KnowledgeRawPayload.create(input), "create curation delegation");
|
|
359
|
+
} else {
|
|
360
|
+
await requireDataResult(client.models.KnowledgeRawPayload.update(input), "update curation delegation");
|
|
361
|
+
}
|
|
362
|
+
await putJsonModelPayload(
|
|
363
|
+
client as any,
|
|
364
|
+
{ ownerKind: "knowledgeRawPayload", ownerId: delegation.delegationId },
|
|
365
|
+
"raw_payload",
|
|
366
|
+
CURATION_DELEGATION_PAYLOAD_KIND,
|
|
367
|
+
delegation as unknown as Record<string, unknown>,
|
|
368
|
+
{ filename: "curation-delegation.json", now },
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function delegationResult(delegation: CurationDelegation, token: string | null) {
|
|
373
|
+
return {
|
|
374
|
+
ok: true,
|
|
375
|
+
delegationId: delegation.delegationId,
|
|
376
|
+
token,
|
|
377
|
+
agentLabel: delegation.agentLabel,
|
|
378
|
+
grantorLabel: delegation.grantorLabel,
|
|
379
|
+
expiresAt: delegation.expiresAt,
|
|
380
|
+
revokedAt: delegation.revokedAt,
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
async function createCurationDelegation(event: Parameters<CreateCurationDelegationHandler>[0]) {
|
|
385
|
+
const person = requireSignedInPerson(event);
|
|
386
|
+
const client = await getDataClient();
|
|
387
|
+
const now = new Date().toISOString();
|
|
388
|
+
const delegationId = `curation-delegation-${randomUUID()}`;
|
|
389
|
+
const { token, tokenHash } = newDelegationToken(delegationId);
|
|
390
|
+
const delegation: CurationDelegation = {
|
|
391
|
+
delegationId,
|
|
392
|
+
tokenHash,
|
|
393
|
+
grantorSub: person.sub,
|
|
394
|
+
grantorLabel: person.label,
|
|
395
|
+
agentLabel: normalizeRequiredString(event.arguments.agentLabel, "agentLabel"),
|
|
396
|
+
createdAt: now,
|
|
397
|
+
expiresAt: delegationExpiry(now, event.arguments.hours),
|
|
398
|
+
revokedAt: null,
|
|
399
|
+
};
|
|
400
|
+
await writeCurationDelegation(client, delegation, now, true);
|
|
401
|
+
return delegationResult(delegation, token);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
async function revokeCurationDelegation(event: Parameters<RevokeCurationDelegationHandler>[0]) {
|
|
405
|
+
const person = requireSignedInPerson(event);
|
|
406
|
+
const client = await getDataClient();
|
|
407
|
+
const delegation = await readCurationDelegation(client, normalizeRequiredString(event.arguments.delegationId, "delegationId"));
|
|
408
|
+
if (!delegation) throw new Error("That delegation was not found.");
|
|
409
|
+
if (delegation.grantorSub !== person.sub && !person.isAdmin) throw new Error("Only the person who granted a delegation can revoke it.");
|
|
410
|
+
const now = new Date().toISOString();
|
|
411
|
+
const revoked = { ...delegation, revokedAt: delegation.revokedAt ?? now };
|
|
412
|
+
await writeCurationDelegation(client, revoked, now, false);
|
|
413
|
+
return delegationResult(revoked, null);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
async function listCurationDelegations(event: Parameters<ListCurationDelegationsHandler>[0]) {
|
|
417
|
+
const person = requireSignedInPerson(event);
|
|
418
|
+
const client = await getDataClient();
|
|
419
|
+
const delegations: CurationDelegation[] = [];
|
|
420
|
+
let nextToken: string | null | undefined;
|
|
421
|
+
do {
|
|
422
|
+
const page: any = await (client.models.KnowledgeRawPayload as any).listKnowledgeRawPayloadsByOwnerAndKind(
|
|
423
|
+
{ ownerId: person.sub, payloadKind: { eq: CURATION_DELEGATION_PAYLOAD_KIND } },
|
|
424
|
+
{ nextToken },
|
|
425
|
+
);
|
|
426
|
+
assertNoDataErrors(page.errors, "list curation delegations");
|
|
427
|
+
for (const payload of page.data ?? []) {
|
|
428
|
+
const delegation = await readCurationDelegation(client, payload.id);
|
|
429
|
+
if (delegation) delegations.push(delegation);
|
|
430
|
+
}
|
|
431
|
+
nextToken = page.nextToken;
|
|
432
|
+
} while (nextToken);
|
|
433
|
+
return delegations
|
|
434
|
+
.sort((first, second) => second.createdAt.localeCompare(first.createdAt))
|
|
435
|
+
.map((delegation) => delegationResult(delegation, null));
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
async function recordDelegatedCuration(event: Parameters<RecordDelegatedCurationHandler>[0]) {
|
|
439
|
+
const client = await getDataClient();
|
|
440
|
+
const now = new Date().toISOString();
|
|
441
|
+
const delegationId = normalizeRequiredString(event.arguments.delegationId, "delegationId");
|
|
442
|
+
const agentLabel = normalizeRequiredString(event.arguments.agentLabel, "agentLabel");
|
|
443
|
+
const delegation = await readCurationDelegation(client, delegationId);
|
|
444
|
+
const verdict = delegationVerdict(delegation, {
|
|
445
|
+
token: normalizeRequiredString(event.arguments.delegationToken, "delegationToken"),
|
|
446
|
+
agentLabel,
|
|
447
|
+
now,
|
|
448
|
+
});
|
|
449
|
+
if (!verdict.ok || !delegation) {
|
|
450
|
+
throw new Error(delegationRefusalMessage(verdict.ok ? "unknown-token" : verdict.reason));
|
|
451
|
+
}
|
|
452
|
+
const instruction = normalizeRequiredString(event.arguments.instruction, "instruction").slice(0, 2000);
|
|
453
|
+
const namedPerson = normalizeRequiredString(event.arguments.onBehalfOf, "onBehalfOf");
|
|
454
|
+
if (namedPerson.toLowerCase() !== delegation.grantorLabel.toLowerCase()) {
|
|
455
|
+
throw new Error(`That delegation was granted by ${delegation.grantorLabel}, not ${namedPerson}.`);
|
|
456
|
+
}
|
|
457
|
+
const onBehalfOfPerson = {
|
|
458
|
+
...event,
|
|
459
|
+
arguments: {
|
|
460
|
+
referenceId: event.arguments.referenceId,
|
|
461
|
+
action: event.arguments.action,
|
|
462
|
+
note: event.arguments.note,
|
|
463
|
+
reasonCode: event.arguments.reasonCode,
|
|
464
|
+
decisionRelationId: event.arguments.decisionRelationId,
|
|
465
|
+
shareable: event.arguments.shareable,
|
|
466
|
+
actorSub: delegation.grantorSub,
|
|
467
|
+
actorLabel: delegation.grantorLabel,
|
|
468
|
+
},
|
|
469
|
+
} as unknown as Parameters<ReferenceCurationHandler>[0];
|
|
470
|
+
return reviewReferenceCuration(onBehalfOfPerson, { delegationId, relayedBy: agentLabel, instruction });
|
|
471
|
+
}
|
|
472
|
+
|
|
304
473
|
async function setReferenceQualityRating(event: Parameters<ReferenceQualityHandler>[0]) {
|
|
305
474
|
const client = await getDataClient();
|
|
306
475
|
const referenceId = normalizeRequiredString(event.arguments.referenceId, "referenceId");
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { createHash, randomBytes } from "node:crypto"
|
|
2
|
+
|
|
3
|
+
export const CURATION_DELEGATION_PAYLOAD_KIND = "curation-delegation"
|
|
4
|
+
export const CURATION_DELEGATION_OWNER_TYPE = "curationDelegation"
|
|
5
|
+
export const DEFAULT_DELEGATION_HOURS = 8
|
|
6
|
+
export const MAXIMUM_DELEGATION_HOURS = 72
|
|
7
|
+
|
|
8
|
+
export type CurationDelegation = {
|
|
9
|
+
delegationId: string
|
|
10
|
+
tokenHash: string
|
|
11
|
+
grantorSub: string
|
|
12
|
+
grantorLabel: string
|
|
13
|
+
agentLabel: string
|
|
14
|
+
createdAt: string
|
|
15
|
+
expiresAt: string
|
|
16
|
+
revokedAt: string | null
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type DelegationVerdict =
|
|
20
|
+
| { ok: true }
|
|
21
|
+
| { ok: false; reason: "unknown-token" | "wrong-agent" | "revoked" | "expired" }
|
|
22
|
+
|
|
23
|
+
export function hashDelegationToken(token: string): string {
|
|
24
|
+
return createHash("sha256").update(token, "utf8").digest("hex")
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function newDelegationToken(delegationId: string): { token: string; tokenHash: string } {
|
|
28
|
+
const token = `${delegationId}~${randomBytes(24).toString("base64url")}`
|
|
29
|
+
return { token, tokenHash: hashDelegationToken(token) }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function delegationIdFromToken(token: string): string | null {
|
|
33
|
+
const separator = token.indexOf("~")
|
|
34
|
+
return separator > 0 ? token.slice(0, separator) : null
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function delegationExpiry(now: string, hours: number | null | undefined): string {
|
|
38
|
+
const requested = hours ?? DEFAULT_DELEGATION_HOURS
|
|
39
|
+
if (!Number.isFinite(requested) || requested <= 0 || requested > MAXIMUM_DELEGATION_HOURS) {
|
|
40
|
+
throw new Error(`A delegation lasts between 1 and ${MAXIMUM_DELEGATION_HOURS} hours.`)
|
|
41
|
+
}
|
|
42
|
+
return new Date(Date.parse(now) + requested * 3_600_000).toISOString()
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function delegationVerdict(
|
|
46
|
+
delegation: CurationDelegation | null,
|
|
47
|
+
presented: { token: string; agentLabel: string; now: string },
|
|
48
|
+
): DelegationVerdict {
|
|
49
|
+
if (!delegation || delegation.tokenHash !== hashDelegationToken(presented.token)) {
|
|
50
|
+
return { ok: false, reason: "unknown-token" }
|
|
51
|
+
}
|
|
52
|
+
if (delegation.agentLabel.trim().toLowerCase() !== presented.agentLabel.trim().toLowerCase()) {
|
|
53
|
+
return { ok: false, reason: "wrong-agent" }
|
|
54
|
+
}
|
|
55
|
+
if (delegation.revokedAt) return { ok: false, reason: "revoked" }
|
|
56
|
+
if (Date.parse(delegation.expiresAt) <= Date.parse(presented.now)) return { ok: false, reason: "expired" }
|
|
57
|
+
return { ok: true }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function delegationRefusalMessage(reason: Exclude<DelegationVerdict, { ok: true }>["reason"]): string {
|
|
61
|
+
return {
|
|
62
|
+
"unknown-token": "That delegation token is not recognized.",
|
|
63
|
+
"wrong-agent": "That delegation was granted to a different agent.",
|
|
64
|
+
"revoked": "That delegation was revoked.",
|
|
65
|
+
"expired": "That delegation has expired. Ask the person for a new one.",
|
|
66
|
+
}[reason]
|
|
67
|
+
}
|
|
@@ -13,6 +13,7 @@ export type ReferenceCurationMetadataInput = {
|
|
|
13
13
|
decisionRelationId?: string | null
|
|
14
14
|
shareable?: boolean | null
|
|
15
15
|
actorKind?: CurationActorKind | null
|
|
16
|
+
delegation?: { delegationId: string; relayedBy: string; instruction: string } | null
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
export type CurationActorKind = "human" | "service"
|
|
@@ -38,6 +39,10 @@ export function referenceCurationMetadata(input: ReferenceCurationMetadataInput)
|
|
|
38
39
|
referenceId: input.referenceId,
|
|
39
40
|
referenceLineageId: input.referenceLineageId,
|
|
40
41
|
...(input.actorKind ? {actorKind: input.actorKind} : {}),
|
|
42
|
+
...(input.delegation
|
|
43
|
+
? {attestation: "delegated", relayedBy: input.delegation.relayedBy, delegationId: input.delegation.delegationId,
|
|
44
|
+
instruction: input.delegation.instruction}
|
|
45
|
+
: {}),
|
|
41
46
|
...(decisionRelationId ? {decisionRelationId} : {}),
|
|
42
47
|
...(decisionRelationId ? {shareable: input.shareable === true} : {}),
|
|
43
48
|
}
|