Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
e712e5f
chore: Reorganize env checks
Aug 8, 2024
ec46dc2
fix: Env types in tests
Aug 8, 2024
6a342c4
chore: Clean up code a little bit
Aug 12, 2024
2c1f53a
test: Run the E2E test locally
Aug 14, 2024
1f54769
feat: Add persistent Agent store
Aug 16, 2024
ace6cb2
test: Attempt to test new persistent agent store
Aug 19, 2024
7c42841
fix: Persistent agent test
Aug 20, 2024
e2ee80d
test: Also make sure receipts are stored in the persistent agent store
Aug 20, 2024
d8cb18e
feat: Add persistent delegation store
Aug 20, 2024
34b322f
chore: Wire up persistent delegation store in main worker
Aug 26, 2024
1580486
Merge pull request #1 from fireproof-storage/persist
icidasset Aug 26, 2024
c500944
chore: Add some notes and reorg
Aug 29, 2024
4458f7f
feat: Implement clock/advance
Aug 30, 2024
0309065
feat: Initial sharing delegation chain
Sep 4, 2024
01bc2f5
Merge pull request #2 from fireproof-storage/clock
jchris Sep 4, 2024
ab78585
feat: Allow `ucan:*` to fetch delegations from the store
Sep 9, 2024
08f44dc
feat: Implement clock/register
Sep 10, 2024
c6664d5
feat: Implement clock/head
Sep 10, 2024
cd11bc8
test: Add more clock tests
Sep 10, 2024
4d15abe
feat: Process email invocation + Demo
Sep 11, 2024
441bae8
feat: Authorize and confirm share
icidasset Sep 17, 2024
0d5be4c
feat: Claim share(s)
icidasset Sep 18, 2024
81a6e16
fix: Share flow inconsistencies + e2e test for share flow
icidasset Sep 23, 2024
8270cf5
chore: Demo improvements
icidasset Sep 23, 2024
e027d21
keep vars
jchris Sep 27, 2024
e0d1f8e
fix: DID route path slash should be optional
icidasset Oct 9, 2024
07ac5b0
fix: Reenable R2 signed-url checksum
icidasset Oct 9, 2024
722ca39
fix: Remove R2 signed-url checksum
icidasset Oct 9, 2024
418b0d5
chore: Remove console.log statements
icidasset Oct 9, 2024
4a2b84d
chore: 🧹
icidasset Oct 9, 2024
1cdffcd
feat: Implement store/get
icidasset Oct 9, 2024
3da3a8c
feat: Enable CORS
icidasset Oct 10, 2024
d988556
chore: Improve clock event decoding
icidasset Oct 16, 2024
b24f8f8
feat: Add upload endpoint for usage on localhost
icidasset Oct 17, 2024
4401577
fix: R2 endpoint should only be usable when running on localhost
icidasset Oct 17, 2024
55dd908
feat: Simplify advance data container
icidasset Oct 18, 2024
9e051e7
fix: Add missing R2 pathname
icidasset Oct 22, 2024
2f855f9
chore: small style changes
mabels Oct 24, 2024
3380ed9
chore: enable CI run
mabels Oct 25, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .dev.vars
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// .dev.vars
ACCESS_KEY_ID=x
ACCOUNT_ID=y
BUCKET_NAME=fireproof-preview
FIREPROOF_SERVICE_PRIVATE_KEY="MgCZc476L5pn6Kiw5YdLHEy5CHZgw5gRWxNj/UcLRQoxaHu0BREgGEsI7N8cQxjO6fdgA/lEAphNmR/um1DEfmBTBByY="
POSTMARK_TOKEN="the-token"
SECRET_ACCESS_KEY=sak
SERVICE_ID="did:key:z6Mkv7fM2mH4CkgWbiqmQCwUEJWHsR8EENsgPnm2UtmDeeDJ"
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,13 @@ FIREPROOF_SERVICE_PRIVATE_KEY = <private key>

```

and then https://dash.cloudflare.com/cae1c33213e6c4d3093a01ae8a7e24b0/r2/api-tokens to create API tokens
and then https://dash.cloudflare.com/cae1c33213e6c4d3093a01ae8a7e24b0/r2/api-tokens to create API tokens


### Testing

To test uploading using a pre-signed URL (eg. during connector tests), run the local server in 'remote' mode so that it uses the preview bucket and KV.

```shell
npm run dev-remote
```
291 changes: 291 additions & 0 deletions demo/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,291 @@
import * as Agent from '@web3-storage/access/agent';
import * as CAR from '@ucanto/transport/car';
import * as CBOR from '@ipld/dag-cbor';
import * as DidMailto from '@web3-storage/did-mailto';
import * as HTTP from '@ucanto/transport/http';
import * as Block from 'multiformats/block';
import * as W3 from '@web3-storage/w3up-client';
import { Absentee } from '@ucanto/principal';
import { ParsedArgs } from 'minimist';
import { connect } from '@ucanto/client';
import { DID, parseLink } from '@ucanto/core';
import { sha256 } from 'multiformats/hashes/sha2';
import minimist from 'minimist';

import * as ClockCapabilities from '../src/capabilities/clock';
import * as StoreCapabilities from '../src/capabilities/store';
import { Service } from '../src/index';

import * as Client from '../test/common/client';
import { bytesToDelegations } from '@web3-storage/access/encoding';

// PREP

const HOST = new URL('https://fireproof-ucan.jchris.workers.dev');
const server = DID.parse('did:key:z6MkjasueTeWJzkNAF7PAU8bZa8a3ii6uK979QKf7Fogyq8M');

console.log('Server ID', server.did());

const args: ParsedArgs = minimist(process.argv.slice(2));
const email = args.email;
if (!email) throw new Error('`email` flag is required');

const persona = Absentee.from({ id: DidMailto.fromEmail(email) });
const connection = Agent.connection<`did:key:${string}`, Service>({
principal: server,
url: HOST,
});

const service = connect<any>({
id: server,
codec: CAR.outbound,
channel: HTTP.open({
url: HOST,
method: 'POST',
}),
});

const w3 = await W3.create({
serviceConf: {
access: service,
filecoin: service,
upload: service,
},
});

// CREATE CLOCK

const clock = await Client.createClock({ audience: persona });
const registration = await Client.registerClock({ clock, connection, server });

console.log('⏰ Clock registration', registration.out);
if (registration.out.error) process.exit(1);

// LOGIN / AUTHORISE USING EMAIL
// NOTE: Could opt for the simpler `@web3-storage/access` package instead using the whole w3up package.
console.log('Waiting for main account to login ...');
const account = await w3.login(email);
console.log('👮 Agent', account.agent.did());
console.log(
'🤹 Account proofs',
account.proofs.map((p) => p.capabilities),
);

const attestation = account.proofs.find((p) => p.capabilities[0].can === 'ucan/attest');
const delegation = account.proofs.find((p) => p.capabilities[0].can === '*');

if (!attestation || !delegation) {
console.error('Unable to locate agent attestion or delegation');
process.exit(1);
}

const agent: Client.Agent = {
attestation,
delegation,
signer: account.agent.issuer,
};

// CREATE & STORE CLOCK EVENT

const event = await Client.createClockEvent({
messageCid: parseLink('bagbaierale63ypabqutmxxbz3qg2yzcp2xhz2yairorogfptwdd5n4lsz5xa'),
});

const storeResp = await StoreCapabilities.add
.invoke({
issuer: agent.signer,
audience: server,
with: agent.signer.did(),
nb: {
link: event.cid,
size: event.bytes.length,
},
})
.execute(connection);

console.log('📦 store/add', storeResp.out);
if (storeResp.out.error) process.exit(1);

const storeUrl = storeResp.out.ok.url;

const r2 = await fetch(storeUrl, {
method: 'PUT',
body: event.bytes,
});

console.log('📦 R2 upload, succeeded', r2.ok);

if (!r2.ok) {
console.error(await r2.text());
console.error(`Couldn't store event on R2, status: ${r2.status}`);
process.exit(1);
}

// ADVANCE CLOCK

const advancement = await Client.advanceClock({ agent, clock, connection, event, server });

console.log('⏰ Clock advancement', advancement.out);
if (advancement.out.error) process.exit(1);

// GET CLOCK HEAD

const head = await Client.getClockHead({ agent, clock, connection, server });

console.log('⏰ Clock head', head.out);
if (head.out.error) process.exit(1);

const getResp = await StoreCapabilities.get
.invoke({
issuer: agent.signer,
audience: server,
with: agent.signer.did(),
nb: {
link: event.cid,
},
})
.execute(connection);

if (getResp.out.error) {
console.error(getResp.out.error);
process.exit(1);
}

const block = await Block.decode({
bytes: getResp.out.ok,
codec: CBOR,
hasher: sha256,
});

const metadataCid = (block.value as any).data.metadata;

console.log('👀 Event data:', metadataCid);

// SHARE TO BOB

if (!args['share-email']) process.exit(0);
const sharePersona = Absentee.from({ id: DidMailto.fromEmail(args['share-email']) });

const share = await Client.shareClock({
issuer: persona,
audience: sharePersona,
clock: clock.did(),
genesisClockDelegation: clock.delegation,
});

console.log('🫴 Shared clock to', sharePersona.did());

// BOB CAN VERIFY ALICE USING AN ATTESTATION FROM THE SERVER
// NOTE: See tests for more details about the flow

const w3Bob = await W3.create({
serviceConf: {
access: service,
filecoin: service,
upload: service,
},
});

console.log('Waiting for share-receiver account to login ...');
const accountBob = await w3Bob.login(args['share-email']);

const attestationAlice = attestation;
const delegationAlice = delegation;

const attestationBob = accountBob.proofs.find((p) => p.capabilities[0].can === 'ucan/attest');
const delegationBob = accountBob.proofs.find((p) => p.capabilities[0].can === '*');

if (!attestationBob || !delegationBob) {
console.error('Unable to locate agent attestion or delegation');
process.exit(1);
}

const agentBob = {
attestation: attestationBob,
delegation: delegationBob,
signer: accountBob.agent.issuer,
};

const aliceIsVerifiedOffline = (() => {
// @ts-ignore
const proof = attestationAlice.capabilities[0].nb?.proof;

return (
attestationAlice.issuer.did() === attestationBob.issuer.did() &&
delegationAlice.link().toString() === proof.toString() &&
share.delegation.issuer.did() === delegationAlice.issuer.did()
);
})();

console.log('Offline verification of Alice, verified:', aliceIsVerifiedOffline);

// SHARER CAN USE CLOCK ABILITIES ON THE SERVER

// (1) ALICE HAS TO AUTHORIZE/CONFIRM THE SHARE

const authorizeShareResp = await ClockCapabilities.authorizeShare
.invoke({
issuer: agent.signer,
audience: server,
with: agent.signer.did(),
nb: {
issuer: persona.did(),
recipient: sharePersona.did(),
proof: share.delegation.cid,
},
proofs: [share.delegation],
})
.execute(connection);

console.log('🫴 Share authorization', authorizeShareResp.out);
if (authorizeShareResp.out.error) process.exit(1);

// (2) WAIT UNTIL SHARER CONFIRMS SHARE

const claim = async () => {
const resp = await ClockCapabilities.claimShare
.invoke({
issuer: accountBob.agent.issuer,
audience: server,
with: agentBob.signer.did(),
nb: {
issuer: persona.did(), // Sharer
recipient: sharePersona.did(), // Receiver
proof: share.delegation.cid,
},
proofs: [agentBob.attestation, agentBob.delegation],
})
.execute(connection);

console.log(resp.out);
if (resp.out.error) throw resp.out.error;

return Object.values(resp.out.ok.delegations).flatMap((proof) => bytesToDelegations(proof));
};

const poll = async () => {
const proofs = await claim();

const attestation = proofs.find((p) => p.capabilities[0].can === 'ucan/attest');

if (!attestation) {
await new Promise((resolve) => {
setTimeout(resolve, 2500);
});

return await poll();
}

return attestation;
};

console.log('Waiting for share confirmation ...');
const shareConfirmation = await poll();
console.log('✅ Share confirmed', shareConfirmation);

// (3) USE CLOCK ABILITY

const headBob = await Client.getClockHead({ agent: agentBob, clock, connection, server });

console.log('⏰ Clock head (Bob)', headBob.out);
if (headBob.out.error) process.exit(1);
Loading