From c7fbe897b7d9510ef5557971711c33a2792fae47 Mon Sep 17 00:00:00 2001 From: Travis Vachon Date: Wed, 5 Nov 2025 15:33:21 -0800 Subject: [PATCH 1/2] feat: script to grant admin rights to users this allllmost works, but needs a tweak I'm proposing in an RFC --- scripts/grant-admin-rights.mts | 39 ++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 scripts/grant-admin-rights.mts diff --git a/scripts/grant-admin-rights.mts b/scripts/grant-admin-rights.mts new file mode 100644 index 0000000..b14f551 --- /dev/null +++ b/scripts/grant-admin-rights.mts @@ -0,0 +1,39 @@ +import { delegate } from '@ucanto/core' +import { create } from '@storacha/client' +import * as Signer from '@ucanto/principal/ed25519' +import { Absentee } from '@ucanto/principal' +import * as DIDMailto from '@storacha/did-mailto' +import { MemoryDriver } from '@storacha/access/drivers/memory' + +// 90 days, in minutes +const EXPIRY = 60 * 24 * 90 + +// this must be run with the private key of the service passed as SERVICE_PRIVATE_KEY +const servicePrincipal = process.env.SERVICE_PRIVATE_KEY ? Signer.parse(process.env.SERVICE_PRIVATE_KEY).withDID("did:web:up.storacha.network") : undefined +if (!servicePrincipal) throw new Error("Principal not defined, can't continue.") + +const delegateeEmail = process.argv[2] as `${string}@${string}` +const delegateeDidMailto = DIDMailto.fromEmail(delegateeEmail) + +// @ts-expect-error type mismatch on the signer here, but it's fine +const client = await create({ principal: servicePrincipal, store: new MemoryDriver() }) +const delegation = await delegate({ + issuer: servicePrincipal, + audience: Absentee.from({ id: delegateeDidMailto }), + // grant capabilities needed for admin work + capabilities: [ + { with: servicePrincipal.did(), can: 'customer/get' }, + { with: servicePrincipal.did(), can: 'consumer/get' }, + { with: servicePrincipal.did(), can: 'subscription/get' }, + { with: servicePrincipal.did(), can: 'rate-limit/*' }, + { with: servicePrincipal.did(), can: 'admin/*' }, + ], + expiration: Math.floor(Date.now() / 1000) + (60 * EXPIRY) +}) +await client.capability.access.delegate({ + // use the did:key of the service principal as the space to store the delegation in + space: servicePrincipal.toDIDKey(), + delegations: [delegation] +}) + +console.log(client) \ No newline at end of file From 9a733bbfea249a39dbf67519faadc33e3c8b9835 Mon Sep 17 00:00:00 2001 From: Travis Vachon Date: Fri, 5 Dec 2025 16:52:51 -0800 Subject: [PATCH 2/2] wip: more work toward showing usage in the admin app --- app/console/page.tsx | 10 ++++++---- app/customers/[did]/page.tsx | 24 +++++++++++++++++++++++- app/layout.tsx | 2 +- app/page.tsx | 2 +- contexts/service.tsx | 14 +++++++++++++- hooks/customer.ts | 25 ++++++++++++++++++++++++- package.json | 2 +- scripts/grant-admin-rights.mts | 8 ++++---- 8 files changed, 73 insertions(+), 14 deletions(-) diff --git a/app/console/page.tsx b/app/console/page.tsx index f394bf9..b87154d 100644 --- a/app/console/page.tsx +++ b/app/console/page.tsx @@ -26,10 +26,8 @@ async function toDelegation (car: Blob): Promise { export default function Console () { const agent = useAgent() const agentDID = agent?.did() - const [delegations, setDelegations] = useState[]>([]) - if (agent) { - setDelegations(agent.proofs()) - } + const [storedDelegations, setDelegations] = useState[]>([]) + const delegations = storedDelegations ?? agent?.proofs() const [configuredServiceSigner, setConfiguredServiceSigner] = useState() const [expiry, setExpiry] = useState(30) const { servicePrincipal } = useContext(ServiceContext) @@ -48,6 +46,7 @@ export default function Console () { { with: servicePrincipal.did(), can: 'subscription/get' }, { with: servicePrincipal.did(), can: 'rate-limit/*' }, { with: servicePrincipal.did(), can: 'admin/*' }, + { with: 'ucan:*', can: 'account/usage/get' }, ], expiration: Math.floor(Date.now() / 1000) + (60 * expiry) }) @@ -92,7 +91,10 @@ export default function Console () { { with: '${servicePrincipal?.did()}', can: 'consumer/get' }, { with: '${servicePrincipal?.did()}', can: 'subscription/get' }, { with: '${servicePrincipal?.did()}', can: 'rate-limit/*' }, + { with: '${servicePrincipal?.did()}', can: 'admin/*' }, + { with: 'ucan:*', can: 'account/usage/get' }, ]` + return (
{ diff --git a/app/customers/[did]/page.tsx b/app/customers/[did]/page.tsx index 14a12f2..529170a 100644 --- a/app/customers/[did]/page.tsx +++ b/app/customers/[did]/page.tsx @@ -4,7 +4,7 @@ import Link from "next/link"; import { notFound } from "next/navigation"; import * as DidMailto from "@storacha/did-mailto"; -import { useCustomer } from "@/hooks/customer"; +import { useCustomer, useCustomerUsage } from "@/hooks/customer"; import { useRateLimitActions } from "@/hooks/rate-limit"; import { SimpleError } from "@/components/error"; import { Loader } from "@/components/brand"; @@ -33,6 +33,7 @@ export default function Customer(props: { params: Promise<{ did: string }> }) { console.log(did) const { data: customer, error, isLoading } = useCustomer(did); + const {data: usage} = useCustomerUsage(did) const { addBlock: addEmailBlock, removeBlock: removeEmailBlock, @@ -105,6 +106,27 @@ export default function Customer(props: { params: Promise<{ did: string }> }) { ))} +

Usage

+

{usage?.total} bytes

+ + + {Object.entries(usage?.spaces || {}).map(([did, storage]) => ( + + + + + ))} + +
+ {did} + + + {storage.total}bytes + +
)} diff --git a/app/layout.tsx b/app/layout.tsx index b3cff76..ee9121f 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -22,7 +22,7 @@ export default function RootLayout({ - +