-
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.
To support users in languages that do not have existing UCAN invocation implementations, we are going to launch a bridge that allows them to make simple HTTP requests with JSON bodies that we transform into proper UCAN invocations. So far this PR has: 1) an untested (but type-checking!) implementation of such a bridge 2) a markdown description of the bridge protocol, intended to be the first draft of an eventual specification Notable design choices: 1) I chose to include JUST the base64pad-encoded "secret" in the Authorization header to avoid running afoul of maximum header size restrictions that exist in [some HTTP environments](https://stackoverflow.com/questions/686217/maximum-on-http-header-values). 2) The `proof` field of the JSON body is a base64pad encoded "delegation archive" (created with `ucanto`'s `Delegation.archive` function) TODO - [ ] factor core bridge logic out to a separate library - [ ] factor HTTP input wrangling out to a separate function - [ ] rename `UPLOAD_API_DID` and `ACCESS_SERVICE_URL` environment variables to `W3UP_SERVICE_DID` and `W3UP_SERVICE_URL` - [ ] add tests - [ ] expand and formalize bridge specification, move it to the specs repo (?)
- Loading branch information
Showing
4 changed files
with
201 additions
and
1 deletion.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
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,48 @@ | ||
# HTTP-UCAN Bridge | ||
|
||
## Summary | ||
|
||
We have implemented a "bridge" that allows w3up users to interact with the service | ||
without implementing the UCAN invocation wire protocols. | ||
|
||
A user can submit an HTTP request like (simplified for clarity): | ||
|
||
``` | ||
POST /bridge | ||
Authorization: MYmI4NWUwMGFhNzNlZDlkM2Y2NDYxYWEwZjk1NDdjOWY= | ||
{ | ||
"ability": "store/add", | ||
"resource": "did:key:z6Mkm5qHN9g9NQSGbBfL7iGp9sexdssioT4CzyVap9ATqGqX", | ||
"inputs": { | ||
"link": "bafybeicxsrpxilwb6bdtq6iztjziosrqts5qq2kgali3xuwgwjjjpx5j24", | ||
"size": 42 | ||
}, | ||
"proof": "MOqJlcm9vdHOB2CpYJQABcRIg8oX3pzNhQ6omQIViTTLdoga/hH4EFdTlQDRJKzd5LQFndmVyc2lvbgGaAgFxEiCa4m5KeAneTomW0WJzcF3a6Wst3m8oLY4/Q4VZRoOsJqdhc1hE7aEDQNCsmjfCsOrj/m0iVZDjxZxwj66cf9hN5yxTkC/t/4MKqR7hsRQzDXep4O0Js9p3cgSlhAOdkbMarQx+qk0i8QNhdmUwLjkuMWNhdHSBomNjYW5hKmR3aXRoeDhkaWQ6a2V5Ono2TWttNXFITjlnOU5RU0diQmZMN2lHcDlzZXhkc3Npb1Q0Q3p5VmFwOUFUcUdxWGNhdWRYIu0Bu8P7ClUb33SVOyLOPQxUZ5Xe5crrHlKRvQO8uOgxtCpjZXhw9mNpc3NYIu0BYoScyDzOAQt1rUCkSiErSfsJrVf1kwu74l1i3GgbnlpjcHJmgMMDAXESIBulnNb9LPB9Br8qPQbpPi7OENfFXODPt9+62t/tNC9Xp2FzWETtoQNAsmFzSClooNmdbQazHFOZ7zus4WGhxq0G5QSk+RZdGo4NVUg8fuwaR4nxlnYr128AtgrE9c6Onzf+Yu10anbxA2F2ZTAuOS4xY2F0dIKiY2NhbmlzdG9yZS9hZGRkd2l0aHg4ZGlkOmtleTp6Nk1rbTVxSE45ZzlOUVNHYkJmTDdpR3A5c2V4ZHNzaW9UNEN6eVZhcDlBVHFHcViiY2Nhbmp1cGxvYWQvYWRkZHdpdGh4OGRpZDprZXk6ejZNa201cUhOOWc5TlFTR2JCZkw3aUdwOXNleGRzc2lvVDRDenlWYXA5QVRxR3FYY2F1ZFgi7QEYGA5FY50aOrnMapmJCO0DvHovsz4HtRZ9bd7PKJarL2NleHABY2lzc1gi7QG7w/sKVRvfdJU7Is49DFRnld7lyuseUpG9A7y46DG0KmNwcmaC2CpYJQABcRIgmuJuSngJ3k6JltFic3Bd2ulrLd5vKC2OP0OFWUaDrCbYKlglAAFxEiCa4m5KeAneTomW0WJzcF3a6Wst3m8oLY4/Q4VZRoOsJlkBcRIg8oX3pzNhQ6omQIViTTLdoga/hH4EFdTlQDRJKzd5LQGhanVjYW5AMC45LjHYKlglAAFxEiAbpZzW/SzwfQa/Kj0G6T4uzhDXxVzgz7ffutrf7TQvVw==" | ||
} | ||
``` | ||
|
||
And receive a JSON-encoded UCAN receipt in response. | ||
|
||
### Authorization | ||
|
||
The `Authorization` header and `proof` field's values can be generated with the `bridge generate-tokens` command of `w3cli`: | ||
|
||
```sh | ||
$ w3 bridge generate-tokens did:key:z6Mkm5qHN9g9NQSGbBfL7iGp9sexdssioT4CzyVap9ATqGqX --expiration 1707264563641 | ||
|
||
Authorization header: MYmI4NWUwMGFhNzNlZDlkM2Y2NDYxYWEwZjk1NDdjOWY= | ||
|
||
Proof: MOqJlcm9vdHOB2CpYJQABcRIg8oX3pzNhQ6omQIViTTLdoga/hH4EFdTlQDRJKzd5LQFndmVyc2lvbgGaAgFxEiCa4m5KeAneTomW0WJzcF3a6Wst3m8oLY4/Q4VZRoOsJqdhc1hE7aEDQNCsmjfCsOrj/m0iVZDjxZxwj66cf9hN5yxTkC/t/4MKqR7hsRQzDXep4O0Js9p3cgSlhAOdkbMarQx+qk0i8QNhdmUwLjkuMWNhdHSBomNjYW5hKmR3aXRoeDhkaWQ6a2V5Ono2TWttNXFITjlnOU5RU0diQmZMN2lHcDlzZXhkc3Npb1Q0Q3p5VmFwOUFUcUdxWGNhdWRYIu0Bu8P7ClUb33SVOyLOPQxUZ5Xe5crrHlKRvQO8uOgxtCpjZXhw9mNpc3NYIu0BYoScyDzOAQt1rUCkSiErSfsJrVf1kwu74l1i3GgbnlpjcHJmgMMDAXESIBulnNb9LPB9Br8qPQbpPi7OENfFXODPt9+62t/tNC9Xp2FzWETtoQNAsmFzSClooNmdbQazHFOZ7zus4WGhxq0G5QSk+RZdGo4NVUg8fuwaR4nxlnYr128AtgrE9c6Onzf+Yu10anbxA2F2ZTAuOS4xY2F0dIKiY2NhbmlzdG9yZS9hZGRkd2l0aHg4ZGlkOmtleTp6Nk1rbTVxSE45ZzlOUVNHYkJmTDdpR3A5c2V4ZHNzaW9UNEN6eVZhcDlBVHFHcViiY2Nhbmp1cGxvYWQvYWRkZHdpdGh4OGRpZDprZXk6ejZNa201cUhOOWc5TlFTR2JCZkw3aUdwOXNleGRzc2lvVDRDenlWYXA5QVRxR3FYY2F1ZFgi7QEYGA5FY50aOrnMapmJCO0DvHovsz4HtRZ9bd7PKJarL2NleHABY2lzc1gi7QG7w/sKVRvfdJU7Is49DFRnld7lyuseUpG9A7y46DG0KmNwcmaC2CpYJQABcRIgmuJuSngJ3k6JltFic3Bd2ulrLd5vKC2OP0OFWUaDrCbYKlglAAFxEiCa4m5KeAneTomW0WJzcF3a6Wst3m8oLY4/Q4VZRoOsJlkBcRIg8oX3pzNhQ6omQIViTTLdoga/hH4EFdTlQDRJKzd5LQGhanVjYW5AMC45LjHYKlglAAFxEiAbpZzW/SzwfQa/Kj0G6T4uzhDXxVzgz7ffutrf7TQvVw== | ||
``` | ||
|
||
|
||
### Invocation Fields | ||
|
||
`ability`, `resource` and `inputs` should be specified according to the capability you wish to invoke. | ||
|
||
`ability` should be a string like `store/add` or `upload/add` and must be included in the set of abilities passed to the `--can` option of `w3 bridge generate-tokens`. By default, `--can` is set to `['upload/add', 'store/add']`. | ||
|
||
Information about possible `inputs` for a particular ability can be found in https://github.com/web3-storage/specs/ | ||
|
||
`resource` MUST match the resource passed as the first option to `w3 bridge generate-tokens`. |
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,150 @@ | ||
import * as Sentry from '@sentry/serverless' | ||
import { invoke, DID } from '@ucanto/core' | ||
import { connect } from '@ucanto/client' | ||
import * as CAR from '@ucanto/transport/car' | ||
import * as HTTP from '@ucanto/transport/http' | ||
import { sha256 } from '@ucanto/core' | ||
import * as Delegation from '@ucanto/core/delegation' | ||
import { ed25519 } from '@ucanto/principal' | ||
import { base64pad } from 'multiformats/bases/base64' | ||
|
||
Sentry.AWSLambda.init({ | ||
environment: process.env.SST_STAGE, | ||
dsn: process.env.SENTRY_DSN, | ||
tracesSampleRate: 1.0, | ||
}) | ||
|
||
/** | ||
* | ||
* @param {Uint8Array} secret | ||
* @returns | ||
*/ | ||
async function deriveSigner(secret) { | ||
const { digest } = await sha256.digest(secret) | ||
return await ed25519.Signer.derive(digest) | ||
} | ||
|
||
/** | ||
* AWS HTTP Gateway handler for POST / with ucan invocation router. | ||
* | ||
* We provide responses in Payload format v2.0 | ||
* see: https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html#http-api-develop-integrations-lambda.proxy-format | ||
* | ||
* @param {import('aws-lambda').APIGatewayProxyEventV2} request | ||
*/ | ||
async function handlerFn(request) { | ||
try { | ||
const { UPLOAD_API_DID, ACCESS_SERVICE_URL } = process.env | ||
|
||
if (!UPLOAD_API_DID) { | ||
return { | ||
statusCode: 500, | ||
body: 'UPLOAD_API_DID is not set' | ||
} | ||
} | ||
|
||
if (!ACCESS_SERVICE_URL) { | ||
return { | ||
statusCode: 500, | ||
body: 'ACCESS_SERVICE_URL is not set' | ||
} | ||
} | ||
|
||
const authorizationHeader = request.headers['authorization'] | ||
if (!authorizationHeader) { | ||
return { | ||
statusCode: 401, | ||
body: 'request has no authorization header' | ||
} | ||
} | ||
const secret = base64pad.decode(authorizationHeader) | ||
|
||
const body = request.body | ||
|
||
if (!body) { | ||
return { | ||
statusCode: 400, | ||
body: 'request has no body' | ||
} | ||
} | ||
|
||
const jsonBody = JSON.parse(body) | ||
|
||
if ( | ||
(typeof jsonBody.ability !== 'string') || | ||
(jsonBody.ability.split('/').length < 2) | ||
) { | ||
return { | ||
statusCode: 400, | ||
body: 'ability must be /-separated string like "store/add" or "admin/store/info"' | ||
} | ||
} | ||
const ability = /** @type {import('@ucanto/interface').Ability} */(jsonBody.ability) | ||
|
||
if ( | ||
(typeof jsonBody.resource !== 'string') || | ||
(jsonBody.resource.split(':').length < 2) | ||
) { | ||
return { | ||
statusCode: 400, | ||
body: 'resource must be a URI' | ||
} | ||
} | ||
const resource = /** @type {import('@ucanto/interface').Resource} */(jsonBody.resource) | ||
|
||
if (typeof jsonBody.proof !== 'string') { | ||
return { | ||
statusCode: 400, | ||
body: 'proof must be a base64pad multibase encoding of a Delegation archive' | ||
} | ||
} | ||
const delegationResult = await Delegation.extract(base64pad.decode(jsonBody.proof)) | ||
if (delegationResult.error) { | ||
return { | ||
statusCode: 400, | ||
body: 'error' | ||
} | ||
} | ||
const delegation = delegationResult.ok | ||
|
||
const invocation = invoke({ | ||
issuer: await deriveSigner(secret), | ||
audience: DID.parse(UPLOAD_API_DID), | ||
capability: { | ||
can: ability, | ||
with: resource, | ||
nb: jsonBody.inputs | ||
}, | ||
proofs: [delegation] | ||
}) | ||
const receipt = await invocation.execute(connect({ | ||
id: DID.parse(UPLOAD_API_DID), | ||
codec: CAR.outbound, | ||
channel: HTTP.open({ | ||
url: new URL(ACCESS_SERVICE_URL), | ||
method: 'POST' | ||
}) | ||
})) | ||
const result = receipt.out | ||
if (result.ok) { | ||
return { | ||
statusCode: 200, | ||
body: result.ok | ||
} | ||
} else { | ||
return { | ||
statusCode: 500, | ||
body: Buffer.from(result.error?.message ?? 'no error message received').toString('base64'), | ||
isBase64Encoded: true | ||
} | ||
} | ||
} catch (/** @type {any} */ error) { | ||
return { | ||
statusCode: error.status ?? 500, | ||
body: Buffer.from(error.message).toString('base64'), | ||
isBase64Encoded: true, | ||
} | ||
} | ||
} | ||
|
||
export const handler = Sentry.AWSLambda.wrapHandler(handlerFn) |