@anthusai/papyrus 1.4.0-next.1 → 1.4.0-next.10
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 +134 -3
- package/amplify/data/resource.ts +145 -3
- package/amplify/data/schema.js +131 -0
- package/amplify/data/schema.ts +142 -0
- package/amplify/functions/category-action/handler.ts +272 -20
- package/amplify/functions/console-chat-responder/src/main.rs +1 -1
- package/amplify/functions/content-actions/handler.py +23 -0
- package/amplify/functions/content-actions/requirements.txt +1 -1
- package/amplify/functions/content-actions/resource.js +1 -1
- package/amplify/functions/content-actions/resource.ts +1 -1
- package/amplify/functions/renditions/requirements.txt +1 -1
- package/amplify/functions/shared/curation-delegation-flow.ts +147 -0
- package/amplify/functions/shared/curation-delegation.ts +115 -0
- package/amplify/functions/shared/python-bundle.js +3 -3
- package/amplify/functions/shared/python-bundle.ts +3 -3
- package/amplify/functions/shared/reference-curation-metadata.ts +51 -0
- package/amplify/functions/shared/slack-events.ts +2 -2
- package/amplify/publication/renditions-function.js +1 -1
- package/amplify/publication/renditions-function.ts +1 -1
- package/app/[year]/[month]/[day]/[articleSlug]/page.tsx +2 -1
- package/app/[year]/[month]/[day]/edition-route-page.tsx +2 -1
- package/app/[year]/[month]/[day]/page/[pageNumber]/page.tsx +2 -1
- package/app/[year]/[month]/[day]/page.tsx +3 -0
- package/app/[year]/[month]/[day]/section/[sectionKey]/page.tsx +2 -0
- package/app/api/archive/editions/route.ts +2 -0
- package/app/api/auth/prospectus/accounts/route.ts +9 -0
- package/app/api/local-preview/derive/route.ts +28 -0
- package/app/api/local-preview/media/route.ts +25 -0
- package/app/archive/page.tsx +3 -0
- package/app/globals.css +28 -28
- package/app/layout.tsx +8 -5
- package/app/newsroom/[section]/[...selection]/page.tsx +0 -1
- package/app/newsroom/[section]/page.tsx +3 -2
- package/app/newsroom/articles/page.tsx +8 -1
- package/app/newsroom/insights/[threadId]/page.tsx +5 -16
- package/app/newsroom/insights/page.tsx +4 -12
- package/app/newsroom/sections/[sectionId]/page.tsx +3 -0
- package/app/page.tsx +30 -1
- package/components/administration-delegations-panel.tsx +156 -0
- package/components/curation-delegation-client.ts +104 -0
- package/components/item-stream-view.tsx +63 -0
- package/components/markus-ir-preview.tsx +47 -2
- package/components/news-desk-page.tsx +0 -1
- package/components/news-desk-taxonomy-client.ts +82 -242
- package/components/newsroom-article-editor.tsx +138 -40
- package/components/newsroom-assignment-detail.tsx +79 -0
- package/components/newsroom-assignments-view.tsx +1 -1
- package/components/newsroom-board-view.tsx +128 -51
- package/components/newsroom-newsprint-board.css +5 -46
- package/components/newsroom-ops-overview.tsx +27 -5
- package/components/newsroom-organizer-shell.tsx +26 -8
- package/components/newsroom-organizer.css +267 -12
- package/components/newsroom-references-view.tsx +100 -23
- package/components/newsroom-topics-view.tsx +1 -1
- package/components/priming-proof-page.tsx +17 -8
- package/components/prospectus-account-chooser.tsx +131 -0
- 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/reference-section-suggestion.tsx +104 -0
- package/components/story-reporting-trail-client.ts +91 -0
- package/components/story-reporting-trail.tsx +115 -0
- package/components/topic-steering-workspace.tsx +245 -678
- package/components/ui/sheet.tsx +10 -4
- package/components/use-paged-content-item-versions.ts +151 -0
- package/components/wiki-concepts-view.tsx +153 -0
- package/components/wiki-index-client.ts +51 -0
- package/infra/amplify-app-shell.js +17 -3
- package/infra/amplify-app-shell.ts +18 -3
- package/infra/site-config.js +6 -1
- package/infra/site-config.ts +7 -1
- package/lib/approved-priming-proof-reports.ts +6 -3
- package/lib/approved-priming-proofs/papyrus.json +90 -0
- package/lib/blog-feature-solver.ts +1 -109
- package/lib/category-repository.ts +1 -1
- package/lib/content-actions-client.ts +3 -0
- package/lib/content-actions-demo.ts +55 -0
- package/lib/content-item-lifecycle.ts +34 -5
- package/lib/content-source-context.ts +9 -1
- package/lib/curation-delegation-helpers.ts +69 -0
- package/lib/cyclotron-suggestions.ts +263 -0
- package/lib/front-matter-fields.ts +42 -0
- package/lib/front-mosaic.ts +2 -1
- package/lib/graphql-content-repository.ts +42 -7
- package/lib/item-stream.ts +47 -0
- package/lib/layout-plan.ts +3 -2
- package/lib/layout-warnings.ts +8 -0
- package/lib/local-preview-section-fixtures.ts +72 -0
- package/lib/local-preview-seed.ts +196 -7
- package/lib/local-preview-tenant.ts +27 -0
- package/lib/newsroom-articles.ts +2 -0
- package/lib/newsroom-assignments.ts +14 -1
- package/lib/newsroom-board.ts +52 -7
- package/lib/newsroom-demo-dashboard.ts +1 -1
- package/lib/newsroom-forum-routes.ts +2 -43
- package/lib/newsroom-index-filters.ts +3 -40
- package/lib/newsroom-media-backend.ts +5 -0
- package/lib/newsroom-nav.ts +1 -3
- package/lib/newsroom-organizer-nav.ts +8 -14
- package/lib/newsroom-section-routing.ts +1 -1
- package/lib/newsroom-sections.ts +12 -0
- package/lib/papyrus-web-locations.ts +3 -24
- package/lib/priming-proof-report.ts +1 -0
- package/lib/prospectus-account-client.ts +90 -0
- package/lib/prospectus-sign-in-routes.ts +45 -0
- package/lib/prospectus-site-identity.ts +13 -1
- package/lib/prospectus-tenant.ts +22 -0
- package/lib/reader-static-params.ts +2 -1
- package/lib/relevance-decisions.ts +29 -0
- package/lib/reporting-story-budget.ts +2 -2
- package/lib/semantic-graph.ts +5 -5
- package/lib/site-brand.ts +20 -0
- package/lib/site-env.ts +5 -0
- package/lib/site-section-suggestions.ts +120 -0
- package/lib/site-stack.ts +1 -1
- package/lib/story-reporting-trail.ts +106 -0
- package/lib/tenant-keys.ts +32 -0
- package/lib/wiki.ts +56 -0
- package/package.json +34 -2
- package/renderers/pretext/rhythm-article-page.tsx +1 -18
- package/routes.manifest.json +34 -7
|
@@ -4,12 +4,39 @@ import { Amplify } from "aws-amplify";
|
|
|
4
4
|
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
|
-
import { referenceCurationMetadata } from "../shared/reference-curation-metadata";
|
|
7
|
+
import { curationActorKind, referenceCurationMetadata, validatedCurationSectionId } 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
|
+
PENDING_REQUEST_GRANTOR_SUB,
|
|
16
|
+
type CurationDelegation,
|
|
17
|
+
} from "../shared/curation-delegation";
|
|
18
|
+
import {
|
|
19
|
+
approveDelegation,
|
|
20
|
+
delegationResult as flowDelegationResult,
|
|
21
|
+
delegationStatus,
|
|
22
|
+
denyDelegation,
|
|
23
|
+
livePendingRequests,
|
|
24
|
+
requestDelegation,
|
|
25
|
+
type DelegationStore,
|
|
26
|
+
} from "../shared/curation-delegation-flow";
|
|
8
27
|
|
|
9
28
|
type ReviewHandler = Schema["reviewSteeringProposal"]["functionHandler"];
|
|
10
29
|
type ReferenceCurationHandler = Schema["reviewReferenceCuration"]["functionHandler"];
|
|
30
|
+
type CreateCurationDelegationHandler = Schema["createCurationDelegation"]["functionHandler"];
|
|
31
|
+
type RevokeCurationDelegationHandler = Schema["revokeCurationDelegation"]["functionHandler"];
|
|
32
|
+
type ListCurationDelegationsHandler = Schema["listCurationDelegations"]["functionHandler"];
|
|
33
|
+
type RequestCurationDelegationHandler = Schema["requestCurationDelegation"]["functionHandler"];
|
|
34
|
+
type ApproveCurationDelegationHandler = Schema["approveCurationDelegation"]["functionHandler"];
|
|
35
|
+
type DenyCurationDelegationHandler = Schema["denyCurationDelegation"]["functionHandler"];
|
|
36
|
+
type GetCurationDelegationStatusHandler = Schema["getCurationDelegationStatus"]["functionHandler"];
|
|
37
|
+
type RecordDelegatedCurationHandler = Schema["recordDelegatedCuration"]["functionHandler"];
|
|
11
38
|
type ReferenceQualityHandler = Schema["setReferenceQualityRating"]["functionHandler"];
|
|
12
|
-
type
|
|
39
|
+
type ReferenceNoteHandler = Schema["createReferenceNote"]["functionHandler"];
|
|
13
40
|
type MoveReferenceCorpusHandler = Schema["moveReferenceCorpus"]["functionHandler"];
|
|
14
41
|
type StartReferenceCurationHandler = Schema["startReferenceCuration"]["functionHandler"];
|
|
15
42
|
type GetReferenceCurationStatusHandler = Schema["getReferenceCurationStatus"]["functionHandler"];
|
|
@@ -23,8 +50,16 @@ type ReviewReferenceTopicLabelHandler = Schema["reviewReferenceTopicLabel"]["fun
|
|
|
23
50
|
type CategoryActionEvent =
|
|
24
51
|
| Parameters<ReviewHandler>[0]
|
|
25
52
|
| Parameters<ReferenceCurationHandler>[0]
|
|
53
|
+
| Parameters<CreateCurationDelegationHandler>[0]
|
|
54
|
+
| Parameters<RevokeCurationDelegationHandler>[0]
|
|
55
|
+
| Parameters<ListCurationDelegationsHandler>[0]
|
|
56
|
+
| Parameters<RequestCurationDelegationHandler>[0]
|
|
57
|
+
| Parameters<ApproveCurationDelegationHandler>[0]
|
|
58
|
+
| Parameters<DenyCurationDelegationHandler>[0]
|
|
59
|
+
| Parameters<GetCurationDelegationStatusHandler>[0]
|
|
60
|
+
| Parameters<RecordDelegatedCurationHandler>[0]
|
|
26
61
|
| Parameters<ReferenceQualityHandler>[0]
|
|
27
|
-
| Parameters<
|
|
62
|
+
| Parameters<ReferenceNoteHandler>[0]
|
|
28
63
|
| Parameters<MoveReferenceCorpusHandler>[0]
|
|
29
64
|
| Parameters<StartReferenceCurationHandler>[0]
|
|
30
65
|
| Parameters<GetReferenceCurationStatusHandler>[0]
|
|
@@ -88,8 +123,16 @@ export const handler = async (event: CategoryActionEvent) => {
|
|
|
88
123
|
}
|
|
89
124
|
if (fieldName === "reviewSteeringProposal") return reviewSteeringProposal(event as Parameters<ReviewHandler>[0]);
|
|
90
125
|
if (fieldName === "reviewReferenceCuration") return reviewReferenceCuration(event as Parameters<ReferenceCurationHandler>[0]);
|
|
126
|
+
if (fieldName === "createCurationDelegation") return createCurationDelegation(event as Parameters<CreateCurationDelegationHandler>[0]);
|
|
127
|
+
if (fieldName === "revokeCurationDelegation") return revokeCurationDelegation(event as Parameters<RevokeCurationDelegationHandler>[0]);
|
|
128
|
+
if (fieldName === "listCurationDelegations") return listCurationDelegations(event as Parameters<ListCurationDelegationsHandler>[0]);
|
|
129
|
+
if (fieldName === "requestCurationDelegation") return requestCurationDelegationAction(event as Parameters<RequestCurationDelegationHandler>[0]);
|
|
130
|
+
if (fieldName === "approveCurationDelegation") return approveCurationDelegationAction(event as Parameters<ApproveCurationDelegationHandler>[0]);
|
|
131
|
+
if (fieldName === "denyCurationDelegation") return denyCurationDelegationAction(event as Parameters<DenyCurationDelegationHandler>[0]);
|
|
132
|
+
if (fieldName === "getCurationDelegationStatus") return getCurationDelegationStatusAction(event as Parameters<GetCurationDelegationStatusHandler>[0]);
|
|
133
|
+
if (fieldName === "recordDelegatedCuration") return recordDelegatedCuration(event as Parameters<RecordDelegatedCurationHandler>[0]);
|
|
91
134
|
if (fieldName === "setReferenceQualityRating") return setReferenceQualityRating(event as Parameters<ReferenceQualityHandler>[0]);
|
|
92
|
-
if (fieldName === "
|
|
135
|
+
if (fieldName === "createReferenceNote") return createReferenceNote(event as Parameters<ReferenceNoteHandler>[0]);
|
|
93
136
|
if (fieldName === "moveReferenceCorpus") return moveReferenceCorpus(event as Parameters<MoveReferenceCorpusHandler>[0]);
|
|
94
137
|
if (fieldName === "startReferenceCuration") return startReferenceCuration(event as Parameters<StartReferenceCurationHandler>[0]);
|
|
95
138
|
if (fieldName === "getReferenceCurationStatus") return getReferenceCurationStatus(event as Parameters<GetReferenceCurationStatusHandler>[0]);
|
|
@@ -162,13 +205,21 @@ async function reviewSteeringProposal(event: Parameters<ReviewHandler>[0]) {
|
|
|
162
205
|
};
|
|
163
206
|
}
|
|
164
207
|
|
|
165
|
-
async function reviewReferenceCuration(
|
|
208
|
+
async function reviewReferenceCuration(
|
|
209
|
+
event: Parameters<ReferenceCurationHandler>[0],
|
|
210
|
+
delegation: { delegationId: string; relayedBy: string; instruction: string } | null = null,
|
|
211
|
+
) {
|
|
166
212
|
const client = await getDataClient();
|
|
167
213
|
const referenceId = normalizeRequiredString(event.arguments.referenceId, "referenceId");
|
|
168
214
|
const action = normalizeReferenceCurationAction(event.arguments.action);
|
|
169
215
|
const nextStatus = referenceCurationStatusForAction(action);
|
|
170
216
|
const reasonCode = normalizeReferenceRejectionReasonCode(event.arguments.reasonCode, action === "reject");
|
|
171
217
|
const reference = await getRequiredRecord(client.models.Reference, referenceId, "Reference");
|
|
218
|
+
const requestedSectionId = normalizeOptionalString(event.arguments.sectionId);
|
|
219
|
+
const sectionRecord = requestedSectionId
|
|
220
|
+
? (await client.models.NewsroomSection.get({ id: requestedSectionId })).data ?? null
|
|
221
|
+
: null;
|
|
222
|
+
const sectionId = validatedCurationSectionId(requestedSectionId, action, sectionRecord);
|
|
172
223
|
const now = new Date().toISOString();
|
|
173
224
|
const actorSub = normalizeOptionalString(event.arguments.actorSub) ?? getIdentitySub(event);
|
|
174
225
|
const actorLabel = normalizeOptionalString(event.arguments.actorLabel) ?? getIdentityLabel(event);
|
|
@@ -234,6 +285,9 @@ async function reviewReferenceCuration(event: Parameters<ReferenceCurationHandle
|
|
|
234
285
|
referenceLineageId,
|
|
235
286
|
decisionRelationId: normalizeOptionalString(event.arguments.decisionRelationId),
|
|
236
287
|
shareable: event.arguments.shareable ?? null,
|
|
288
|
+
actorKind: delegation ? "human" : curationActorKind(event.identity),
|
|
289
|
+
sectionId,
|
|
290
|
+
delegation,
|
|
237
291
|
}),
|
|
238
292
|
{ filename: "metadata.json", now },
|
|
239
293
|
);
|
|
@@ -300,6 +354,204 @@ async function reviewReferenceCuration(event: Parameters<ReferenceCurationHandle
|
|
|
300
354
|
};
|
|
301
355
|
}
|
|
302
356
|
|
|
357
|
+
function requireSignedInPerson(event: { identity?: unknown }): { sub: string; label: string; isAdmin: boolean } {
|
|
358
|
+
if (curationActorKind(event.identity) !== "human") {
|
|
359
|
+
throw new Error("Only a person signed in to the Newsroom can manage curation delegations.");
|
|
360
|
+
}
|
|
361
|
+
const identity = event.identity as { sub?: unknown; claims?: Record<string, unknown> };
|
|
362
|
+
const claims = identity.claims ?? {};
|
|
363
|
+
const groups = Array.isArray(claims["cognito:groups"]) ? (claims["cognito:groups"] as unknown[]) : [];
|
|
364
|
+
return {
|
|
365
|
+
sub: normalizeRequiredString(claims.sub ?? identity.sub, "identity.sub"),
|
|
366
|
+
label: normalizeOptionalString(claims.email) ?? normalizeOptionalString(claims.name) ?? "Newsroom editor",
|
|
367
|
+
isAdmin: groups.includes("admin"),
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
async function readCurationDelegation(client: DataClient, delegationId: string): Promise<CurationDelegation | null> {
|
|
372
|
+
const stored = await readJsonModelPayload(client as any, "knowledgeRawPayload", delegationId, "raw_payload", CURATION_DELEGATION_PAYLOAD_KIND);
|
|
373
|
+
return stored ? (stored as unknown as CurationDelegation) : null;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
async function writeCurationDelegation(client: DataClient, delegation: CurationDelegation, now: string, created: boolean) {
|
|
377
|
+
const input = {
|
|
378
|
+
id: delegation.delegationId,
|
|
379
|
+
ownerType: CURATION_DELEGATION_OWNER_TYPE,
|
|
380
|
+
ownerId: delegation.grantorSub,
|
|
381
|
+
payloadKind: CURATION_DELEGATION_PAYLOAD_KIND,
|
|
382
|
+
createdAt: delegation.createdAt,
|
|
383
|
+
updatedAt: now,
|
|
384
|
+
};
|
|
385
|
+
if (created) {
|
|
386
|
+
await requireDataResult(client.models.KnowledgeRawPayload.create(input), "create curation delegation");
|
|
387
|
+
} else {
|
|
388
|
+
await requireDataResult(client.models.KnowledgeRawPayload.update(input), "update curation delegation");
|
|
389
|
+
}
|
|
390
|
+
await putJsonModelPayload(
|
|
391
|
+
client as any,
|
|
392
|
+
{ ownerKind: "knowledgeRawPayload", ownerId: delegation.delegationId },
|
|
393
|
+
"raw_payload",
|
|
394
|
+
CURATION_DELEGATION_PAYLOAD_KIND,
|
|
395
|
+
delegation as unknown as Record<string, unknown>,
|
|
396
|
+
{ filename: "curation-delegation.json", now },
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function delegationResult(delegation: CurationDelegation, token: string | null) {
|
|
401
|
+
return { ...flowDelegationResult(delegation, new Date().toISOString()), token };
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
async function listPendingCurationDelegations(client: DataClient): Promise<CurationDelegation[]> {
|
|
405
|
+
const pending: CurationDelegation[] = [];
|
|
406
|
+
let nextToken: string | null | undefined;
|
|
407
|
+
do {
|
|
408
|
+
const page: any = await (client.models.KnowledgeRawPayload as any).listKnowledgeRawPayloadsByOwnerAndKind(
|
|
409
|
+
{ ownerId: PENDING_REQUEST_GRANTOR_SUB, payloadKind: { eq: CURATION_DELEGATION_PAYLOAD_KIND } },
|
|
410
|
+
{ nextToken },
|
|
411
|
+
);
|
|
412
|
+
assertNoDataErrors(page.errors, "list pending curation delegations");
|
|
413
|
+
for (const payload of page.data ?? []) {
|
|
414
|
+
const delegation = await readCurationDelegation(client, payload.id);
|
|
415
|
+
if (delegation) pending.push(delegation);
|
|
416
|
+
}
|
|
417
|
+
nextToken = page.nextToken;
|
|
418
|
+
} while (nextToken);
|
|
419
|
+
return pending;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function curationDelegationStore(client: DataClient): DelegationStore {
|
|
423
|
+
return {
|
|
424
|
+
read: (delegationId) => readCurationDelegation(client, delegationId),
|
|
425
|
+
write: (delegation, now, created) => writeCurationDelegation(client, delegation, now, created),
|
|
426
|
+
listPending: () => listPendingCurationDelegations(client),
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
async function requestCurationDelegationAction(event: Parameters<RequestCurationDelegationHandler>[0]) {
|
|
431
|
+
const client = await getDataClient();
|
|
432
|
+
return requestDelegation(
|
|
433
|
+
curationDelegationStore(client),
|
|
434
|
+
{
|
|
435
|
+
delegationId: normalizeRequiredString(event.arguments.delegationId, "delegationId"),
|
|
436
|
+
tokenHash: normalizeRequiredString(event.arguments.tokenHash, "tokenHash"),
|
|
437
|
+
agentLabel: normalizeRequiredString(event.arguments.agentLabel, "agentLabel"),
|
|
438
|
+
hours: event.arguments.hours,
|
|
439
|
+
},
|
|
440
|
+
new Date().toISOString(),
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
async function approveCurationDelegationAction(event: Parameters<ApproveCurationDelegationHandler>[0]) {
|
|
445
|
+
const person = requireSignedInPerson(event);
|
|
446
|
+
const client = await getDataClient();
|
|
447
|
+
const now = new Date().toISOString();
|
|
448
|
+
return { ...(await approveDelegation(curationDelegationStore(client), person, normalizeRequiredString(event.arguments.userCode, "userCode"), now)), token: null };
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
async function denyCurationDelegationAction(event: Parameters<DenyCurationDelegationHandler>[0]) {
|
|
452
|
+
const person = requireSignedInPerson(event);
|
|
453
|
+
const client = await getDataClient();
|
|
454
|
+
const now = new Date().toISOString();
|
|
455
|
+
return { ...(await denyDelegation(curationDelegationStore(client), person, normalizeRequiredString(event.arguments.userCode, "userCode"), now)), token: null };
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
async function getCurationDelegationStatusAction(event: Parameters<GetCurationDelegationStatusHandler>[0]) {
|
|
459
|
+
const client = await getDataClient();
|
|
460
|
+
return delegationStatus(curationDelegationStore(client), normalizeRequiredString(event.arguments.delegationId, "delegationId"), new Date().toISOString());
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
async function createCurationDelegation(event: Parameters<CreateCurationDelegationHandler>[0]) {
|
|
464
|
+
const person = requireSignedInPerson(event);
|
|
465
|
+
const client = await getDataClient();
|
|
466
|
+
const now = new Date().toISOString();
|
|
467
|
+
const delegationId = `curation-delegation-${randomUUID()}`;
|
|
468
|
+
const { token, tokenHash } = newDelegationToken(delegationId);
|
|
469
|
+
const delegation: CurationDelegation = {
|
|
470
|
+
delegationId,
|
|
471
|
+
tokenHash,
|
|
472
|
+
grantorSub: person.sub,
|
|
473
|
+
grantorLabel: person.label,
|
|
474
|
+
agentLabel: normalizeRequiredString(event.arguments.agentLabel, "agentLabel"),
|
|
475
|
+
createdAt: now,
|
|
476
|
+
expiresAt: delegationExpiry(now, event.arguments.hours),
|
|
477
|
+
revokedAt: null,
|
|
478
|
+
};
|
|
479
|
+
await writeCurationDelegation(client, delegation, now, true);
|
|
480
|
+
return delegationResult(delegation, token);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
async function revokeCurationDelegation(event: Parameters<RevokeCurationDelegationHandler>[0]) {
|
|
484
|
+
const person = requireSignedInPerson(event);
|
|
485
|
+
const client = await getDataClient();
|
|
486
|
+
const delegation = await readCurationDelegation(client, normalizeRequiredString(event.arguments.delegationId, "delegationId"));
|
|
487
|
+
if (!delegation) throw new Error("That delegation was not found.");
|
|
488
|
+
if (delegation.grantorSub !== person.sub && delegation.grantorSub !== PENDING_REQUEST_GRANTOR_SUB && !person.isAdmin) throw new Error("Only the person who granted a delegation can revoke it.");
|
|
489
|
+
const now = new Date().toISOString();
|
|
490
|
+
const revoked = { ...delegation, revokedAt: delegation.revokedAt ?? now };
|
|
491
|
+
await writeCurationDelegation(client, revoked, now, false);
|
|
492
|
+
return delegationResult(revoked, null);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
async function listCurationDelegations(event: Parameters<ListCurationDelegationsHandler>[0]) {
|
|
496
|
+
const person = requireSignedInPerson(event);
|
|
497
|
+
const client = await getDataClient();
|
|
498
|
+
const delegations: CurationDelegation[] = [];
|
|
499
|
+
let nextToken: string | null | undefined;
|
|
500
|
+
do {
|
|
501
|
+
const page: any = await (client.models.KnowledgeRawPayload as any).listKnowledgeRawPayloadsByOwnerAndKind(
|
|
502
|
+
{ ownerId: person.sub, payloadKind: { eq: CURATION_DELEGATION_PAYLOAD_KIND } },
|
|
503
|
+
{ nextToken },
|
|
504
|
+
);
|
|
505
|
+
assertNoDataErrors(page.errors, "list curation delegations");
|
|
506
|
+
for (const payload of page.data ?? []) {
|
|
507
|
+
const delegation = await readCurationDelegation(client, payload.id);
|
|
508
|
+
if (delegation) delegations.push(delegation);
|
|
509
|
+
}
|
|
510
|
+
nextToken = page.nextToken;
|
|
511
|
+
} while (nextToken);
|
|
512
|
+
const now = new Date().toISOString();
|
|
513
|
+
delegations.push(...(await livePendingRequests(curationDelegationStore(client), now)));
|
|
514
|
+
return delegations
|
|
515
|
+
.sort((first, second) => second.createdAt.localeCompare(first.createdAt))
|
|
516
|
+
.map((delegation) => delegationResult(delegation, null));
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
async function recordDelegatedCuration(event: Parameters<RecordDelegatedCurationHandler>[0]) {
|
|
520
|
+
const client = await getDataClient();
|
|
521
|
+
const now = new Date().toISOString();
|
|
522
|
+
const delegationId = normalizeRequiredString(event.arguments.delegationId, "delegationId");
|
|
523
|
+
const agentLabel = normalizeRequiredString(event.arguments.agentLabel, "agentLabel");
|
|
524
|
+
const delegation = await readCurationDelegation(client, delegationId);
|
|
525
|
+
const verdict = delegationVerdict(delegation, {
|
|
526
|
+
token: normalizeRequiredString(event.arguments.delegationToken, "delegationToken"),
|
|
527
|
+
agentLabel,
|
|
528
|
+
now,
|
|
529
|
+
});
|
|
530
|
+
if (!verdict.ok || !delegation) {
|
|
531
|
+
throw new Error(delegationRefusalMessage(verdict.ok ? "unknown-token" : verdict.reason));
|
|
532
|
+
}
|
|
533
|
+
const instruction = normalizeRequiredString(event.arguments.instruction, "instruction").slice(0, 2000);
|
|
534
|
+
const namedPerson = normalizeRequiredString(event.arguments.onBehalfOf, "onBehalfOf");
|
|
535
|
+
if (namedPerson.toLowerCase() !== delegation.grantorLabel.toLowerCase()) {
|
|
536
|
+
throw new Error(`That delegation was granted by ${delegation.grantorLabel}, not ${namedPerson}.`);
|
|
537
|
+
}
|
|
538
|
+
const onBehalfOfPerson = {
|
|
539
|
+
...event,
|
|
540
|
+
arguments: {
|
|
541
|
+
referenceId: event.arguments.referenceId,
|
|
542
|
+
action: event.arguments.action,
|
|
543
|
+
note: event.arguments.note,
|
|
544
|
+
reasonCode: event.arguments.reasonCode,
|
|
545
|
+
decisionRelationId: event.arguments.decisionRelationId,
|
|
546
|
+
shareable: event.arguments.shareable,
|
|
547
|
+
sectionId: event.arguments.sectionId,
|
|
548
|
+
actorSub: delegation.grantorSub,
|
|
549
|
+
actorLabel: delegation.grantorLabel,
|
|
550
|
+
},
|
|
551
|
+
} as unknown as Parameters<ReferenceCurationHandler>[0];
|
|
552
|
+
return reviewReferenceCuration(onBehalfOfPerson, { delegationId, relayedBy: agentLabel, instruction });
|
|
553
|
+
}
|
|
554
|
+
|
|
303
555
|
async function setReferenceQualityRating(event: Parameters<ReferenceQualityHandler>[0]) {
|
|
304
556
|
const client = await getDataClient();
|
|
305
557
|
const referenceId = normalizeRequiredString(event.arguments.referenceId, "referenceId");
|
|
@@ -403,7 +655,7 @@ async function setReferenceQualityRating(event: Parameters<ReferenceQualityHandl
|
|
|
403
655
|
};
|
|
404
656
|
}
|
|
405
657
|
|
|
406
|
-
async function
|
|
658
|
+
async function createReferenceNote(event: Parameters<ReferenceNoteHandler>[0]) {
|
|
407
659
|
const client = await getDataClient();
|
|
408
660
|
const referenceId = normalizeRequiredString(event.arguments.referenceId, "referenceId");
|
|
409
661
|
const summary = normalizeRequiredString(event.arguments.summary, "summary");
|
|
@@ -414,10 +666,10 @@ async function createReferenceInsight(event: Parameters<ReferenceInsightHandler>
|
|
|
414
666
|
const actorLabel = normalizeOptionalString(event.arguments.actorLabel) ?? getIdentityLabel(event);
|
|
415
667
|
const actor = actorLabel ?? actorSub ?? "Papyrus newsroom";
|
|
416
668
|
const referenceLineageId = normalizeOptionalString(reference.lineageId) ?? referenceId;
|
|
417
|
-
const messageId = `message-reference-
|
|
669
|
+
const messageId = `message-reference-note-${safeId(referenceLineageId)}-${now.replace(/[^0-9TZ]/g, "")}-${randomUUID().slice(0, 8)}`;
|
|
418
670
|
const relationId = `semantic-relation-${hashStable([
|
|
419
671
|
`message#${messageId}`,
|
|
420
|
-
"
|
|
672
|
+
"note_about",
|
|
421
673
|
`reference#${referenceId}`,
|
|
422
674
|
]).slice(0, 24)}`;
|
|
423
675
|
|
|
@@ -425,7 +677,7 @@ async function createReferenceInsight(event: Parameters<ReferenceInsightHandler>
|
|
|
425
677
|
client.models.Message.create({
|
|
426
678
|
...buildCanonicalMessageInput({
|
|
427
679
|
id: messageId,
|
|
428
|
-
messageKind: "
|
|
680
|
+
messageKind: "note",
|
|
429
681
|
messageDomain: "knowledge",
|
|
430
682
|
summary,
|
|
431
683
|
source: "newsroom",
|
|
@@ -436,7 +688,7 @@ async function createReferenceInsight(event: Parameters<ReferenceInsightHandler>
|
|
|
436
688
|
threadId: messageId,
|
|
437
689
|
sequenceNumber: 1,
|
|
438
690
|
}),
|
|
439
|
-
"create
|
|
691
|
+
"create note Message",
|
|
440
692
|
);
|
|
441
693
|
await putTextModelPayload(
|
|
442
694
|
client as any,
|
|
@@ -452,7 +704,7 @@ async function createReferenceInsight(event: Parameters<ReferenceInsightHandler>
|
|
|
452
704
|
"metadata",
|
|
453
705
|
"metadata",
|
|
454
706
|
{
|
|
455
|
-
kind: "reference.
|
|
707
|
+
kind: "reference.note.created",
|
|
456
708
|
targetKind: "reference",
|
|
457
709
|
targetId: referenceId,
|
|
458
710
|
targetLineageId: referenceLineageId,
|
|
@@ -463,8 +715,8 @@ async function createReferenceInsight(event: Parameters<ReferenceInsightHandler>
|
|
|
463
715
|
client.models.SemanticRelation.create({
|
|
464
716
|
id: relationId,
|
|
465
717
|
relationState: "current",
|
|
466
|
-
predicate: "
|
|
467
|
-
...semanticRelationTypeFieldsForPredicate("
|
|
718
|
+
predicate: "note_about",
|
|
719
|
+
...semanticRelationTypeFieldsForPredicate("note_about"),
|
|
468
720
|
subjectKind: "message",
|
|
469
721
|
subjectId: messageId,
|
|
470
722
|
subjectLineageId: messageId,
|
|
@@ -476,7 +728,7 @@ async function createReferenceInsight(event: Parameters<ReferenceInsightHandler>
|
|
|
476
728
|
subjectStateKey: semanticStateKey("message", messageId),
|
|
477
729
|
objectStateKey: semanticStateKey("reference", referenceLineageId),
|
|
478
730
|
objectSubjectStateKey: `${semanticStateKey("reference", referenceLineageId)}#message`,
|
|
479
|
-
predicateObjectStateKey: `
|
|
731
|
+
predicateObjectStateKey: `note_about#${semanticStateKey("reference", referenceLineageId)}`,
|
|
480
732
|
subjectVersionKey: semanticVersionKey("message", messageId),
|
|
481
733
|
objectVersionKey: semanticVersionKey("reference", referenceId),
|
|
482
734
|
score: 1,
|
|
@@ -492,17 +744,17 @@ async function createReferenceInsight(event: Parameters<ReferenceInsightHandler>
|
|
|
492
744
|
updatedAt: now,
|
|
493
745
|
newsroomFeedKey: "semanticRelations",
|
|
494
746
|
metadata: JSON.stringify({
|
|
495
|
-
kind: "reference.
|
|
747
|
+
kind: "reference.note",
|
|
496
748
|
summary,
|
|
497
749
|
actor,
|
|
498
750
|
}),
|
|
499
751
|
}),
|
|
500
|
-
"create
|
|
752
|
+
"create note_about relation",
|
|
501
753
|
);
|
|
502
754
|
await updateNewsroomSummaryForSemanticRelationDelta(
|
|
503
755
|
client,
|
|
504
756
|
{
|
|
505
|
-
relationTypeKey: "
|
|
757
|
+
relationTypeKey: "note_about",
|
|
506
758
|
relationDomain: "commentary",
|
|
507
759
|
subjectKind: "message",
|
|
508
760
|
objectKind: "reference",
|
|
@@ -1497,10 +1749,10 @@ function semanticRelationTypeFieldsForPredicate(predicate: string): {
|
|
|
1497
1749
|
relationDomain: "curation",
|
|
1498
1750
|
};
|
|
1499
1751
|
}
|
|
1500
|
-
if (predicate === "
|
|
1752
|
+
if (predicate === "note_about") {
|
|
1501
1753
|
return {
|
|
1502
|
-
relationTypeId: "semantic-relation-type-
|
|
1503
|
-
relationTypeKey: "
|
|
1754
|
+
relationTypeId: "semantic-relation-type-note-about",
|
|
1755
|
+
relationTypeKey: "note_about",
|
|
1504
1756
|
relationDomain: "knowledge",
|
|
1505
1757
|
};
|
|
1506
1758
|
}
|
|
@@ -2190,7 +2190,7 @@ fn build_openai_messages(
|
|
|
2190
2190
|
) -> Vec<Value> {
|
|
2191
2191
|
let mut messages = vec![json!({
|
|
2192
2192
|
"role": "system",
|
|
2193
|
-
"content": "You are Papyrus, an editorial assistant for an autonomous newsroom. Be concise, accurate, and concrete. Raw console chat turns are working memory and are excluded from default semantic searches unless explicitly requested. When a chat produces durable
|
|
2193
|
+
"content": "You are Papyrus, an editorial assistant for an autonomous newsroom. Be concise, accurate, and concrete. Raw console chat turns are working memory and are excluded from default semantic searches unless explicitly requested. When a chat produces durable note, recommend creating a note Message instead of making every chat turn canonical knowledge. Use execute_tactus for Papyrus runtime work. Distinguish references from assignments: Reference.* is for scholarly sources; Assignment.* is for newsroom work items (research, curation, intake). When the user provides a URL and asks to add, register, or file a reference, immediately call Reference.create{ url = \"<url>\", title = \"<optional>\", apply = true }—do not use Reference.register, papyrus.reference.create, or Assignment.create unless they asked for research work. For \"most recent references\" or similar, immediately call Reference.list{ limit = <count>, order = \"newest\" } and summarize. For \"last/latest/most recent research assignment\" (or \"research assignment\" without a specific id), immediately call Assignment.list{ type = \"research\", limit = 1 } (includes research.candidate, research.tavily-deep, and other research.* types), then Assignment.get{ id = \"<that id>\" } if the user wants detail—do not use Reference.list and do not list all assignment types (curation.reference-intake is not a research assignment). To create research work, use Assignment.create{ type = \"research\", title = \"...\", apply = true }."
|
|
2194
2194
|
})];
|
|
2195
2195
|
if !static_prompt.publication_mission.trim().is_empty() {
|
|
2196
2196
|
messages.push(json!({
|
|
@@ -22,6 +22,7 @@ from papyrus_content.rebuild_trigger import (
|
|
|
22
22
|
trigger_rebuild,
|
|
23
23
|
trigger_staging_build,
|
|
24
24
|
)
|
|
25
|
+
from papyrus_content.story_reporting import save_story_version
|
|
25
26
|
from papyrus_content.publishing import (
|
|
26
27
|
ItemFields,
|
|
27
28
|
PublishError,
|
|
@@ -188,6 +189,28 @@ def run_projection_task(task: dict) -> dict:
|
|
|
188
189
|
|
|
189
190
|
def save_item_draft_action(arguments: dict, actor: str) -> dict:
|
|
190
191
|
client, _claims = create_authoring_client()
|
|
192
|
+
if arguments.get("newVersion") and arguments.get("id"):
|
|
193
|
+
existing = client.get_record("Item", arguments["id"])
|
|
194
|
+
if existing is None:
|
|
195
|
+
raise PublishError("not-found", "That story was not found.")
|
|
196
|
+
record = save_story_version(
|
|
197
|
+
client,
|
|
198
|
+
existing.get("lineageId") or existing["id"],
|
|
199
|
+
front_matter_yaml=arguments.get("frontMatterYaml"),
|
|
200
|
+
body_markus=arguments.get("bodyMarkus") or "",
|
|
201
|
+
actor=actor,
|
|
202
|
+
change_reason=arguments.get("changeReason") or "edited in the newsroom",
|
|
203
|
+
)
|
|
204
|
+
return {
|
|
205
|
+
"ok": True,
|
|
206
|
+
"item": {
|
|
207
|
+
"id": record["id"],
|
|
208
|
+
"contentHash": record["contentHash"],
|
|
209
|
+
"status": record["status"],
|
|
210
|
+
"slug": record["slug"],
|
|
211
|
+
"versionNumber": record.get("versionNumber"),
|
|
212
|
+
},
|
|
213
|
+
}
|
|
191
214
|
fields = ItemFields(
|
|
192
215
|
type=arguments["type"],
|
|
193
216
|
slug=arguments["slug"],
|
|
@@ -7,7 +7,7 @@ import { fileURLToPath } from "node:url";
|
|
|
7
7
|
import { pythonLambdaCode } from "../shared/python-bundle.js";
|
|
8
8
|
const dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
9
9
|
const projectRoot = path.resolve(dirname, "../../..");
|
|
10
|
-
export const contentActionsRequirements = ["PyYAML>=6.0.2,<7", "citeproc-py>=0.8.2,<1", "anthus-markus==0.
|
|
10
|
+
export const contentActionsRequirements = ["PyYAML>=6.0.2,<7", "citeproc-py>=0.8.2,<1", "anthus-markus==0.7.0", "Pillow>=11.3,<12", "awscrt>=0.23,<1"];
|
|
11
11
|
function installCheckoutRequirements(outputDir) {
|
|
12
12
|
const requirementsPath = path.join(projectRoot, "amplify/functions/content-actions/requirements.txt");
|
|
13
13
|
execSync(`python3 -m pip install -r "${requirementsPath}" -t "${outputDir}" --no-cache-dir --platform manylinux2014_aarch64 --implementation cp --python-version 3.12 --only-binary=:all: --ignore-requires-python`, { cwd: projectRoot, stdio: "inherit" });
|
|
@@ -10,7 +10,7 @@ import { pythonLambdaCode, type SiteBundleFile } from "../shared/python-bundle";
|
|
|
10
10
|
const dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
11
11
|
const projectRoot = path.resolve(dirname, "../../..");
|
|
12
12
|
|
|
13
|
-
export const contentActionsRequirements = ["PyYAML>=6.0.2,<7", "citeproc-py>=0.8.2,<1", "anthus-markus==0.
|
|
13
|
+
export const contentActionsRequirements = ["PyYAML>=6.0.2,<7", "citeproc-py>=0.8.2,<1", "anthus-markus==0.7.0", "Pillow>=11.3,<12", "awscrt>=0.23,<1"];
|
|
14
14
|
|
|
15
15
|
function installCheckoutRequirements(outputDir: string): void {
|
|
16
16
|
const requirementsPath = path.join(projectRoot, "amplify/functions/content-actions/requirements.txt");
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import {
|
|
2
|
+
EXPIRED_REQUEST_GRANTOR_SUB,
|
|
3
|
+
MAXIMUM_PENDING_DELEGATION_REQUESTS,
|
|
4
|
+
PENDING_DELEGATION_MINUTES,
|
|
5
|
+
PENDING_REQUEST_GRANTOR_SUB,
|
|
6
|
+
delegationExpiry,
|
|
7
|
+
delegationState,
|
|
8
|
+
formatUserCode,
|
|
9
|
+
newUserCode,
|
|
10
|
+
normalizeUserCode,
|
|
11
|
+
type CurationDelegation,
|
|
12
|
+
} from "./curation-delegation"
|
|
13
|
+
|
|
14
|
+
export type DelegationStore = {
|
|
15
|
+
read(delegationId: string): Promise<CurationDelegation | null>
|
|
16
|
+
write(delegation: CurationDelegation, now: string, created: boolean): Promise<void>
|
|
17
|
+
listPending(): Promise<CurationDelegation[]>
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type SignedInPerson = { sub: string; label: string; isAdmin: boolean }
|
|
21
|
+
|
|
22
|
+
const DELEGATION_ID_PATTERN = /^curation-delegation-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/
|
|
23
|
+
const TOKEN_HASH_PATTERN = /^[0-9a-f]{64}$/
|
|
24
|
+
const MAXIMUM_AGENT_LABEL_LENGTH = 80
|
|
25
|
+
const USER_CODE_ATTEMPTS = 50
|
|
26
|
+
|
|
27
|
+
export function delegationResult(delegation: CurationDelegation, now: string) {
|
|
28
|
+
const state = delegationState(delegation, now)
|
|
29
|
+
return {
|
|
30
|
+
ok: true,
|
|
31
|
+
delegationId: delegation.delegationId,
|
|
32
|
+
token: null,
|
|
33
|
+
agentLabel: delegation.agentLabel,
|
|
34
|
+
grantorLabel: delegation.grantorLabel || null,
|
|
35
|
+
expiresAt: delegation.expiresAt,
|
|
36
|
+
revokedAt: delegation.revokedAt,
|
|
37
|
+
state,
|
|
38
|
+
userCode: state === "pending" && delegation.userCode ? formatUserCode(delegation.userCode) : null,
|
|
39
|
+
requestedHours: delegation.requestedHours ?? null,
|
|
40
|
+
pendingExpiresAt: delegation.pendingExpiresAt ?? null,
|
|
41
|
+
approvedAt: delegation.approvedAt ?? null,
|
|
42
|
+
deniedAt: delegation.deniedAt ?? null,
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function livePendingRequests(store: DelegationStore, now: string): Promise<CurationDelegation[]> {
|
|
47
|
+
const live: CurationDelegation[] = []
|
|
48
|
+
for (const candidate of await store.listPending()) {
|
|
49
|
+
if (delegationState(candidate, now) === "pending") {
|
|
50
|
+
live.push(candidate)
|
|
51
|
+
} else if (candidate.grantorSub === PENDING_REQUEST_GRANTOR_SUB) {
|
|
52
|
+
await store.write({ ...candidate, grantorSub: EXPIRED_REQUEST_GRANTOR_SUB }, now, false)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return live
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function requestDelegation(
|
|
59
|
+
store: DelegationStore,
|
|
60
|
+
input: { delegationId: string; tokenHash: string; agentLabel: string; hours: number | null | undefined },
|
|
61
|
+
now: string,
|
|
62
|
+
) {
|
|
63
|
+
if (!DELEGATION_ID_PATTERN.test(input.delegationId)) {
|
|
64
|
+
throw new Error("The delegation id must look like curation-delegation-<uuid>.")
|
|
65
|
+
}
|
|
66
|
+
if (!TOKEN_HASH_PATTERN.test(input.tokenHash)) throw new Error("The token hash must be 64 lowercase hexadecimal characters.")
|
|
67
|
+
const agentLabel = input.agentLabel.trim()
|
|
68
|
+
if (!agentLabel) throw new Error("Name the agent asking for the delegation.")
|
|
69
|
+
if (agentLabel.length > MAXIMUM_AGENT_LABEL_LENGTH) throw new Error("The agent name is too long.")
|
|
70
|
+
const requestedHours = input.hours ?? 8
|
|
71
|
+
delegationExpiry(now, requestedHours)
|
|
72
|
+
if (await store.read(input.delegationId)) throw new Error("That delegation id is already in use.")
|
|
73
|
+
const live = await livePendingRequests(store, now)
|
|
74
|
+
if (live.length >= MAXIMUM_PENDING_DELEGATION_REQUESTS) {
|
|
75
|
+
throw new Error("Too many delegation requests are waiting for approval. Try again later.")
|
|
76
|
+
}
|
|
77
|
+
const takenCodes = new Set(live.map((request) => request.userCode))
|
|
78
|
+
let userCode = newUserCode()
|
|
79
|
+
for (let attempt = 0; takenCodes.has(userCode); attempt += 1) {
|
|
80
|
+
if (attempt >= USER_CODE_ATTEMPTS) throw new Error("Could not allocate a unique approval code. Try again.")
|
|
81
|
+
userCode = newUserCode()
|
|
82
|
+
}
|
|
83
|
+
const pendingExpiresAt = new Date(Date.parse(now) + PENDING_DELEGATION_MINUTES * 60_000).toISOString()
|
|
84
|
+
const delegation: CurationDelegation = {
|
|
85
|
+
delegationId: input.delegationId,
|
|
86
|
+
tokenHash: input.tokenHash,
|
|
87
|
+
grantorSub: PENDING_REQUEST_GRANTOR_SUB,
|
|
88
|
+
grantorLabel: "",
|
|
89
|
+
agentLabel,
|
|
90
|
+
createdAt: now,
|
|
91
|
+
expiresAt: pendingExpiresAt,
|
|
92
|
+
revokedAt: null,
|
|
93
|
+
userCode,
|
|
94
|
+
requestedHours,
|
|
95
|
+
pendingExpiresAt,
|
|
96
|
+
approvedAt: null,
|
|
97
|
+
deniedAt: null,
|
|
98
|
+
}
|
|
99
|
+
await store.write(delegation, now, true)
|
|
100
|
+
return {
|
|
101
|
+
ok: true,
|
|
102
|
+
delegationId: delegation.delegationId,
|
|
103
|
+
userCode: formatUserCode(userCode),
|
|
104
|
+
pendingExpiresAt,
|
|
105
|
+
requestedHours,
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function findLivePendingByCode(store: DelegationStore, typedCode: string, now: string): Promise<CurationDelegation> {
|
|
110
|
+
const code = normalizeUserCode(typedCode)
|
|
111
|
+
const found = code ? (await livePendingRequests(store, now)).find((request) => request.userCode === code) : undefined
|
|
112
|
+
if (!found) throw new Error("No pending delegation request has that code. It may have expired or already been decided.")
|
|
113
|
+
return found
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export async function approveDelegation(store: DelegationStore, person: SignedInPerson, typedCode: string, now: string) {
|
|
117
|
+
const pending = await findLivePendingByCode(store, typedCode, now)
|
|
118
|
+
const approved: CurationDelegation = {
|
|
119
|
+
...pending,
|
|
120
|
+
grantorSub: person.sub,
|
|
121
|
+
grantorLabel: person.label,
|
|
122
|
+
createdAt: now,
|
|
123
|
+
expiresAt: delegationExpiry(now, pending.requestedHours),
|
|
124
|
+
approvedAt: now,
|
|
125
|
+
userCode: null,
|
|
126
|
+
}
|
|
127
|
+
await store.write(approved, now, false)
|
|
128
|
+
return delegationResult(approved, now)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export async function denyDelegation(store: DelegationStore, person: SignedInPerson, typedCode: string, now: string) {
|
|
132
|
+
const pending = await findLivePendingByCode(store, typedCode, now)
|
|
133
|
+
const denied: CurationDelegation = { ...pending, grantorSub: person.sub, grantorLabel: person.label, deniedAt: now, userCode: null }
|
|
134
|
+
await store.write(denied, now, false)
|
|
135
|
+
return delegationResult(denied, now)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export async function delegationStatus(store: DelegationStore, delegationId: string, now: string) {
|
|
139
|
+
const delegation = await store.read(delegationId)
|
|
140
|
+
if (!delegation) throw new Error("That delegation was not found.")
|
|
141
|
+
return {
|
|
142
|
+
delegationId: delegation.delegationId,
|
|
143
|
+
state: delegationState(delegation, now),
|
|
144
|
+
expiresAt: delegation.expiresAt,
|
|
145
|
+
grantorLabel: delegation.grantorLabel || null,
|
|
146
|
+
}
|
|
147
|
+
}
|