-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: persist ucan invocation car to s3 and replicate to r2
- Loading branch information
1 parent
1313dc1
commit f4a9b03
Showing
22 changed files
with
20,719 additions
and
9,850 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3' | ||
|
||
/** | ||
* Abstraction layer with Factory to perform operations on bucket storing | ||
* handled UCANs | ||
* | ||
* @param {string} region | ||
* @param {string} bucketName | ||
* @param {import('@aws-sdk/client-s3').ServiceInputTypes} [options] | ||
* @returns {import('../service/types').UcanBucket} | ||
*/ | ||
export function createUcanStore (region, bucketName, options = {}) { | ||
const s3client = new S3Client({ | ||
region, | ||
...options | ||
}) | ||
|
||
return { | ||
/** | ||
* Put UCAN invocation CAR file into bucket | ||
* | ||
* @param {string} carCid | ||
* @param {Uint8Array} bytes | ||
*/ | ||
put: async (carCid, bytes) => { | ||
const putCmd = new PutObjectCommand({ | ||
Bucket: bucketName, | ||
Key: carCid, | ||
Body: bytes | ||
}) | ||
await s3client.send(putCmd) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -39,4 +39,4 @@ export function createServiceRouter (context) { | |
}) | ||
|
||
return server | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
import { testUcanInvocation as test } from '../helpers/context.js' | ||
|
||
import { HeadObjectCommand } from '@aws-sdk/client-s3' | ||
import * as Signer from '@ucanto/principal/ed25519' | ||
import { CAR } from '@ucanto/transport' | ||
import * as UCAN from '@ipld/dag-ucan' | ||
|
||
import { createSpace } from '../helpers/ucanto.js' | ||
import { createS3, createBucket } from '../helpers/resources.js' | ||
|
||
import { createUcanStore } from '../../buckets/ucan-store.js' | ||
import { parseUcanInvocationRequest, persistUcanInvocation } from '../../ucan-invocation.js' | ||
|
||
test.before(async t => { | ||
const { client: s3Client, clientOpts: s3ClientOpts } = await createS3({ port: 9000 }) | ||
|
||
t.context.s3Client = s3Client | ||
t.context.s3ClientOpts = s3ClientOpts | ||
}) | ||
|
||
test('parses ucan invocation request', async t => { | ||
const uploadService = await Signer.generate() | ||
const alice = await Signer.generate() | ||
const { proof, spaceDid } = await createSpace(alice) | ||
|
||
const data = new Uint8Array([11, 22, 34, 44, 55]) | ||
const link = await CAR.codec.link(data) | ||
const nb = { link, size: data.byteLength } | ||
const can = 'store/add' | ||
|
||
const request = await CAR.encode([ | ||
{ | ||
issuer: alice, | ||
audience: uploadService, | ||
capabilities: [{ | ||
can, | ||
with: spaceDid, | ||
nb | ||
}], | ||
proofs: [proof], | ||
} | ||
]) | ||
|
||
// @ts-expect-error different type interface in AWS expected request | ||
const ucanInvocationObject = await parseUcanInvocationRequest(request) | ||
|
||
const requestCar = await CAR.codec.decode(request.body) | ||
const requestCarRootCid = requestCar.roots[0].cid | ||
|
||
t.is(ucanInvocationObject.carCid, requestCarRootCid.toString()) | ||
t.truthy(ucanInvocationObject.bytes) | ||
|
||
// Decode and validate bytes | ||
const ucanCar = await CAR.codec.decode(ucanInvocationObject.bytes) | ||
// @ts-expect-error UCAN.View<UCAN.Capabilities> inferred as UCAN.View<unknown> | ||
const ucan = UCAN.decode(ucanCar.roots[0].bytes) | ||
|
||
t.is(ucan.iss.did(), alice.did()) | ||
t.is(ucan.aud.did(), uploadService.did()) | ||
t.deepEqual(ucan.prf, [proof.root.cid]) | ||
t.is(ucan.att.length, 1) | ||
t.like(ucan.att[0], { | ||
nb, | ||
can, | ||
with: spaceDid, | ||
}) | ||
}) | ||
|
||
test('persists ucan invocation CAR file', async t => { | ||
const { bucketName } = await prepareResources(t.context.s3Client) | ||
const ucanStore = createUcanStore('us-west-2', bucketName, t.context.s3ClientOpts) | ||
|
||
const uploadService = await Signer.generate() | ||
const alice = await Signer.generate() | ||
const { proof, spaceDid } = await createSpace(alice) | ||
|
||
const data = new Uint8Array([11, 22, 34, 44, 55]) | ||
const link = await CAR.codec.link(data) | ||
const nb = { link, size: data.byteLength } | ||
const can = 'store/add' | ||
|
||
const request = await CAR.encode([ | ||
{ | ||
issuer: alice, | ||
audience: uploadService, | ||
capabilities: [{ | ||
can, | ||
with: spaceDid, | ||
nb | ||
}], | ||
proofs: [proof], | ||
} | ||
]) | ||
|
||
// @ts-expect-error different type interface in AWS expected request | ||
await persistUcanInvocation(request, ucanStore) | ||
|
||
const requestCar = await CAR.codec.decode(request.body) | ||
const requestCarRootCid = requestCar.roots[0].cid | ||
|
||
const cmd = new HeadObjectCommand({ | ||
Key: requestCarRootCid.toString(), | ||
Bucket: bucketName, | ||
}) | ||
const s3Response = await t.context.s3Client.send(cmd) | ||
t.is(s3Response.$metadata.httpStatusCode, 200) | ||
}) | ||
|
||
/** | ||
* @param {import("@aws-sdk/client-s3").S3Client} s3Client | ||
*/ | ||
async function prepareResources (s3Client) { | ||
const bucketName = await createBucket(s3Client) | ||
|
||
return { | ||
bucketName | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
import * as CAR from '@ucanto/transport/car' | ||
|
||
/** | ||
* Persist successful UCAN invocations handled by the router. | ||
* | ||
* @param {import('aws-lambda').APIGatewayProxyEventV2} request | ||
* @param {import('./service/types').UcanBucket} ucanStore | ||
*/ | ||
export async function persistUcanInvocation (request, ucanStore) { | ||
const { carCid, bytes } = await parseUcanInvocationRequest(request) | ||
|
||
await ucanStore.put(carCid, bytes) | ||
} | ||
|
||
/** | ||
* @param {import('aws-lambda').APIGatewayProxyEventV2} request | ||
*/ | ||
export async function parseUcanInvocationRequest (request) { | ||
if (!request.body) { | ||
throw new Error('service requests are required to have body') | ||
} | ||
|
||
const bytes = Buffer.from(request.body, 'base64') | ||
const car = await CAR.codec.decode(bytes) | ||
const carCid = car.roots[0].cid.toString() | ||
|
||
return { | ||
bytes, | ||
carCid | ||
} | ||
} |
Oops, something went wrong.