@anthusai/papyrus 1.14.0 → 1.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/amplify/data/resource.js +62 -0
- package/amplify/data/resource.ts +68 -0
- package/amplify/data/schema.js +62 -0
- package/amplify/data/schema.ts +68 -0
- package/amplify/functions/category-action/handler.ts +83 -8
- package/amplify/functions/shared/curation-delegation-flow.ts +147 -0
- package/amplify/functions/shared/curation-delegation.ts +50 -2
- package/app/api/auth/prospectus/accounts/route.ts +9 -0
- package/components/administration-delegations-panel.tsx +52 -100
- package/components/curation-delegation-client.ts +46 -23
- package/components/newsroom-organizer.css +21 -0
- package/components/prospectus-account-chooser.tsx +34 -1
- package/lib/curation-delegation-helpers.ts +35 -25
- package/lib/prospectus-account-client.ts +26 -0
- package/lib/prospectus-sign-in-routes.ts +45 -0
- package/lib/prospectus-site-identity.ts +13 -1
- package/package.json +2 -1
- package/routes.manifest.json +12 -1
package/amplify/data/resource.js
CHANGED
|
@@ -219,6 +219,25 @@ const schema = a.schema({
|
|
|
219
219
|
grantorLabel: a.string(),
|
|
220
220
|
expiresAt: a.datetime().required(),
|
|
221
221
|
revokedAt: a.datetime(),
|
|
222
|
+
state: a.string(),
|
|
223
|
+
userCode: a.string(),
|
|
224
|
+
requestedHours: a.integer(),
|
|
225
|
+
pendingExpiresAt: a.datetime(),
|
|
226
|
+
approvedAt: a.datetime(),
|
|
227
|
+
deniedAt: a.datetime(),
|
|
228
|
+
}),
|
|
229
|
+
CurationDelegationRequestResult: a.customType({
|
|
230
|
+
ok: a.boolean().required(),
|
|
231
|
+
delegationId: a.id().required(),
|
|
232
|
+
userCode: a.string().required(),
|
|
233
|
+
pendingExpiresAt: a.datetime().required(),
|
|
234
|
+
requestedHours: a.integer().required(),
|
|
235
|
+
}),
|
|
236
|
+
CurationDelegationStatusResult: a.customType({
|
|
237
|
+
delegationId: a.id().required(),
|
|
238
|
+
state: a.string().required(),
|
|
239
|
+
expiresAt: a.datetime().required(),
|
|
240
|
+
grantorLabel: a.string(),
|
|
222
241
|
}),
|
|
223
242
|
ReferenceCurationActionResult: a.customType({
|
|
224
243
|
ok: a.boolean().required(),
|
|
@@ -333,6 +352,49 @@ const schema = a.schema({
|
|
|
333
352
|
allow.groups(categoryWriteGroups),
|
|
334
353
|
])
|
|
335
354
|
.handler(a.handler.function(categoryAction)),
|
|
355
|
+
requestCurationDelegation: a
|
|
356
|
+
.mutation()
|
|
357
|
+
.arguments({
|
|
358
|
+
delegationId: a.id().required(),
|
|
359
|
+
tokenHash: a.string().required(),
|
|
360
|
+
agentLabel: a.string().required(),
|
|
361
|
+
hours: a.integer(),
|
|
362
|
+
})
|
|
363
|
+
.returns(a.ref("CurationDelegationRequestResult"))
|
|
364
|
+
.authorization((allow) => [
|
|
365
|
+
allow.authenticated("identityPool"),
|
|
366
|
+
])
|
|
367
|
+
.handler(a.handler.function(categoryAction)),
|
|
368
|
+
approveCurationDelegation: a
|
|
369
|
+
.mutation()
|
|
370
|
+
.arguments({
|
|
371
|
+
userCode: a.string().required(),
|
|
372
|
+
})
|
|
373
|
+
.returns(a.ref("CurationDelegationResult"))
|
|
374
|
+
.authorization((allow) => [
|
|
375
|
+
allow.groups(categoryWriteGroups),
|
|
376
|
+
])
|
|
377
|
+
.handler(a.handler.function(categoryAction)),
|
|
378
|
+
denyCurationDelegation: a
|
|
379
|
+
.mutation()
|
|
380
|
+
.arguments({
|
|
381
|
+
userCode: a.string().required(),
|
|
382
|
+
})
|
|
383
|
+
.returns(a.ref("CurationDelegationResult"))
|
|
384
|
+
.authorization((allow) => [
|
|
385
|
+
allow.groups(categoryWriteGroups),
|
|
386
|
+
])
|
|
387
|
+
.handler(a.handler.function(categoryAction)),
|
|
388
|
+
getCurationDelegationStatus: a
|
|
389
|
+
.query()
|
|
390
|
+
.arguments({
|
|
391
|
+
delegationId: a.id().required(),
|
|
392
|
+
})
|
|
393
|
+
.returns(a.ref("CurationDelegationStatusResult"))
|
|
394
|
+
.authorization((allow) => [
|
|
395
|
+
allow.authenticated("identityPool"),
|
|
396
|
+
])
|
|
397
|
+
.handler(a.handler.function(categoryAction)),
|
|
336
398
|
recordDelegatedCuration: a
|
|
337
399
|
.mutation()
|
|
338
400
|
.arguments({
|
package/amplify/data/resource.ts
CHANGED
|
@@ -236,6 +236,27 @@ const schema = a.schema({
|
|
|
236
236
|
grantorLabel: a.string(),
|
|
237
237
|
expiresAt: a.datetime().required(),
|
|
238
238
|
revokedAt: a.datetime(),
|
|
239
|
+
state: a.string(),
|
|
240
|
+
userCode: a.string(),
|
|
241
|
+
requestedHours: a.integer(),
|
|
242
|
+
pendingExpiresAt: a.datetime(),
|
|
243
|
+
approvedAt: a.datetime(),
|
|
244
|
+
deniedAt: a.datetime(),
|
|
245
|
+
}),
|
|
246
|
+
|
|
247
|
+
CurationDelegationRequestResult: a.customType({
|
|
248
|
+
ok: a.boolean().required(),
|
|
249
|
+
delegationId: a.id().required(),
|
|
250
|
+
userCode: a.string().required(),
|
|
251
|
+
pendingExpiresAt: a.datetime().required(),
|
|
252
|
+
requestedHours: a.integer().required(),
|
|
253
|
+
}),
|
|
254
|
+
|
|
255
|
+
CurationDelegationStatusResult: a.customType({
|
|
256
|
+
delegationId: a.id().required(),
|
|
257
|
+
state: a.string().required(),
|
|
258
|
+
expiresAt: a.datetime().required(),
|
|
259
|
+
grantorLabel: a.string(),
|
|
239
260
|
}),
|
|
240
261
|
|
|
241
262
|
ReferenceCurationActionResult: a.customType({
|
|
@@ -363,6 +384,53 @@ const schema = a.schema({
|
|
|
363
384
|
])
|
|
364
385
|
.handler(a.handler.function(categoryAction)),
|
|
365
386
|
|
|
387
|
+
requestCurationDelegation: a
|
|
388
|
+
.mutation()
|
|
389
|
+
.arguments({
|
|
390
|
+
delegationId: a.id().required(),
|
|
391
|
+
tokenHash: a.string().required(),
|
|
392
|
+
agentLabel: a.string().required(),
|
|
393
|
+
hours: a.integer(),
|
|
394
|
+
})
|
|
395
|
+
.returns(a.ref("CurationDelegationRequestResult"))
|
|
396
|
+
.authorization((allow) => [
|
|
397
|
+
allow.authenticated("identityPool"),
|
|
398
|
+
])
|
|
399
|
+
.handler(a.handler.function(categoryAction)),
|
|
400
|
+
|
|
401
|
+
approveCurationDelegation: a
|
|
402
|
+
.mutation()
|
|
403
|
+
.arguments({
|
|
404
|
+
userCode: a.string().required(),
|
|
405
|
+
})
|
|
406
|
+
.returns(a.ref("CurationDelegationResult"))
|
|
407
|
+
.authorization((allow) => [
|
|
408
|
+
allow.groups(categoryWriteGroups),
|
|
409
|
+
])
|
|
410
|
+
.handler(a.handler.function(categoryAction)),
|
|
411
|
+
|
|
412
|
+
denyCurationDelegation: a
|
|
413
|
+
.mutation()
|
|
414
|
+
.arguments({
|
|
415
|
+
userCode: a.string().required(),
|
|
416
|
+
})
|
|
417
|
+
.returns(a.ref("CurationDelegationResult"))
|
|
418
|
+
.authorization((allow) => [
|
|
419
|
+
allow.groups(categoryWriteGroups),
|
|
420
|
+
])
|
|
421
|
+
.handler(a.handler.function(categoryAction)),
|
|
422
|
+
|
|
423
|
+
getCurationDelegationStatus: a
|
|
424
|
+
.query()
|
|
425
|
+
.arguments({
|
|
426
|
+
delegationId: a.id().required(),
|
|
427
|
+
})
|
|
428
|
+
.returns(a.ref("CurationDelegationStatusResult"))
|
|
429
|
+
.authorization((allow) => [
|
|
430
|
+
allow.authenticated("identityPool"),
|
|
431
|
+
])
|
|
432
|
+
.handler(a.handler.function(categoryAction)),
|
|
433
|
+
|
|
366
434
|
recordDelegatedCuration: a
|
|
367
435
|
.mutation()
|
|
368
436
|
.arguments({
|
package/amplify/data/schema.js
CHANGED
|
@@ -211,6 +211,25 @@ const baseDefinitions = {
|
|
|
211
211
|
grantorLabel: a.string(),
|
|
212
212
|
expiresAt: a.datetime().required(),
|
|
213
213
|
revokedAt: a.datetime(),
|
|
214
|
+
state: a.string(),
|
|
215
|
+
userCode: a.string(),
|
|
216
|
+
requestedHours: a.integer(),
|
|
217
|
+
pendingExpiresAt: a.datetime(),
|
|
218
|
+
approvedAt: a.datetime(),
|
|
219
|
+
deniedAt: a.datetime(),
|
|
220
|
+
}),
|
|
221
|
+
CurationDelegationRequestResult: a.customType({
|
|
222
|
+
ok: a.boolean().required(),
|
|
223
|
+
delegationId: a.id().required(),
|
|
224
|
+
userCode: a.string().required(),
|
|
225
|
+
pendingExpiresAt: a.datetime().required(),
|
|
226
|
+
requestedHours: a.integer().required(),
|
|
227
|
+
}),
|
|
228
|
+
CurationDelegationStatusResult: a.customType({
|
|
229
|
+
delegationId: a.id().required(),
|
|
230
|
+
state: a.string().required(),
|
|
231
|
+
expiresAt: a.datetime().required(),
|
|
232
|
+
grantorLabel: a.string(),
|
|
214
233
|
}),
|
|
215
234
|
ReferenceCurationActionResult: a.customType({
|
|
216
235
|
ok: a.boolean().required(),
|
|
@@ -291,6 +310,49 @@ const baseDefinitions = {
|
|
|
291
310
|
allow.groups(categoryWriteGroups),
|
|
292
311
|
])
|
|
293
312
|
.handler(a.handler.function(categoryAction)),
|
|
313
|
+
requestCurationDelegation: a
|
|
314
|
+
.mutation()
|
|
315
|
+
.arguments({
|
|
316
|
+
delegationId: a.id().required(),
|
|
317
|
+
tokenHash: a.string().required(),
|
|
318
|
+
agentLabel: a.string().required(),
|
|
319
|
+
hours: a.integer(),
|
|
320
|
+
})
|
|
321
|
+
.returns(a.ref("CurationDelegationRequestResult"))
|
|
322
|
+
.authorization((allow) => [
|
|
323
|
+
allow.authenticated("identityPool"),
|
|
324
|
+
])
|
|
325
|
+
.handler(a.handler.function(categoryAction)),
|
|
326
|
+
approveCurationDelegation: a
|
|
327
|
+
.mutation()
|
|
328
|
+
.arguments({
|
|
329
|
+
userCode: a.string().required(),
|
|
330
|
+
})
|
|
331
|
+
.returns(a.ref("CurationDelegationResult"))
|
|
332
|
+
.authorization((allow) => [
|
|
333
|
+
allow.groups(categoryWriteGroups),
|
|
334
|
+
])
|
|
335
|
+
.handler(a.handler.function(categoryAction)),
|
|
336
|
+
denyCurationDelegation: a
|
|
337
|
+
.mutation()
|
|
338
|
+
.arguments({
|
|
339
|
+
userCode: a.string().required(),
|
|
340
|
+
})
|
|
341
|
+
.returns(a.ref("CurationDelegationResult"))
|
|
342
|
+
.authorization((allow) => [
|
|
343
|
+
allow.groups(categoryWriteGroups),
|
|
344
|
+
])
|
|
345
|
+
.handler(a.handler.function(categoryAction)),
|
|
346
|
+
getCurationDelegationStatus: a
|
|
347
|
+
.query()
|
|
348
|
+
.arguments({
|
|
349
|
+
delegationId: a.id().required(),
|
|
350
|
+
})
|
|
351
|
+
.returns(a.ref("CurationDelegationStatusResult"))
|
|
352
|
+
.authorization((allow) => [
|
|
353
|
+
allow.authenticated("identityPool"),
|
|
354
|
+
])
|
|
355
|
+
.handler(a.handler.function(categoryAction)),
|
|
294
356
|
recordDelegatedCuration: a
|
|
295
357
|
.mutation()
|
|
296
358
|
.arguments({
|
package/amplify/data/schema.ts
CHANGED
|
@@ -230,6 +230,27 @@ const baseDefinitions = {
|
|
|
230
230
|
grantorLabel: a.string(),
|
|
231
231
|
expiresAt: a.datetime().required(),
|
|
232
232
|
revokedAt: a.datetime(),
|
|
233
|
+
state: a.string(),
|
|
234
|
+
userCode: a.string(),
|
|
235
|
+
requestedHours: a.integer(),
|
|
236
|
+
pendingExpiresAt: a.datetime(),
|
|
237
|
+
approvedAt: a.datetime(),
|
|
238
|
+
deniedAt: a.datetime(),
|
|
239
|
+
}),
|
|
240
|
+
|
|
241
|
+
CurationDelegationRequestResult: a.customType({
|
|
242
|
+
ok: a.boolean().required(),
|
|
243
|
+
delegationId: a.id().required(),
|
|
244
|
+
userCode: a.string().required(),
|
|
245
|
+
pendingExpiresAt: a.datetime().required(),
|
|
246
|
+
requestedHours: a.integer().required(),
|
|
247
|
+
}),
|
|
248
|
+
|
|
249
|
+
CurationDelegationStatusResult: a.customType({
|
|
250
|
+
delegationId: a.id().required(),
|
|
251
|
+
state: a.string().required(),
|
|
252
|
+
expiresAt: a.datetime().required(),
|
|
253
|
+
grantorLabel: a.string(),
|
|
233
254
|
}),
|
|
234
255
|
|
|
235
256
|
ReferenceCurationActionResult: a.customType({
|
|
@@ -319,6 +340,53 @@ const baseDefinitions = {
|
|
|
319
340
|
])
|
|
320
341
|
.handler(a.handler.function(categoryAction)),
|
|
321
342
|
|
|
343
|
+
requestCurationDelegation: a
|
|
344
|
+
.mutation()
|
|
345
|
+
.arguments({
|
|
346
|
+
delegationId: a.id().required(),
|
|
347
|
+
tokenHash: a.string().required(),
|
|
348
|
+
agentLabel: a.string().required(),
|
|
349
|
+
hours: a.integer(),
|
|
350
|
+
})
|
|
351
|
+
.returns(a.ref("CurationDelegationRequestResult"))
|
|
352
|
+
.authorization((allow) => [
|
|
353
|
+
allow.authenticated("identityPool"),
|
|
354
|
+
])
|
|
355
|
+
.handler(a.handler.function(categoryAction)),
|
|
356
|
+
|
|
357
|
+
approveCurationDelegation: a
|
|
358
|
+
.mutation()
|
|
359
|
+
.arguments({
|
|
360
|
+
userCode: a.string().required(),
|
|
361
|
+
})
|
|
362
|
+
.returns(a.ref("CurationDelegationResult"))
|
|
363
|
+
.authorization((allow) => [
|
|
364
|
+
allow.groups(categoryWriteGroups),
|
|
365
|
+
])
|
|
366
|
+
.handler(a.handler.function(categoryAction)),
|
|
367
|
+
|
|
368
|
+
denyCurationDelegation: a
|
|
369
|
+
.mutation()
|
|
370
|
+
.arguments({
|
|
371
|
+
userCode: a.string().required(),
|
|
372
|
+
})
|
|
373
|
+
.returns(a.ref("CurationDelegationResult"))
|
|
374
|
+
.authorization((allow) => [
|
|
375
|
+
allow.groups(categoryWriteGroups),
|
|
376
|
+
])
|
|
377
|
+
.handler(a.handler.function(categoryAction)),
|
|
378
|
+
|
|
379
|
+
getCurationDelegationStatus: a
|
|
380
|
+
.query()
|
|
381
|
+
.arguments({
|
|
382
|
+
delegationId: a.id().required(),
|
|
383
|
+
})
|
|
384
|
+
.returns(a.ref("CurationDelegationStatusResult"))
|
|
385
|
+
.authorization((allow) => [
|
|
386
|
+
allow.authenticated("identityPool"),
|
|
387
|
+
])
|
|
388
|
+
.handler(a.handler.function(categoryAction)),
|
|
389
|
+
|
|
322
390
|
recordDelegatedCuration: a
|
|
323
391
|
.mutation()
|
|
324
392
|
.arguments({
|
|
@@ -12,14 +12,28 @@ import {
|
|
|
12
12
|
delegationRefusalMessage,
|
|
13
13
|
delegationVerdict,
|
|
14
14
|
newDelegationToken,
|
|
15
|
+
PENDING_REQUEST_GRANTOR_SUB,
|
|
15
16
|
type CurationDelegation,
|
|
16
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";
|
|
17
27
|
|
|
18
28
|
type ReviewHandler = Schema["reviewSteeringProposal"]["functionHandler"];
|
|
19
29
|
type ReferenceCurationHandler = Schema["reviewReferenceCuration"]["functionHandler"];
|
|
20
30
|
type CreateCurationDelegationHandler = Schema["createCurationDelegation"]["functionHandler"];
|
|
21
31
|
type RevokeCurationDelegationHandler = Schema["revokeCurationDelegation"]["functionHandler"];
|
|
22
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"];
|
|
23
37
|
type RecordDelegatedCurationHandler = Schema["recordDelegatedCuration"]["functionHandler"];
|
|
24
38
|
type ReferenceQualityHandler = Schema["setReferenceQualityRating"]["functionHandler"];
|
|
25
39
|
type ReferenceNoteHandler = Schema["createReferenceNote"]["functionHandler"];
|
|
@@ -39,6 +53,10 @@ type CategoryActionEvent =
|
|
|
39
53
|
| Parameters<CreateCurationDelegationHandler>[0]
|
|
40
54
|
| Parameters<RevokeCurationDelegationHandler>[0]
|
|
41
55
|
| Parameters<ListCurationDelegationsHandler>[0]
|
|
56
|
+
| Parameters<RequestCurationDelegationHandler>[0]
|
|
57
|
+
| Parameters<ApproveCurationDelegationHandler>[0]
|
|
58
|
+
| Parameters<DenyCurationDelegationHandler>[0]
|
|
59
|
+
| Parameters<GetCurationDelegationStatusHandler>[0]
|
|
42
60
|
| Parameters<RecordDelegatedCurationHandler>[0]
|
|
43
61
|
| Parameters<ReferenceQualityHandler>[0]
|
|
44
62
|
| Parameters<ReferenceNoteHandler>[0]
|
|
@@ -108,6 +126,10 @@ export const handler = async (event: CategoryActionEvent) => {
|
|
|
108
126
|
if (fieldName === "createCurationDelegation") return createCurationDelegation(event as Parameters<CreateCurationDelegationHandler>[0]);
|
|
109
127
|
if (fieldName === "revokeCurationDelegation") return revokeCurationDelegation(event as Parameters<RevokeCurationDelegationHandler>[0]);
|
|
110
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]);
|
|
111
133
|
if (fieldName === "recordDelegatedCuration") return recordDelegatedCuration(event as Parameters<RecordDelegatedCurationHandler>[0]);
|
|
112
134
|
if (fieldName === "setReferenceQualityRating") return setReferenceQualityRating(event as Parameters<ReferenceQualityHandler>[0]);
|
|
113
135
|
if (fieldName === "createReferenceNote") return createReferenceNote(event as Parameters<ReferenceNoteHandler>[0]);
|
|
@@ -376,17 +398,68 @@ async function writeCurationDelegation(client: DataClient, delegation: CurationD
|
|
|
376
398
|
}
|
|
377
399
|
|
|
378
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 {
|
|
379
423
|
return {
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
agentLabel: delegation.agentLabel,
|
|
384
|
-
grantorLabel: delegation.grantorLabel,
|
|
385
|
-
expiresAt: delegation.expiresAt,
|
|
386
|
-
revokedAt: delegation.revokedAt,
|
|
424
|
+
read: (delegationId) => readCurationDelegation(client, delegationId),
|
|
425
|
+
write: (delegation, now, created) => writeCurationDelegation(client, delegation, now, created),
|
|
426
|
+
listPending: () => listPendingCurationDelegations(client),
|
|
387
427
|
};
|
|
388
428
|
}
|
|
389
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
|
+
|
|
390
463
|
async function createCurationDelegation(event: Parameters<CreateCurationDelegationHandler>[0]) {
|
|
391
464
|
const person = requireSignedInPerson(event);
|
|
392
465
|
const client = await getDataClient();
|
|
@@ -412,7 +485,7 @@ async function revokeCurationDelegation(event: Parameters<RevokeCurationDelegati
|
|
|
412
485
|
const client = await getDataClient();
|
|
413
486
|
const delegation = await readCurationDelegation(client, normalizeRequiredString(event.arguments.delegationId, "delegationId"));
|
|
414
487
|
if (!delegation) throw new Error("That delegation was not found.");
|
|
415
|
-
if (delegation.grantorSub !== person.sub && !person.isAdmin) throw new Error("Only the person who granted a delegation can revoke it.");
|
|
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.");
|
|
416
489
|
const now = new Date().toISOString();
|
|
417
490
|
const revoked = { ...delegation, revokedAt: delegation.revokedAt ?? now };
|
|
418
491
|
await writeCurationDelegation(client, revoked, now, false);
|
|
@@ -436,6 +509,8 @@ async function listCurationDelegations(event: Parameters<ListCurationDelegations
|
|
|
436
509
|
}
|
|
437
510
|
nextToken = page.nextToken;
|
|
438
511
|
} while (nextToken);
|
|
512
|
+
const now = new Date().toISOString();
|
|
513
|
+
delegations.push(...(await livePendingRequests(curationDelegationStore(client), now)));
|
|
439
514
|
return delegations
|
|
440
515
|
.sort((first, second) => second.createdAt.localeCompare(first.createdAt))
|
|
441
516
|
.map((delegation) => delegationResult(delegation, null));
|
|
@@ -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
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createHash, randomBytes } from "node:crypto"
|
|
1
|
+
import { createHash, randomBytes, randomInt } from "node:crypto"
|
|
2
2
|
|
|
3
3
|
export const CURATION_DELEGATION_PAYLOAD_KIND = "curation-delegation"
|
|
4
4
|
export const CURATION_DELEGATION_OWNER_TYPE = "curationDelegation"
|
|
@@ -14,11 +14,18 @@ export type CurationDelegation = {
|
|
|
14
14
|
createdAt: string
|
|
15
15
|
expiresAt: string
|
|
16
16
|
revokedAt: string | null
|
|
17
|
+
userCode?: string | null
|
|
18
|
+
requestedHours?: number | null
|
|
19
|
+
pendingExpiresAt?: string | null
|
|
20
|
+
approvedAt?: string | null
|
|
21
|
+
deniedAt?: string | null
|
|
17
22
|
}
|
|
18
23
|
|
|
24
|
+
export type DelegationState = "pending" | "approved" | "denied" | "expired" | "revoked"
|
|
25
|
+
|
|
19
26
|
export type DelegationVerdict =
|
|
20
27
|
| { ok: true }
|
|
21
|
-
| { ok: false; reason: "unknown-token" | "wrong-agent" | "revoked" | "expired" }
|
|
28
|
+
| { ok: false; reason: "unknown-token" | "wrong-agent" | "revoked" | "not-approved" | "expired" }
|
|
22
29
|
|
|
23
30
|
export function hashDelegationToken(token: string): string {
|
|
24
31
|
return createHash("sha256").update(token, "utf8").digest("hex")
|
|
@@ -53,6 +60,10 @@ export function delegationVerdict(
|
|
|
53
60
|
return { ok: false, reason: "wrong-agent" }
|
|
54
61
|
}
|
|
55
62
|
if (delegation.revokedAt) return { ok: false, reason: "revoked" }
|
|
63
|
+
const state = delegationState(delegation, presented.now)
|
|
64
|
+
if (state === "pending" || state === "denied" || (state === "expired" && delegation.userCode && !delegation.approvedAt)) {
|
|
65
|
+
return { ok: false, reason: "not-approved" }
|
|
66
|
+
}
|
|
56
67
|
if (Date.parse(delegation.expiresAt) <= Date.parse(presented.now)) return { ok: false, reason: "expired" }
|
|
57
68
|
return { ok: true }
|
|
58
69
|
}
|
|
@@ -62,6 +73,43 @@ export function delegationRefusalMessage(reason: Exclude<DelegationVerdict, { ok
|
|
|
62
73
|
"unknown-token": "That delegation token is not recognized.",
|
|
63
74
|
"wrong-agent": "That delegation was granted to a different agent.",
|
|
64
75
|
"revoked": "That delegation was revoked.",
|
|
76
|
+
"not-approved": "That delegation has not been approved by a person. Ask them to approve the code in the Newsroom.",
|
|
65
77
|
"expired": "That delegation has expired. Ask the person for a new one.",
|
|
66
78
|
}[reason]
|
|
67
79
|
}
|
|
80
|
+
|
|
81
|
+
export const PENDING_DELEGATION_MINUTES = 10
|
|
82
|
+
export const MAXIMUM_PENDING_DELEGATION_REQUESTS = 5
|
|
83
|
+
export const PENDING_REQUEST_GRANTOR_SUB = "pending-device-authorization"
|
|
84
|
+
export const EXPIRED_REQUEST_GRANTOR_SUB = "expired-device-authorization"
|
|
85
|
+
export const USER_CODE_ALPHABET = "ABCDEFGHJKMNPQRSTUVWXYZ23456789"
|
|
86
|
+
export const USER_CODE_LENGTH = 8
|
|
87
|
+
|
|
88
|
+
export function newUserCode(): string {
|
|
89
|
+
let code = ""
|
|
90
|
+
for (let position = 0; position < USER_CODE_LENGTH; position += 1) {
|
|
91
|
+
code += USER_CODE_ALPHABET[randomInt(USER_CODE_ALPHABET.length)]
|
|
92
|
+
}
|
|
93
|
+
return code
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function formatUserCode(code: string): string {
|
|
97
|
+
return `${code.slice(0, 4)}-${code.slice(4)}`
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function normalizeUserCode(typed: string | null | undefined): string | null {
|
|
101
|
+
const stripped = (typed ?? "").toUpperCase().replace(/[\s-]/g, "")
|
|
102
|
+
if (stripped.length !== USER_CODE_LENGTH) return null
|
|
103
|
+
for (const character of stripped) if (!USER_CODE_ALPHABET.includes(character)) return null
|
|
104
|
+
return stripped
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function delegationState(delegation: CurationDelegation, now: string): DelegationState {
|
|
108
|
+
if (delegation.revokedAt) return "revoked"
|
|
109
|
+
if (delegation.deniedAt) return "denied"
|
|
110
|
+
if (delegation.userCode && !delegation.approvedAt) {
|
|
111
|
+
const pendingEnd = delegation.pendingExpiresAt ?? delegation.expiresAt
|
|
112
|
+
return Date.parse(pendingEnd) <= Date.parse(now) ? "expired" : "pending"
|
|
113
|
+
}
|
|
114
|
+
return Date.parse(delegation.expiresAt) <= Date.parse(now) ? "expired" : "approved"
|
|
115
|
+
}
|