-
Notifications
You must be signed in to change notification settings - Fork 23
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: access-api forwards store/ and upload/ invocations to upload-api #334
Changes from 24 commits
0f79d89
70a7576
ab27309
658b8f1
6a633c6
fc665af
f0ac9ad
ddb1351
a692aec
d596bc1
fa6a437
69ee9de
88a1c07
2395049
fcc80ea
9559d0e
373e983
8b1d76b
b9a3ed8
b9c2644
b46dd85
49144ad
51794b0
5eb45fa
3f50270
08981d8
8b03b1b
94614d0
fa1245e
3a46454
394662b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,4 @@ | ||
import * as DID from '@ipld/dag-ucan/did' | ||
import * as ucanto from '@ucanto/core' | ||
import * as Server from '@ucanto/server' | ||
import { Failure } from '@ucanto/server' | ||
import * as Space from '@web3-storage/capabilities/space' | ||
|
@@ -9,13 +9,18 @@ import { | |
} from '@web3-storage/access/encoding' | ||
import { voucherClaimProvider } from './voucher-claim.js' | ||
import { voucherRedeemProvider } from './voucher-redeem.js' | ||
import { UploadApiProxyService } from './upload-api-proxy.js' | ||
|
||
/** | ||
* @param {import('../bindings').RouteContext} ctx | ||
* @returns {import('@web3-storage/access/types').Service} | ||
*/ | ||
export function service(ctx) { | ||
return { | ||
...UploadApiProxyService.create({ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: I think There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i agree and will do as fast follow #338 |
||
fetch: globalThis.fetch, | ||
}), | ||
|
||
voucher: { | ||
claim: voucherClaimProvider(ctx), | ||
redeem: voucherRedeemProvider(ctx), | ||
|
@@ -97,7 +102,7 @@ export function service(ctx) { | |
const inv = await Space.recover | ||
.invoke({ | ||
issuer: ctx.signer, | ||
audience: DID.parse(capability.with), | ||
audience: ucanto.DID.parse(capability.with), | ||
with: ctx.signer.did(), | ||
lifetimeInSeconds: 60 * 10, | ||
nb: { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,188 @@ | ||
import * as Client from '@ucanto/client' | ||
import * as CAR from '@ucanto/transport/car' | ||
import * as CBOR from '@ucanto/transport/cbor' | ||
// eslint-disable-next-line no-unused-vars | ||
import * as dagUcan from '@ipld/dag-ucan' | ||
import { DID } from '@ucanto/core' | ||
import * as HTTP from '@ucanto/transport/http' | ||
// eslint-disable-next-line no-unused-vars | ||
import * as Store from '@web3-storage/capabilities/store' | ||
// eslint-disable-next-line no-unused-vars | ||
import * as Upload from '@web3-storage/capabilities/upload' | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can we please not do these type only imports, dag-ucan can be imported from ucanto interface and caps types can be imported inline from caps types export. Or just create a types.ts file There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i got rid of them and will avoid it in the future too |
||
// eslint-disable-next-line no-unused-vars | ||
import * as Ucanto from '@ucanto/interface' | ||
import { createProxyHandler } from '../ucanto/proxy.js' | ||
|
||
/** | ||
* @template {Ucanto.Capability} C | ||
* @template [Success=unknown] | ||
* @template {{ error: true }} [Failure={error:true}] | ||
* @callback InvocationResponder | ||
* @param {Ucanto.Invocation<C>} invocationIn | ||
* @param {Ucanto.InvocationContext} context | ||
* @returns {Promise<Ucanto.Result<Success, Failure>>} | ||
*/ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should this just be Ucanto There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The only difference is the defaults on Success and Failure. I don't need those so I removed this. Good find. I didn't know all the types we had at our disposal but after this PR I've definitely gotten more familiar with them. |
||
|
||
/** | ||
* @typedef StoreService | ||
* @property {InvocationResponder<Ucanto.InferInvokedCapability<typeof Store.add>>} add | ||
* @property {InvocationResponder<Ucanto.InferInvokedCapability<typeof Store.list>>} list | ||
* @property {InvocationResponder<Ucanto.InferInvokedCapability<typeof Store.remove>>} remove | ||
*/ | ||
|
||
/** | ||
* @typedef UploadService | ||
* @property {InvocationResponder<Ucanto.InferInvokedCapability<typeof Upload.add>>} add | ||
* @property {InvocationResponder<Ucanto.InferInvokedCapability<typeof Upload.list>>} list | ||
* @property {InvocationResponder<Ucanto.InferInvokedCapability<typeof Upload.remove>>} remove | ||
*/ | ||
gobengo marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
/** | ||
* @template {Record<string, any>} T | ||
* @param {object} options | ||
* @param {Ucanto.Signer} [options.signer] | ||
* @param {Pick<Map<dagUcan.DID, Ucanto.ConnectionView<T>>, 'get'>} options.connections | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. what is this type? what is it picking ? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. it was I got rid of this in favor of a record/obj with no functions (which iirc Irakli had also preferred). 08981d8#diff-5fb7be1f94b33171518ab23f095f24d6e1e025282c99e8131c58e9db6c2caa4eL17 |
||
*/ | ||
function createProxyStoreService(options) { | ||
const handleInvocation = createProxyHandler(options) | ||
/** | ||
* @type {StoreService} | ||
*/ | ||
const store = { | ||
add: handleInvocation, | ||
list: handleInvocation, | ||
remove: handleInvocation, | ||
} | ||
return store | ||
} | ||
|
||
/** | ||
* @template {Record<string, any>} T | ||
* @param {object} options | ||
* @param {Ucanto.Signer} [options.signer] | ||
* @param {Pick<Map<dagUcan.DID, Ucanto.ConnectionView<T>>, 'get'>} options.connections | ||
*/ | ||
function createProxyUploadService(options) { | ||
const handleInvocation = createProxyHandler(options) | ||
/** | ||
* @type {UploadService} | ||
*/ | ||
const store = { | ||
add: handleInvocation, | ||
list: handleInvocation, | ||
remove: handleInvocation, | ||
} | ||
return store | ||
} | ||
|
||
/** | ||
* @typedef UcantoHttpConnectionOptions | ||
* @property {dagUcan.DID} audience | ||
* @property {typeof globalThis.fetch} options.fetch | ||
* @property {URL} options.url | ||
*/ | ||
|
||
/** | ||
* @param {UcantoHttpConnectionOptions} options | ||
* @returns {Ucanto.ConnectionView<any>} | ||
*/ | ||
function createUcantoHttpConnection(options) { | ||
return Client.connect({ | ||
id: DID.parse(options.audience), | ||
encoder: CAR, | ||
decoder: CBOR, | ||
channel: HTTP.open({ | ||
fetch: options.fetch, | ||
url: options.url, | ||
}), | ||
}) | ||
} | ||
|
||
/** | ||
* @type {Record<string, { | ||
* audience: dagUcan.DID, | ||
* url: URL, | ||
* }>} | ||
*/ | ||
const uploadApiEnvironments = { | ||
production: { | ||
audience: 'did:web:web3.storage', | ||
url: new URL('https://up.web3.storage'), | ||
}, | ||
staging: { | ||
audience: 'did:web:staging.web3.storage', | ||
url: new URL('https://staging.up.web3.storage'), | ||
}, | ||
} | ||
|
||
/** | ||
* @interface {Pick<Map<dagUcan.DID, Ucanto.Connection<any>>, 'get'>} | ||
*/ | ||
const audienceConnections = { | ||
audienceToUrl: (() => { | ||
/** @type {{ [k: keyof typeof uploadApiEnvironments]: URL }} */ | ||
const object = {} | ||
for (const [, { audience, url }] of Object.entries(uploadApiEnvironments)) { | ||
object[audience] = url | ||
} | ||
return object | ||
})(), | ||
fetch: globalThis.fetch, | ||
/** @type {undefined|Ucanto.DID} */ | ||
defaultAudience: uploadApiEnvironments.production.audience, | ||
/** | ||
* Return a ucanto connection to use for the provided invocation audience. | ||
* If no connection is available for the provided audience, return a connection for the default audience. | ||
* | ||
* @param {dagUcan.DID} audience | ||
*/ | ||
get(audience) { | ||
const defaultedAudience = | ||
audience in this.audienceToUrl ? audience : this.defaultAudience | ||
if (!defaultedAudience) { | ||
return | ||
} | ||
const url = this.audienceToUrl[defaultedAudience] | ||
return createUcantoHttpConnection({ | ||
audience: defaultedAudience, | ||
fetch: this.fetch, | ||
url, | ||
}) | ||
}, | ||
} | ||
|
||
export class UploadApiProxyService { | ||
/** @type {StoreService} */ | ||
store | ||
/** @type {UploadService} */ | ||
upload | ||
|
||
/** | ||
* @param {object} options | ||
* @param {Ucanto.Signer} [options.signer] | ||
* @param {typeof globalThis.fetch} options.fetch | ||
*/ | ||
static create(options) { | ||
const proxyOptions = { | ||
signer: options.signer, | ||
connections: { | ||
...audienceConnections, | ||
...(options.fetch && { fetch: options.fetch }), | ||
}, | ||
} | ||
return new this( | ||
createProxyStoreService(proxyOptions), | ||
createProxyUploadService(proxyOptions) | ||
) | ||
} | ||
|
||
/** | ||
* @protected | ||
* @param {StoreService} store | ||
* @param {UploadService} upload | ||
*/ | ||
constructor(store, upload) { | ||
this.store = store | ||
this.upload = upload | ||
} | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. having an hard time following this cant we just to the following ? all the types should match and its easier to understand /**
* @param {object} options
* @param {typeof globalThis.fetch} options.fetch
*/
function createUploadProxy(options) {
/** @type {Record<Ucanto.DID, Ucanto.ConnectionView<import('@web3-storage/access/types').Service>>} */
const connections = {
'did:web:web3.storage': createUcantoHttpConnection({
audience: 'did:web:web3.storage',
url: new URL('https://up.web3.storage'),
fetch: options.fetch
}),
'did:web:staging.web3.storage': createUcantoHttpConnection({
audience: 'did:web:staging.web3.storage',
url: new URL('https://staging.up.web3.storage'),
fetch: options.fetch
})
}
return {
store: {
add: Server.provide(Store.add, async ({capability, invocation}) => {
const conn = connections[invocation.audience.did()]
return conn.execute(invocation)
})
},
.... // all the other handlers
}
} There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I iterated towards this in two ways:
I don't believe that the way you're doing @Gozala and I discussed a few ways that ucanto could make this easier, but also decided to do something that works with ucanto as is, ship that, and decde after that whether to push some of those tools (that are now in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why does it not work ? this was the promise of "lets make a single endpoint because ucanto makes this super simple" this is not super simple |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
// eslint-disable-next-line no-unused-vars | ||
import * as Ucanto from '@ucanto/interface' | ||
import * as Client from '@ucanto/client' | ||
|
||
/** | ||
* @template {Ucanto.Capability} C | ||
* @template [Success=unknown] | ||
* @template {{ error: true }} [Failure={error:true}] | ||
* @callback InvocationResponder | ||
* @param {Ucanto.Invocation<C>} invocationIn | ||
* @param {Ucanto.InvocationContext} context | ||
* @returns {Promise<Ucanto.Result<Success, Failure>>} | ||
*/ | ||
|
||
/** | ||
* @param {object} options | ||
* @param {Pick<Map<Ucanto.DID, Ucanto.ConnectionView<any>>, 'get'>} options.connections | ||
* @param {Ucanto.Signer} [options.signer] | ||
*/ | ||
export function createProxyHandler(options) { | ||
/** | ||
* @template {import('@ucanto/interface').Capability} Capability | ||
* @param {Ucanto.Invocation<Capability>} invocationIn | ||
* @param {Ucanto.InvocationContext} context | ||
* @returns {Promise<Ucanto.Result<any, { error: true }>>} | ||
*/ | ||
return async function handleInvocation(invocationIn, context) { | ||
const connection = options.connections.get(invocationIn.audience.did()) | ||
if (!connection) { | ||
throw new Error( | ||
`unable to get connection for audience ${invocationIn.audience.did()}}` | ||
) | ||
} | ||
// eslint-disable-next-line unicorn/prefer-logical-operator-over-ternary | ||
const proxyInvocationIssuer = options.signer | ||
? // this results in a forwarded invocation, but the upstream will reject the signature | ||
// created using options.signer unless options.signer signs w/ the same private key as the original issuer | ||
// and it'd be nice to not even have to pass around `options.signer` | ||
options.signer | ||
: // this works, but involves lying about the issuer type (it wants a Signer but context.id is only a Verifier) | ||
// @todo obviate this type override via https://github.com/web3-storage/ucanto/issues/195 | ||
/** @type {Ucanto.Signer} */ (context.id) | ||
|
||
const [result] = await Client.execute( | ||
[ | ||
Client.invoke({ | ||
issuer: proxyInvocationIssuer, | ||
capability: invocationIn.capabilities[0], | ||
audience: invocationIn.audience, | ||
proofs: [invocationIn], | ||
}), | ||
], | ||
/** @type {Client.ConnectionView<any>} */ (connection) | ||
) | ||
return result | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
whats the difference here between these?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is none. Great catch :) ty
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
8b03b1b