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

Ucan-core refactor #82

Merged
merged 26 commits into from
Jul 22, 2022
Merged

Ucan-core refactor #82

merged 26 commits into from
Jul 22, 2022

Conversation

dholms
Copy link
Collaborator

@dholms dholms commented Jun 27, 2022

Problem

Several problems are addressed by this refactor:

  • the ucan library required you to pull in several dependencies. This may be undesirable for high-security environments, the specific dependencies may not work in a certain environment (one-webcrypto in react-native for instance), and developers may want to choose a different crypto implementation
  • there was no was to do DID method resolution outside of did:key and the library only understood did:keys that had been hard coded in

Closes #77

Solution

Refactor the library into three pieces

  • @ucans/core: a 1-dependency (I didn't get rid of uint8arrays yet 😛) library that contains the core logic for working with ucans
  • @ucans/plugins: a set of basic default "plugins" for the ucan library: RSA, NIST P-256, and ED25519
  • @ucans/ucans (still workshopping the name on this one): Loads the default plugins into @ucans/core & re-exports core + plugins

Implementation notes

Take a look at core/plugins & each of the plugin files in the plugins package to get an idea of the semantics.
You'll notice that I handle did:key differently from the other methods, because it's such a common use case.

I ended up doing a global state variable for plugins. If you remember from the issue, this wasn't my original plan and I frankly still don't love it. But I was having to thread the plugins through a lot of functions, which mean re-exporting all those functions in the top-level package, and some of those it was difficult, because static methods on classes needed them, etc. I figured it was much more straightforward to do it in this manner. Anyone that wants to load in other dependencies would likely wrap their implementation similarly to our top-level.

I left uint8arrays in core. I'd like to make it 0-dependency, but I didn't want to go through re-creating all the encoding fns just yet. Also the library is pretty lightweight & comes from a trusted source. We can switch it out later if we feel so inclined.

I got rid of the KeyType type. These mapped 1:1 with jwtAlg and I figured that we could simplify by just using jwtAlg.

I also got rid of the BaseKey class, and just have each keypair implementing the interfaces directly. It was causing more headaches than good. I also removed the required publicKey on the Keypair interface. This can lead to some confusion with, for instance, P-256 keys. Is it the compressed or decompressed public key? We don't really need it anywhere in the code, so I removed it to avoid that confusion.

Tests of crypto functions are in the plugins library. I left the rest of the tests in core. Most of the tests don't make a ton of sense without being able to use some crypto functions, so I import @ucans/plugins as a devDependency and load it in for core tests. I don't love this approach, but it was the best I saw for now. Let me know if you have other ideas!

I mostly left the configs & build systems untouched. I know a bunch of trial & error has gone into them & my brain glazes over when I try to mess with build systems 😅. However, this did lead to a good bit of repeat code. There's a tsconfig.json, jest.config.js, and tsconfig.eslint.json in each package root and at the repo root. It would be nice to remove that duplication. Similarly, I just copied over the package.json to each package, with some minor edits. I left the package version where it's at on all of them (0.9.1, though I figure this refactor will bump it to 0.10.0). I'm very amenable to any suggestions/recommendations on any of this stuff.

I briefly considered splitting each cryptosystem out into its own library (instead of all in one plugins library), but decided not to proliferate the packages too much at this point. However, to mimic this separation of concerns, I split up the package by crypto system instead of function type (before it was crypto/, keypairs/, etc; now it is p256/, ed25519/, etc).


Thanks for helping plan this PR! Very excited to get it going. I hope the general direction I took makes sense, but happy for any feedback on it/changes requested.

If it's cool with yall, I'd like to publish an alpha version of core that we can start messing around with internally :)

@dholms dholms marked this pull request as ready for review June 29, 2022 16:10
@dholms dholms requested a review from matheus23 June 29, 2022 16:16
@matheus23
Copy link
Member

Wohooo! Nice.

Some minor bikeshedding before I deeply look into this:
What do you think of naming it @ucans/base-plugins or @ucans/default-plugins?
Or perhaps something including the workd "did"? @ucans/base-did-support? Might be too wordy.
To outsiders I worry the first impression of @ucans/plugins would be that it's some kind of plugin system, not the actual plugins themselves.

@matheus23
Copy link
Member

Oh and - by the way - does [email protected] still make sense? I guess it should be something @bluesky now.

@dholms
Copy link
Collaborator Author

dholms commented Jun 29, 2022

ah yup thats a good point. i like @ucans/default-plugins 👍

ah right. hmm i mostly use my personal email even with work things lol. I'll switch it over to either bluesky or personal 👌

Copy link
Member

@matheus23 matheus23 left a comment

Choose a reason for hiding this comment

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

Thanks so much for working on this, I think it's great we're moving in this direction! Better abstractions!

I added a bunch

Plugin Abstraction

I ended up doing a global state variable for plugins. If you remember from the issue, this wasn't my original plan and I frankly still don't love it. But I was having to thread the plugins through a lot of functions, which mean re-exporting all those functions in the top-level package, and some of those it was difficult, because static methods on classes needed them, etc. I figured it was much more straightforward to do it in this manner. Anyone that wants to load in other dependencies would likely wrap their implementation similarly to our top-level.

I'm not 100% convinced. I know it sucks to thread through an argument for lots of functions.

  • For classes: We could parameterize the whole class instead of each parameter. So you'd export const MkStore = plugins => class Store { ....
  • For functions: I think there's some cases where you can prevent having to "pass through" the plugins parameter to a bunch of functions. You should only have to pass them through a function, if it's exposed. So e.g. in the attenuations.ts file, you could write the delegationChains definition like this: export const mkDelegationChains = plugins => function * delegationChains(...) { async function* capabilitiesFromDelegation(...) { /* can use plugins here */ ... } ... }
  • Generally we can try to reduce the API surface. Maybe it'll get better when we remove the did.ts APIs that I think won't actually work out in the long run? The fewer APIs we have, the less functions to have to thread through our parameters.

I know I'm kind of "why don't you just?"-ing you, let me know if I'm pushing too hard on this and it's not worthwhile, or in case I don't understand how bad that version would look, maybe we can get on a call and discuss more concrete cases in depth. ✌️

Tooling

The monorepo is giving my VSCode a hard time 😅 What setup are you using?

  • Is eslint working for you? For me it fails on the .eslintrc.js, because it links to ./tsconfig.eslint.json file which was moved.
  • VSCode shows me errors on import statements such as import ... from "@ucans/core". Apparently it can't resolve @ucans/core. Does that work for you?

We should totally offload those problems to another PR that is just focused on improving the tooling situation though, as long as all of the individual package.json scripts & publishing work fine 👍


PS: There's no line for me to put this comment on, so I'm putting it here: The packages/plugins/src/rsa/index.ts file is empty, we can probably remove it.

packages/core/src/types.ts Outdated Show resolved Hide resolved
packages/core/src/types.ts Show resolved Hide resolved
packages/core/src/plugins.ts Outdated Show resolved Hide resolved
packages/core/src/did.ts Outdated Show resolved Hide resolved
packages/core/src/did.ts Outdated Show resolved Hide resolved
packages/core/src/plugins.ts Outdated Show resolved Hide resolved
@dholms
Copy link
Collaborator Author

dholms commented Jul 5, 2022

I'll take another look at the plugins abstraction. I'd also like for it not to be a global var. Reducing the API surface will certainly help. Big fan of eliminating the DID utility fns.

I think I can make threading the plugins through work. If it really looks like it's not shaking out, I'll give a shout & we can reassess.

The monorepo is giving my VSCode a hard time 😅 What setup are you using?
Is eslint working for you? For me it fails on the .eslintrc.js, because it links to ./tsconfig.eslint.json file which was moved.

Oops! That's on me. I had the same issue, but readded tsconfig.eslint.json at the root & it fixed it but forgot to push it!

VSCode shows me errors on import statements such as import ... from "@ucans/core". Apparently it can't resolve @ucans/core. Does that work for you?

This does work for me. It's a little bit fickle. Make sure you run yarn from the monorepo root to link the packages together. And then make sure you've done yarn build in @ucans/core. If you still have the red underline, then use the command prompt (cmd + shift + p) & type in "Restart TS Server". That command has saved my ass a lot in monorepos.

Thanks for the feedback! Glad this roughly feels like the right direction. I'll get this patched up in the next day or two 👍

@matheus23
Copy link
Member

Make sure you run yarn from the monorepo root to link the packages together. And then make sure you've done yarn build in @ucans/core. If you still have the red underline, then use the command prompt (cmd + shift + p) & type in "Restart TS Server". That command has saved my ass a lot in monorepos.

Ah! That was exactly it. That, and running yarn build in the plugins package. Perfect, thanks!


beforeAll(loadTestPlugins)

it("handles old RSA Keys", async () => {
Copy link
Member

Choose a reason for hiding this comment

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

I see you've added a couple of tests in the plugins package, and they make most of this file redundant.
But at fission we still need this test for backwards compatibility 😅
That should be kept in the rsa.test.ts 🙏

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Does this cover it alright?

ce32e24

@dholms
Copy link
Collaborator Author

dholms commented Jul 5, 2022

Alrighty threaded plugins through functions calls:

61ed38e
(+ a minor change here): b60d09c

This ended up being cleaner than I expected. A couple notes on implementation. Please lmk if you have other ideas on any of this 🙏

I ended up breaking the static methods out of Builder & Store and just making them functions. The types were getting very wonky when I was trying to parameterize the whole class, this made it pretty straightforward.

You can see the surface area of functions that take the injected plugins in getPluginInjectedApi in core/index.ts.

I decided not to have different names for the parameterized fns. such as mkSign => sign. Instead I just call it sign and overwrite it in the exports. I'm not 100% confident in this choice, but I did it so that our exports don't get clogged up with a bunch of pretty low level functions. It also keeps the internal code more readable imo. I'm happy to change this if you have other thoughts.

In the tests, I mimic the high-level ucans lib in tests/lib.ts by injected plugins and re-exporting it. Most of the tests run off of this lib instead of importing from src/.

@dholms
Copy link
Collaborator Author

dholms commented Jul 5, 2022

Started on simplifying tooling by moving jest config to the root: 62d07c0

I tried to do the same with tsconfig, but I'm running into hiccups because of the build system copying it over in dist & adding an extra level to the folder hierarchy 🤔

Copy link
Member

@matheus23 matheus23 left a comment

Choose a reason for hiding this comment

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

Nice!

In the tests, I mimic the high-level ucans lib in tests/lib.ts by injected plugins and re-exporting it. Most of the tests run off of this lib instead of importing from src/.

I think we should just move all tests that use DID stuff from packages/core into packages/ucans (which may be all tests).

I decided not to have different names for the parameterized fns. such as mkSign => sign. Instead I just call it sign and overwrite it in the exports. I'm not 100% confident in this choice, but I did it so that our exports don't get clogged up with a bunch of pretty low level functions. It also keeps the internal code more readable imo. I'm happy to change this if you have other thoughts.

No the code looks good!

This ended up being cleaner than I expected. A couple notes on implementation. Please lmk if you have other ideas on any of this 🙏

I ended up breaking the static methods out of Builder & Store and just making them functions. The types were getting very wonky when I was trying to parameterize the whole class, this made it pretty straightforward.

Hm. I'm not sure what you mean by the types getting wonky if you parameterize the whole class 🤔
I added some suggested changes for what I was thinking :)

@@ -59,26 +75,16 @@ function isCapabilityLookupCapableState(obj: unknown): obj is CapabilityLookupCa
*/
export class Builder<State extends Partial<BuildableState>> {
Copy link
Member

Choose a reason for hiding this comment

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

Why not

Suggested change
export class Builder<State extends Partial<BuildableState>> {
export const MkBuilder = (plugins: Plugins) => class Builder<State extends Partial<BuildableState>> {

?

And then there's no need for a private plugins: Plugins member.

Copy link
Member

Choose a reason for hiding this comment

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

The Mk- based naming here I'm not too sure nor am I attached to it. It may be simpler to just have it be builder or Builder or similar.

(knownSemantics: DelegationSemantics) => {
return new Store(plugins, knownSemantics, {})
}

export class Store {
Copy link
Member

Choose a reason for hiding this comment

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

Same thing here?

Suggested change
export class Store {
export const MkStore = (plugins: Plugins) => class Store {

@dholms
Copy link
Collaborator Author

dholms commented Jul 8, 2022

Sorry had a ton to do this week & just finally getting back to this. I'll get some fixes pushed up on monday :)

I think we should just move all tests that use DID stuff from packages/core into packages/ucans (which may be all tests).

ok yup this makes sense to me. I was feeling that same tension. (There's also a pseudo-circular dependency in here since @ucans/default-plugins is a dev dependency of @ucans/core that we could then eliminate).
The downside is the tests are further away from the code they're testing. So before running any tests, you have to build @ucans/core each time.
Although speaking of: I just did a big ol tooling/monorepo revamp on our adx repo & learned some nice tsconfig tricks that can close that gap a bit. I'll try and bring them over.

Hm. I'm not sure what you mean by the types getting wonky if you parameterize the whole class 🤔

right so the issue I was running into here was that other files in the package use, for instance, the Store class. We wouldn't be exporting it anywhere until after the plugins are injected. Thinking through it again, we should be able to manage this with interfaces (which is probably a good rule of thumb anyway) 🤔

I'll poke it a bit more & see what I can't come up with

@matheus23
Copy link
Member

right so the issue I was running into here was that other files in the package use, for instance, the Store class. We wouldn't be exporting it anywhere until after the plugins are injected. Thinking through it again, we should be able to manage this with interfaces (which is probably a good rule of thumb anyway) 🤔

riiiight. Yes. Interfaces is the way to go I think 👍

&& util.hasProp(obj, "expiration") && typeof obj.expiration === "number"
}

type BuilderConstructor = new <State extends Partial<BuildableState>>(state: State, defaultable: DefaultableState) => BuilderI<State>
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Just a couple notes on this:
Looks like the function has to be a default export for some reason:
microsoft/TypeScript#30355

& this type annotation needs to be included to suggest to the compiler how to write the declaration file

As well, I had to export some previously internal types in ./types so the compiler was able to write the declaration file.

I was running into this error: microsoft/TypeScript#5711

@dholms
Copy link
Collaborator Author

dholms commented Jul 11, 2022

alrighty!

moved most of the tests to @ucans/ucans. I left behind the ones that don't rely on plugins: semver & capability.
wrapped Builder & Store in a mk function. Had to do a little bit of typescript massaging to get it to work, detailed that in the above comment.

I briefly tried to do some tsconfig monorepo tricks like config inheritance & a composite config with references to other packages. But the current build setup was preventing me from doing that. Namely, copying the tsconfig into the dist folder which would add an extra director to the file hierarchy & screw up relative paths. I think this should be doable, but we may need to chat through the current build process. & maybe belongs in a future PR?

@matheus23
Copy link
Member

matheus23 commented Jul 15, 2022

I think this should be doable, but we may need to chat through the current build process. & maybe belongs in a future PR?

Sounds like a plan. Created an issue for this: #84


Do you know why CI is failing? It says

warning Lockfile has incorrect entry for "@ucans/core@*". Ignoring it.
error Couldn't find any versions for "@ucans/core" that matches "*"

Is it just because we've never published @ucans/core? It should just pick up that package locally, right? 🤔

@dholms
Copy link
Collaborator Author

dholms commented Jul 21, 2022

It didn't like my alpha version in one of the core package.json.

I also added in some workspace commands for building & testing all three packages from the workspace root.

Seems to all be passing now 👌

Copy link
Member

@matheus23 matheus23 left a comment

Choose a reason for hiding this comment

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

GIF: Clapping; that's what I'm talking about

Yeeeees! 🎉

I'm super happy with this PR. Thanks! @dholms 🙏

Let's ship it :)

@dholms dholms merged commit 6fad150 into main Jul 22, 2022
@dholms dholms deleted the refactor branch July 22, 2022 16:34
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.

Remove dependencies
2 participants