@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.
@@ -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({
@@ -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({
@@ -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({
@@ -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(event: Parameters<ReferenceCurationHandler>[0]) {
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
  }