-
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
feat: access-api forwards store/ and upload/ invocations to upload-api #334
Conversation
…d-upload-invocations-to-upload-api
// which will allow the access-api ucanto server to accept | ||
// invocations where aud=web3storageDid | ||
DID: web3storageDid, | ||
PRIVATE_KEY: privateKeyFromEnv ?? process.env.PRIVATE_KEY, |
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.
some of this extra stuff is in here because I wanted to support running the tests with the production private key, which works with a non-error result. But if you don't pass that in, the assertions shift slightly to ensuring that it's an error result of the kind that would happen only if the server signer is wrong
Note how this test was run with WEB3_STORAGE_PRIVATE_KEY
:
bengo@bengo ~/protocol.ai/w3protocol/packages/access-api ⚡ DID="$PROD_DID_KEY" WEB3_STORAGE_PRIVATE_KEY="$PROD_PRIVATE_KEY" pnpm test -- -f 'upload-api'
> @web3-storage/[email protected] test /Users/bengo/protocol.ai/w3protocol/packages/access-api
> pnpm build && mocha --bail --timeout 10s -n no-warnings -n experimental-vm-modules "-f" "upload-api"
> @web3-storage/[email protected] build /Users/bengo/protocol.ai/w3protocol/packages/access-api
> scripts/cli.js build
proxy store/list invocations to upload-api
✔ forwards store/list invocations with aud=did:key (648ms)
✔ forwards invocations with aud=did:web:web3.storage (460ms)
✔ errors when a bad delegation is given as proof (320ms)
Store.all
✔ proxies store/add to upload-api (331ms)
✔ proxies store/remove to upload-api (306ms)
✔ proxies store/list to upload-api (385ms)
Upload.all
✔ proxies upload/add to upload-api (303ms)
✔ proxies upload/remove to upload-api (287ms)
✔ proxies upload/list to upload-api (342ms)
9 passing (3s)
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.
Added bunch of Nits, which you can completely ignore or address before or after landing. Only thing I do wish is that we had a test(s) where we spin up local store
and upload
services and route to those in test so that we have passing cases that don't require actual w3up service and all worked in offline mode.
That said it's probably tricky to do it without moving bunch of code from w3infra here so I think it's good enough for now.
packages/access-api/src/config.js
Outdated
@@ -1,5 +1,7 @@ | |||
import { Signer } from '@ucanto/principal/ed25519' | |||
import * as DID from '@ipld/dag-ucan/did' | |||
// eslint-disable-next-line no-unused-vars | |||
import * as dagUcan from '@ipld/dag-ucan' |
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.
Nit: We re-export all of the types from the dag-ucan in the ucanto interface & it would be better to use those instead, so we don't have a chance of having two diff versions of dag-ucan in the tree.
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.
Thanks, I updated to that, and will prefer importing from ucanto libs in the future instead of dag-ucan.
import { UCAN } from '@ucanto/interface';
import { DID } from '@ucanto/core';
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.
|
||
/** | ||
* @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 comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: I think ...uploadProvider(), ...storeProvider()
would be more aligned with other stuff already in use. Also I think it would be a good idea to make fetch
optional so we only need to pass it when overriding default.
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.
i agree and will do as fast follow #338
/** | ||
* @implements {Pick<Map<dagUcan.DID, iucanto.Connection<any>>, 'get'>} | ||
*/ | ||
class AudienceConnections { |
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.
Nit: This whole class seems like incidental complexity, things would be a whole lot simpler with just config
object and static function for get
method.
P.S.: I know that's not a popular opinion, yet I like to share it in a hope to bring peers to a true side :P
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.
Is this like what you had in mind? b46dd85
P.S.: I know that's not a popular opinion, yet I like to share it in a hope to bring peers to a true side :P
Eh, I don't disagree necessarily. good suggestion.
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.
Not exactly, sorry for not been very clear. In linked version you still have audienceConnections
which has get
function. When I criticize classes what I mean is:
Separate data part which is basically a state from the behavior which are all the methods
That way it does not matter how you constructed state, you can just pass it into a function and it will compute whatever. Separating data and behavior de-tangles two allowing you to store data in e.g. indexDB and then apply function after reading it from indexDB without having to built a class first. Not the case here but complex class heirerchies also often entangle initialization order.
I think there are only two valid reasons to resort to a classes with methods:
- When you have to deal with open-ended variant space. In other sometimes you have many different state options but only one of them will be used in practice. In this scenario dispatching from one function introduces unnecessary coordination and defining interface is better option.
- When your methods are simply a convenience sugar for the static functions. I do it all the time to allow method chaining and remove need for importing lot of dependencies. In this case they don't tangle anything they are nothing but a sugar
I would argue use of classes with methods for any other reason just introduces incidental complexity and gets between you and data nothing else.
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.
@Gozala ah I understand what you mean now.
Separating data and behavior de-tangles two allowing you to store data in e.g. indexDB and then apply function after reading it from indexDB without having to built a class first.
def have run into that. That helped me understand.
Created this issue to that effect storacha/w3infra#127 |
cc @hugomrdias |
packages/access-api/src/config.js
Outdated
* @template {UCAN.DID} ConfigDID | ||
* @template {UCAN.DID} [ID=UCAN.DID] |
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.
// 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 comment
The 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 comment
The 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
/** | ||
* @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 comment
The reason will be displayed to describe this comment to others. Learn more.
should this just be Ucanto ServiceMethod
?
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.
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.
fa1245e
* @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 comment
The 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 comment
The reason will be displayed to describe this comment to others. Learn more.
it was { get: (audience: Ucanto.UCAN.DID) => ConnectionView<T> }
I got rid of this in favor of a record/obj with no functions (which iirc Irakli had also preferred). 08981d8#diff-5fb7be1f94b33171518ab23f095f24d6e1e025282c99e8131c58e9db6c2caa4eL17
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 comment
The 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 comment
The reason will be displayed to describe this comment to others. Learn more.
I iterated towards this in two ways:
connections
is now just an object like you illustrate here noget
method- unbundling the class with store + upload into separate functions that build each
I don't believe that the way you're doing conn.execute(invocation)
will actually work. I wish it did. I tried that first. but had to try some various things to avoid making ucanto changes.
@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 access-api/src/ucanto/proxy.js
) down into ucanto itself.
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.
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
…tore/upload service types from the capability defs
…ceMethod with defaults
…yProxy (resolves #339 ]
i was hoping for another round of review |
Upon for testing this should work as expected /**
* @template {API.Ability} A
* @template {API.URI} R
* @template {API.Caveats} C
* @param {API.CapabilityParser<API.Match<API.ParsedCapability<A, R, API.InferCaveats<C>>>>} capability
* @param {import('../bindings').RouteContext} ctx
*/
function createProxyProvider(capability, ctx) {
const provider = Server.provide(
capability,
async ({ capability, invocation }) => {
/** @type {import('@ucanto/interface').ConnectionView<import('@web3-storage/access/types').Service>} */
const connection = connect({
// This should be the same did:web as access api
id: ctx.signer,
encoder: CAR,
decoder: CBOR,
channel: HTTP.open({
// this is the only new global env var
url: new URL('https://staging.up.web3.storage'),
method: 'POST',
}),
})
const inv = ucanto.invoke({
issuer: ctx.signer,
// This should be the same did:web as access api
audience: ctx.signer,
capability,
proofs: [invocation],
expiration: invocation.expiration,
facts: invocation.facts,
nonce: invocation.nonce,
notBefore: invocation.notBefore,
})
const [result] = await connection.execute(
/** @type {API.ServiceInvocation<{can: A, with: R, nb: C}, import('@web3-storage/access/types').Service>} */ (
inv
)
)
return result
}
)
return provider
}
In the service definition ( /**
* @param {import('../bindings').RouteContext} ctx
* @returns {import('@web3-storage/access/types').Service}
*/
export function service(ctx) {
return {
store: {
add: createProxyProvider(Store.add, ctx),
},
.....
}
} and here https://github.com/web3-storage/w3protocol/blob/b77337692d9e4580031c429c429d4055d6f6ebff/packages/access-client/src/types.ts#L67 we need to add store: {
add: ServiceMethod<StoreAdd, StoreAddResponse, Failure>
} |
🤖 I have created a release *beep* *boop* --- ## [4.3.0](access-api-v4.2.0...access-api-v4.3.0) (2023-01-17) ### Features * access-api forwards store/ and upload/ invocations to upload-api ([#334](#334)) ([b773376](b773376)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please).
🤖 I have created a release *beep* *boop* --- ## [2.2.0](capabilities-v2.1.0...capabilities-v2.2.0) (2023-01-30) ### Features * access-api forwards store/ and upload/ invocations to upload-api ([#334](#334)) ([b773376](b773376)) * **capabilities:** implement access/authorize and ./update caps ([#387](#387)) ([4242ce0](4242ce0)), closes [#385](#385) * embedded key resolution ([#312](#312)) ([4da91d5](4da91d5)) * update @ucanto/* to ~4.2.3 ([#405](#405)) ([50c0c80](50c0c80)) * update access-api ucanto proxy to not need a signer ([#390](#390)) ([71cbeb7](71cbeb7)) ### Bug Fixes * fix client cli service did resolve ([#292](#292)) ([6be9608](6be9608)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please).
🤖 I have created a release *beep* *boop* --- ## [9.3.0](access-v9.2.0...access-v9.3.0) (2023-01-30) ### Features * access-api forwards store/ and upload/ invocations to upload-api ([#334](#334)) ([b773376](b773376)) * access-api handling store/info for space not in db returns failure with name ([#391](#391)) ([9610fcf](9610fcf)) * update @ucanto/* to ~4.2.3 ([#405](#405)) ([50c0c80](50c0c80)) * update access-api ucanto proxy to not need a signer ([#390](#390)) ([71cbeb7](71cbeb7)) ### Bug Fixes * remove unecessary awaits ([#352](#352)) ([64da6e5](64da6e5)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please).
🤖 I have created a release *beep* *boop* --- ## [4.3.0](access-api-v4.2.0...access-api-v4.3.0) (2023-01-17) ### Features * access-api forwards store/ and upload/ invocations to upload-api ([#334](#334)) ([6be7217](6be7217)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please).
🤖 I have created a release *beep* *boop* --- ## [2.2.0](capabilities-v2.1.0...capabilities-v2.2.0) (2023-01-30) ### Features * access-api forwards store/ and upload/ invocations to upload-api ([#334](#334)) ([6be7217](6be7217)) * **capabilities:** implement access/authorize and ./update caps ([#387](#387)) ([ebe1032](ebe1032)), closes [#385](#385) * embedded key resolution ([#312](#312)) ([45f367d](45f367d)) * update @ucanto/* to ~4.2.3 ([#405](#405)) ([ec39443](ec39443)) * update access-api ucanto proxy to not need a signer ([#390](#390)) ([163fb74](163fb74)) ### Bug Fixes * fix client cli service did resolve ([#292](#292)) ([45e7ad4](45e7ad4)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please).
🤖 I have created a release *beep* *boop* --- ## [9.3.0](access-v9.2.0...access-v9.3.0) (2023-01-30) ### Features * access-api forwards store/ and upload/ invocations to upload-api ([#334](#334)) ([6be7217](6be7217)) * access-api handling store/info for space not in db returns failure with name ([#391](#391)) ([665dac9](665dac9)) * update @ucanto/* to ~4.2.3 ([#405](#405)) ([ec39443](ec39443)) * update access-api ucanto proxy to not need a signer ([#390](#390)) ([163fb74](163fb74)) ### Bug Fixes * remove unecessary awaits ([#352](#352)) ([2e8c1a1](2e8c1a1)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please).
Motivation: