Skip to content
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

Conversation

gobengo
Copy link
Contributor

@gobengo gobengo commented Jan 11, 2023

@gobengo gobengo temporarily deployed to dev January 11, 2023 04:52 — with GitHub Actions Inactive
@gobengo gobengo temporarily deployed to dev January 11, 2023 23:25 — with GitHub Actions Inactive
@gobengo gobengo temporarily deployed to dev January 11, 2023 23:35 — with GitHub Actions Inactive
@gobengo gobengo temporarily deployed to dev January 12, 2023 00:59 — with GitHub Actions Inactive
@gobengo gobengo temporarily deployed to dev January 12, 2023 01:09 — with GitHub Actions Inactive
@gobengo gobengo marked this pull request as ready for review January 12, 2023 01:10
// which will allow the access-api ucanto server to accept
// invocations where aud=web3storageDid
DID: web3storageDid,
PRIVATE_KEY: privateKeyFromEnv ?? process.env.PRIVATE_KEY,
Copy link
Contributor Author

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)

@gobengo gobengo temporarily deployed to dev January 12, 2023 01:21 — with GitHub Actions Inactive
Copy link
Contributor

@Gozala Gozala left a 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.

@@ -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'
Copy link
Contributor

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.

Copy link
Contributor Author

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';

Copy link
Contributor Author

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({
Copy link
Contributor

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.

Copy link
Contributor Author

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

packages/access-api/src/service/upload-api-proxy.js Outdated Show resolved Hide resolved
packages/access-api/src/service/upload-api-proxy.js Outdated Show resolved Hide resolved
packages/access-api/src/service/upload-api-proxy.js Outdated Show resolved Hide resolved
/**
* @implements {Pick<Map<dagUcan.DID, iucanto.Connection<any>>, 'get'>}
*/
class AudienceConnections {
Copy link
Contributor

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

Copy link
Contributor Author

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.

Copy link
Contributor

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:

  1. 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.
  2. 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.

Copy link
Contributor Author

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.

packages/access-api/src/service/upload-api-proxy.js Outdated Show resolved Hide resolved
packages/access-api/test/helpers/context.js Show resolved Hide resolved
packages/access-api/test/upload-api-proxy.test.js Outdated Show resolved Hide resolved
packages/access-api/test/store-list.js Outdated Show resolved Hide resolved
@Gozala
Copy link
Contributor

Gozala commented Jan 12, 2023

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.

Created this issue to that effect storacha/w3infra#127

@gobengo gobengo mentioned this pull request Jan 12, 2023
3 tasks
@gobengo
Copy link
Contributor Author

gobengo commented Jan 12, 2023

cc @hugomrdias

@hugomrdias hugomrdias self-requested a review January 12, 2023 17:41
@gobengo gobengo mentioned this pull request Jan 12, 2023
1 task
@gobengo gobengo temporarily deployed to dev January 13, 2023 01:29 — with GitHub Actions Inactive
Comment on lines 135 to 136
* @template {UCAN.DID} ConfigDID
* @template {UCAN.DID} [ID=UCAN.DID]
Copy link
Contributor

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?

Copy link
Contributor Author

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

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

packages/access-api/src/service/upload-api-proxy.js Outdated Show resolved Hide resolved
Comment on lines 4 to 11
// 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'
Copy link
Contributor

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

Copy link
Contributor Author

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

Comment on lines 16 to 24
/**
* @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>>}
*/
Copy link
Contributor

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 ?

Copy link
Contributor Author

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
Copy link
Contributor

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 ?

Copy link
Contributor Author

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

Comment on lines 154 to 188
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
}
}
Copy link
Contributor

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
  }
}

Copy link
Contributor Author

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 no get 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.

Copy link
Contributor

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

@gobengo gobengo temporarily deployed to dev January 13, 2023 21:02 — with GitHub Actions Inactive
@gobengo gobengo temporarily deployed to dev January 13, 2023 22:59 — with GitHub Actions Inactive
@gobengo gobengo temporarily deployed to dev January 13, 2023 23:07 — with GitHub Actions Inactive
@gobengo gobengo temporarily deployed to dev January 13, 2023 23:16 — with GitHub Actions Inactive
@gobengo gobengo temporarily deployed to dev January 13, 2023 23:31 — with GitHub Actions Inactive
@gobengo gobengo temporarily deployed to dev January 14, 2023 01:54 — with GitHub Actions Inactive
@gobengo gobengo merged commit b773376 into main Jan 14, 2023
@gobengo gobengo deleted the 331-make-it-so-access-api-can-proxy-store-and-upload-invocations-to-upload-api branch January 14, 2023 02:03
@hugomrdias
Copy link
Contributor

i was hoping for another round of review

@hugomrdias
Copy link
Contributor

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 (src/service/index.js) we only need:

/**
 * @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>
  }

gobengo pushed a commit that referenced this pull request Jan 17, 2023
🤖 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).
hugomrdias added a commit that referenced this pull request Jan 31, 2023
🤖 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).
hugomrdias added a commit that referenced this pull request Jan 31, 2023
🤖 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).
gobengo pushed a commit that referenced this pull request Apr 11, 2023
🤖 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).
gobengo pushed a commit that referenced this pull request Apr 11, 2023
🤖 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).
gobengo pushed a commit that referenced this pull request Apr 11, 2023
🤖 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).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

make it so access-api can proxy store and upload invocations to upload-api
3 participants