diff --git a/cypress/e2e/spec.cy.ts b/cypress/e2e/spec.cy.ts index c04241f5a..774b8130a 100644 --- a/cypress/e2e/spec.cy.ts +++ b/cypress/e2e/spec.cy.ts @@ -1,5 +1,7 @@ // @ts-nocheck +import { SessionCapabilityObject } from '@lit-protocol/auth'; + let window: any; let savedParams: any = { accs: [ @@ -722,12 +724,13 @@ describe('Session', () => { ); let resources = [`litSigningCondition://${hashedResourceId}`]; - - let capabilities = savedParams.litNodeClient.getSessionCapabilities( - [], - resources + let capabilityObject = savedParams.litNodeClient.getSessionCapabilityObject( + resources, + new SessionCapabilityObject() + ); + expect(capabilityObject.getCapableActionsForAllResources()[0]).to.be.eq( + 'litSigningCondition' ); - expect(capabilities[0]).to.be.eq('litSigningConditionCapability://*'); }); it('gets expiration', () => { @@ -806,4 +809,4 @@ describe('Session', () => { // await cy.get('#metamask').click(); // // expect(sessionSigs).to.be.eq(1); // }); -}); \ No newline at end of file +}); diff --git a/package.json b/package.json index b6d835e49..a846dee7f 100644 --- a/package.json +++ b/package.json @@ -73,6 +73,7 @@ "g": "^2.0.1", "ipfs-unixfs-importer": "^12.0.0", "jszip": "^3.10.1", + "js-base64": "^3.7.2", "lit-connect-modal": "0.1.8", "lit-siwe": "^1.1.8", "multiformats": "^10.0.2", diff --git a/packages/access-control-conditions/package.json b/packages/access-control-conditions/package.json index 147f33484..3155a3da3 100644 --- a/packages/access-control-conditions/package.json +++ b/packages/access-control-conditions/package.json @@ -12,4 +12,4 @@ ], "main": "./dist/src/index.js", "typings": "./dist/src/index.d.ts" -} +} \ No newline at end of file diff --git a/packages/auth-browser/package.json b/packages/auth-browser/package.json index 95dc231b1..51442a765 100644 --- a/packages/auth-browser/package.json +++ b/packages/auth-browser/package.json @@ -20,4 +20,4 @@ ], "main": "./dist/src/index.js", "typings": "./dist/src/index.d.ts" -} +} \ No newline at end of file diff --git a/packages/auth-browser/src/index.ts b/packages/auth-browser/src/index.ts index 0def19326..095c85874 100644 --- a/packages/auth-browser/src/index.ts +++ b/packages/auth-browser/src/index.ts @@ -4,6 +4,3 @@ export * from './lib/auth-browser'; export * as ethConnect from './lib/chains/eth'; export * as cosmosConnect from './lib/chains/cosmos'; export * as solConnect from './lib/chains/sol'; - -// -- session management -export * from './lib/session'; diff --git a/packages/auth-browser/src/lib/session.ts b/packages/auth-browser/src/lib/session.ts deleted file mode 100644 index 99f445584..000000000 --- a/packages/auth-browser/src/lib/session.ts +++ /dev/null @@ -1,248 +0,0 @@ -import { - EITHER_TYPE, - IEither, - JsonAuthSig, - LIT_SESSION_KEY_URI, - LOCAL_STORAGE_KEYS, - SessionKeyPair, - SessionSigsProp, -} from '@lit-protocol/constants'; -import { SiweMessage } from 'lit-siwe'; -import nacl from 'tweetnacl'; - -import { - uint8arrayFromString, - uint8arrayToString, -} from '@lit-protocol/uint8arrays'; - -import { getStorageItem } from '@lit-protocol/misc-browser'; -import { generateSessionKeyPair } from '@lit-protocol/crypto'; -import { checkAndSignAuthMessage } from './auth-browser'; -/** ========== Local Helpers ========== */ - -/** - * Get the session key pair from local storage if it exists, - * otherwise generate a new one - * - * @returns { SessionKeyPair } sessionKey - */ -const getSessionKey = (): SessionKeyPair => { - const storageKey = LOCAL_STORAGE_KEYS.SESSION_KEY; - - // check if we already have a session key + signature for this chain - let sessionKeyOrError: IEither = getStorageItem(storageKey); - let sessionKey: SessionKeyPair; - - // if we don't have a session key, generate one - if (sessionKeyOrError.type === EITHER_TYPE.ERROR) { - sessionKey = generateSessionKeyPair(); - localStorage.setItem(storageKey, JSON.stringify(sessionKey)); - } else { - sessionKey = JSON.parse(sessionKeyOrError.result); - } - - return sessionKey; -}; - -/** - * Get the wallet signature from local storage if it exists, - * otherwise check and sign auth message - * - * @returns { JsonAuthSig } walletSignature - */ -const getWalletSignature = async ( - params: SessionSigsProp, - sessionKeyUri: string -): Promise => { - const storageKey = LOCAL_STORAGE_KEYS.WALLET_SIGNATURE; - - // check if we already have a wallet signature - let walletSignatureOrError: IEither = getStorageItem(storageKey); - let walletSignature: JsonAuthSig; - - // if we don't have a wallet signature - if (walletSignatureOrError.type === EITHER_TYPE.ERROR) { - walletSignature = await checkAndSignAuthMessage({ - chain: params.chain, - resources: params.sessionCapabilities, - switchChain: params.switchChain, - expiration: params.expiration, - uri: sessionKeyUri, - }); - localStorage.setItem(storageKey, JSON.stringify(walletSignature)); - } else { - walletSignature = JSON.parse(walletSignatureOrError.result); - } - - return walletSignature; -}; - -/** ========== Exports ========== */ - -/** - * - * High level, how this works: - * 1. Generate or retrieve session key - * 2. Generate or retrieve the wallet signature of the session key - * 3. Sign the specific resources with the session key - * - * @param { SessionSigsProp } params - * - * @returns - */ -export async function getSessionSigs(params: SessionSigsProp) { - // ========== Prepare Params ========== - - let { - expiration, - chain, - resources = [], - sessionCapabilities, - switchChain, - litNodeClient, - } = params; - - // -- get session key - const sessionKey: SessionKeyPair = getSessionKey(); - - // -- get session key URI - let sessionKeyUri = getSessionKeyUri({ publicKey: sessionKey.publicKey }); - - // -- get sessionCapabilities - // if the user passed no sessionCapabilities, let's create them for them - // with wildcards so the user doesn't have to sign every time - if (!sessionCapabilities || sessionCapabilities.length === 0) { - sessionCapabilities = resources.map((resource: any) => { - const { protocol, resourceId } = parseResource({ resource }); - return `${protocol}Capability://*`; - }); - } - - // -- get wallet signature - let walletSig: JsonAuthSig = await getWalletSignature(params, sessionKeyUri); - - // ========== Validate Signature ========== - // Check a few things, including that: - // 1. the sig isn't expired - // 2. the sig is for the correct session key - // 3. the sig has the sessionCapabilities requires to fulfill the resources requested - - // NOTE: "verify" doesn't exist on SwieMessage - const siweMessage: any = new SiweMessage(walletSig.signedMessage); - let needToReSignSessionKey = false; - - try { - // make sure it's legit - await siweMessage.verify({ signature: walletSig.sig }); - } catch (e) { - needToReSignSessionKey = true; - } - - // make sure the sig is for the correct session key - if (siweMessage.uri !== sessionKeyUri) { - needToReSignSessionKey = true; - } - - // make sure the sig has the session capabilities required to fulfill the resources requested - for (let i = 0; i < resources.length; i++) { - const resource = resources[i]; - const { protocol, resourceId } = parseResource({ resource }); - - // check if we have blanket permissions or if we authed the specific resource for the protocol - const permissionsFound = sessionCapabilities.some((capability: any) => { - const capabilityParts = parseResource({ resource: capability }); - return ( - capabilityParts.protocol === protocol && - (capabilityParts.resourceId === '*' || - capabilityParts.resourceId === resourceId) - ); - }); - if (!permissionsFound) { - needToReSignSessionKey = true; - } - } - - if (needToReSignSessionKey) { - walletSig = await checkAndSignAuthMessage({ - chain, - resources: sessionCapabilities, - switchChain, - expiration, - uri: sessionKeyUri, - }); - } - - // ========== Sign Resources with Session Key ========== - // okay great, now we have a valid signed session key - // let's sign the resources with the session key - // 5 minutes is the default expiration for a session signature - // because we can generate a new session sig every time the user wants to access a resource - // without prompting them to sign with their wallet - let sessionExpiration = new Date(Date.now() + 1000 * 60 * 5); - - const signingTemplate = { - sessionKey: sessionKey.publicKey, - resources, - capabilities: [walletSig], - issuedAt: new Date().toISOString(), - expiration: sessionExpiration.toISOString(), - }; - const signatures: any = {}; - - litNodeClient.connectedNodes.forEach((nodeAddress: any) => { - const toSign = { - ...signingTemplate, - nodeAddress, - }; - let signedMessage = JSON.stringify(toSign); - const uint8arrayKey = uint8arrayFromString(sessionKey.secretKey, 'base16'); - const uint8arrayMessage = uint8arrayFromString(signedMessage, 'utf8'); - let signature = nacl.sign.detached(uint8arrayMessage, uint8arrayKey); - // console.log("signature", signature); - signatures[nodeAddress] = { - sig: uint8arrayToString(signature, 'base16'), - derivedVia: 'litSessionSignViaNacl', - signedMessage, - address: sessionKey.publicKey, - algo: 'ed25519', - }; - }); - - return signatures; -} - -/** - * - * Get Session Key URI eg. lit:session: - * - * @returns { string } - * - */ -export const getSessionKeyUri = ({ - publicKey, -}: { - publicKey: string; -}): string => { - return LIT_SESSION_KEY_URI + publicKey; -}; - -/** - * - * Parse resource - * - * @property { any } resource - * - * @returns { { protocol: string, resourceId: string } } - * - */ -export const parseResource = ({ - resource, -}: { - resource: any; -}): { - protocol: any; - resourceId: any; -} => { - const [protocol, resourceId] = resource.split('://'); - return { protocol, resourceId }; -}; diff --git a/packages/auth/.babelrc b/packages/auth/.babelrc new file mode 100644 index 000000000..e24a5465f --- /dev/null +++ b/packages/auth/.babelrc @@ -0,0 +1,10 @@ +{ + "presets": [ + [ + "@nrwl/web/babel", + { + "useBuiltIns": "usage" + } + ] + ] +} diff --git a/packages/auth/.eslintrc.json b/packages/auth/.eslintrc.json new file mode 100644 index 000000000..9d9c0db55 --- /dev/null +++ b/packages/auth/.eslintrc.json @@ -0,0 +1,18 @@ +{ + "extends": ["../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + } + ] +} diff --git a/packages/auth/LICENSE.md b/packages/auth/LICENSE.md new file mode 100644 index 000000000..e69de29bb diff --git a/packages/auth/README.md b/packages/auth/README.md new file mode 100644 index 000000000..30eb1aa4f --- /dev/null +++ b/packages/auth/README.md @@ -0,0 +1,16 @@ +# Quick Start + +### node.js / browser + +``` +yarn add @lit-protocol/auth +``` + +### Vanilla JS (UMD) + +```js + + +``` \ No newline at end of file diff --git a/packages/auth/babel.config.json b/packages/auth/babel.config.json new file mode 100644 index 000000000..1e4a7f574 --- /dev/null +++ b/packages/auth/babel.config.json @@ -0,0 +1,11 @@ +{ + "presets": [ + [ + "@nrwl/web/babel", + { + "useBuiltIns": "usage" + } + ] + ] + } + \ No newline at end of file diff --git a/packages/auth/jest.config.ts b/packages/auth/jest.config.ts new file mode 100644 index 000000000..e0681e1dd --- /dev/null +++ b/packages/auth/jest.config.ts @@ -0,0 +1,15 @@ +/* eslint-disable */ +export default { + displayName: 'auth', + preset: '../../jest.preset.js', + globals: { + 'ts-jest': { + tsconfig: '/tsconfig.spec.json', + }, + }, + transform: { + '^.+\\.[tj]s$': 'ts-jest', + }, + moduleFileExtensions: ['ts', 'js', 'html'], + coverageDirectory: '../../coverage/packages/auth', +}; diff --git a/packages/auth/package.json b/packages/auth/package.json new file mode 100644 index 000000000..5f569de63 --- /dev/null +++ b/packages/auth/package.json @@ -0,0 +1,14 @@ +{ + "name": "@lit-protocol/auth", + "version": "2.1.17", + "publishConfig": { + "access": "public", + "directory": "../../dist/packages/auth" + }, + "gitHead": "0d7334c2c55f448e91fe32f29edc5db8f5e09e4b", + "tags": [ + "universal" + ], + "main": "./dist/src/index.js", + "typings": "./dist/src/index.d.ts" +} \ No newline at end of file diff --git a/packages/auth/project.json b/packages/auth/project.json new file mode 100644 index 000000000..2f7991c20 --- /dev/null +++ b/packages/auth/project.json @@ -0,0 +1,77 @@ +{ + "name": "auth", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "packages/auth/src", + "projectType": "library", + "targets": { + + "build":{ + "executor": "nx:run-commands", + "options": { + "command": "yarn build:target auth" + } + }, + + "_buildTsc": { + "executor": "@nrwl/js:tsc", + "outputs": ["{options.outputPath}"], + "options": { + "outputPath": "dist/packages/auth", + "main": "packages/auth/src/index.ts", + "tsConfig": "packages/auth/tsconfig.lib.json", + "assets": ["packages/auth/*.md"] + } + }, + "_buildWeb": { + "executor": "@websaam/nx-esbuild:package", + "options": { + "bundle": true, + "sourcemap": true, + "metafile": true, + "globalName": "LitJsSdk_auth", + "outfile":"dist/packages/auth-vanilla/auth.js", + "entryPoints": ["./packages/auth/src/index.ts"], + "define": { "global": "window" }, + "plugins":[ + { + "package": "esbuild-node-builtins", + "function": "nodeBuiltIns" + } + ] + } + }, + + "generateDoc":{ + "executor": "nx:run-commands", + "options": { + "command": "yarn typedoc --entryPointStrategy expand packages/auth/src --exclude packages/auth/src/**/*.spec.** --tsconfig packages/auth/tsconfig.json" + } + }, + + "lint": { + "executor": "@nrwl/linter:eslint", + "outputs": ["{options.outputFile}"], + "options": { + "lintFilePatterns": ["packages/auth/**/*.ts"] + } + }, + "test": { + "executor": "@nrwl/jest:jest", + "outputs": ["coverage/packages/auth"], + "options": { + "jestConfig": "packages/auth/jest.config.ts", + "passWithNoTests": true + } + }, + "testWatch": { + "executor": "@nrwl/jest:jest", + "outputs": ["coverage/packages/auth"], + "options": { + "jestConfig": "packages/auth/jest.config.ts", + "passWithNoTests": true, + "watch": true + } + } + }, + "tags": [] +} diff --git a/packages/auth/src/index.ts b/packages/auth/src/index.ts new file mode 100644 index 000000000..20d95f396 --- /dev/null +++ b/packages/auth/src/index.ts @@ -0,0 +1 @@ +export * from './lib/session-capability-object'; diff --git a/packages/auth/src/lib/session-capability-object.spec.ts b/packages/auth/src/lib/session-capability-object.spec.ts new file mode 100644 index 000000000..f73400b26 --- /dev/null +++ b/packages/auth/src/lib/session-capability-object.spec.ts @@ -0,0 +1,88 @@ +import { SessionCapabilityObject } from './session-capability-object'; + +const isClass = (v: any) => { + return typeof v === 'function' && /^\s*class\s+/.test(v.toString()); +}; + +describe('sessionCapabilityObject', () => { + // --global + let sessionCapabilityObject; + + // -- start + it('imported { SessionCapabilityObject } is a class', async () => { + expect(isClass(SessionCapabilityObject)).toBe(true); + }); + + it('should be able to instantiate a new SessionCapabilityObject', async () => { + const sessionCapabilityObject = new SessionCapabilityObject(); + expect(sessionCapabilityObject).toBeDefined(); + }); + + it('should be empty for a new SessionCapabilityObject', async () => { + const sessionCapabilityObject = new SessionCapabilityObject(); + expect(sessionCapabilityObject.isEmpty()).toBe(true); + }); + + it('should not be empty for a SessionCapabilityObject with a capability', async () => { + const sessionCapabilityObject = new SessionCapabilityObject(); + sessionCapabilityObject.setCapableActionsForAllResources(['read']); + expect(sessionCapabilityObject.isEmpty()).toBe(false); + }); + + it('should be able to set a capability for all resources', async () => { + const sessionCapabilityObject = new SessionCapabilityObject(); + sessionCapabilityObject.setCapableActionsForAllResources(['read']); + expect(sessionCapabilityObject.getCapableActionsForAllResources()).toEqual([ + 'read', + ]); + }); + + it('should be able to set a capability for a resource', async () => { + const sessionCapabilityObject = new SessionCapabilityObject(); + sessionCapabilityObject.addCapableActionForResource('resourceId', 'read'); + expect( + sessionCapabilityObject.hasCapabilitiesForResource('read', 'resourceId') + ).toBe(true); + }); + + it('should be able to set multiple capable actions for a resource and check for capability to be true', async () => { + const sessionCapabilityObject = new SessionCapabilityObject(); + sessionCapabilityObject.addCapableActionForResource('resourceId', 'read'); + sessionCapabilityObject.addCapableActionForResource('resourceId', 'write'); + expect( + sessionCapabilityObject.hasCapabilitiesForResource('write', 'resourceId') + ).toBe(true); + }); + + it('should not have capability for resource for a new SessionCapabilityObject', async () => { + const sessionCapabilityObject = new SessionCapabilityObject(); + expect( + sessionCapabilityObject.hasCapabilitiesForResource('read', 'resourceId') + ).toBe(false); + }); + + it('should not have a capability for a resource if the capable action is not set', async () => { + const sessionCapabilityObject = new SessionCapabilityObject(); + sessionCapabilityObject.addCapableActionForResource('resourceId', 'read'); + expect( + sessionCapabilityObject.hasCapabilitiesForResource('write', 'resourceId') + ).toBe(false); + }); + + it('should have a capability for a resource if the capable action is set', async () => { + const sessionCapabilityObject = new SessionCapabilityObject(); + sessionCapabilityObject.addCapableActionForResource('resourceId', 'read'); + expect( + sessionCapabilityObject.hasCapabilitiesForResource('read', 'resourceId') + ).toBe(true); + }); + + it('should encode itself as a SIWE resource string', async () => { + const sessionCapabilityObject = new SessionCapabilityObject(); + sessionCapabilityObject.setCapableActionsForAllResources(['read']); + sessionCapabilityObject.addCapableActionForResource('resourceId', 'write'); + expect(sessionCapabilityObject.encodeAsSiweResource()).toEqual( + 'urn:recap:lit:session:eyJkZWYiOlsicmVhZCJdLCJ0YXIiOnsicmVzb3VyY2VJZCI6WyJ3cml0ZSJdfX0=' + ); + }); +}); diff --git a/packages/auth/src/lib/session-capability-object.ts b/packages/auth/src/lib/session-capability-object.ts new file mode 100644 index 000000000..172d96b71 --- /dev/null +++ b/packages/auth/src/lib/session-capability-object.ts @@ -0,0 +1,141 @@ +import { Base64 } from 'js-base64'; + +// TODO: use new ReCap x UCAN compatible format +export class SessionCapabilityObject { + private def?: string[]; + private tar?: { [key: string]: string[] }; + private ext?: { [key: string]: string }; + + constructor() {} + + /** + * + * Checks whether the session capability object is empty. + * + * @returns { boolean } true if the session capability object is empty. + * + */ + isEmpty(): boolean { + return ( + this.def === undefined && this.tar === undefined && this.ext === undefined + ); + } + + /** + * + * Gets the capable actions for all resources. + * + * @returns { string[] } the capable actions for all resources. + * + */ + getCapableActionsForAllResources(): string[] { + return this.def ? this.def : []; + } + + /** + * + * Adds a capability to perform the given action for all resources. + * + */ + setCapableActionsForAllResources(actions: string[]): void { + this.def = actions; + } + + /** + * + * Adds a capability to perform the given action for the given resource. + * + * @param resourceId is the resource to add the capability for. + * @param action is the action to add the capability for. + * + */ + addCapableActionForResource(resourceId: string, action: string): void { + if (this.tar === undefined) { + this.tar = {}; + } + + if (this.tar[resourceId] === undefined) { + this.tar[resourceId] = []; + } + + // Check if action already exists. + if (this.tar[resourceId].indexOf(action) !== -1) { + return; + } + + this.tar[resourceId].push(action); + } + + /** + * + * Encodes the session capability object as a SIWE resource string. + * + * Context: The SIWE ReCap standard encodes the session capability object as a resource. + * + * @param { SessionCapabilityObject } sessionCapabilityObject is the session capability object. + * @returns { string } the encoded resource string. + * + */ + encodeAsSiweResource(): string { + return `urn:recap:lit:session:${Base64.encode( + JSON.stringify(this.#getAsObject()) + )}`; + } + + /** + * + * Checks whether the session capability object has the given capabilities to perform the given action + * for the given resource. + * + * @param action is the action to check for capabilities. + * @param resourceId is the resource to check for capabilities. + * @returns { boolean } true if the session capability object has the given capabilities to perform the given action. + * + */ + hasCapabilitiesForResource = ( + action: string, + resourceId: string + ): boolean => { + // first check default permitted actions + if (this.def) { + for (const defaultAction of this.def) { + if (defaultAction === '*' || defaultAction === action) { + return true; + } + } + } + + // then check specific targets + if (this.tar) { + if (Object.keys(this.tar).indexOf(resourceId) === -1) { + return false; + } + + for (const permittedAction of this.tar[resourceId]) { + if (permittedAction === '*' || permittedAction === action) { + return true; + } + } + } + + return false; + }; + + #getAsObject = (): object => { + const obj: { [key: string]: any } = {}; + + if (this.def) { + obj['def'] = this.def; + } + + if (this.tar) { + obj['tar'] = this.tar; + } + + if (this.ext) { + obj['ext'] = this.ext; + } + + return obj; + }; +} diff --git a/packages/auth/tsconfig.json b/packages/auth/tsconfig.json new file mode 100644 index 000000000..f5b85657a --- /dev/null +++ b/packages/auth/tsconfig.json @@ -0,0 +1,22 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "module": "commonjs", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/packages/auth/tsconfig.lib.json b/packages/auth/tsconfig.lib.json new file mode 100644 index 000000000..e85ef50f6 --- /dev/null +++ b/packages/auth/tsconfig.lib.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "declaration": true, + "types": [] + }, + "include": ["**/*.ts"], + "exclude": ["jest.config.ts", "**/*.spec.ts", "**/*.test.ts"] +} diff --git a/packages/auth/tsconfig.spec.json b/packages/auth/tsconfig.spec.json new file mode 100644 index 000000000..546f12877 --- /dev/null +++ b/packages/auth/tsconfig.spec.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "module": "commonjs", + "types": ["jest", "node"] + }, + "include": ["jest.config.ts", "**/*.test.ts", "**/*.spec.ts", "**/*.d.ts"] +} diff --git a/packages/bls-sdk/package.json b/packages/bls-sdk/package.json index d5bd07312..44d83d005 100644 --- a/packages/bls-sdk/package.json +++ b/packages/bls-sdk/package.json @@ -15,4 +15,4 @@ ], "main": "./dist/src/index.js", "typings": "./dist/src/index.d.ts" -} +} \ No newline at end of file diff --git a/packages/constants/package.json b/packages/constants/package.json index 3b2fb1fbc..81c5bab13 100644 --- a/packages/constants/package.json +++ b/packages/constants/package.json @@ -11,4 +11,4 @@ ], "main": "./dist/src/index.js", "typings": "./dist/src/index.d.ts" -} +} \ No newline at end of file diff --git a/packages/constants/src/lib/errors.ts b/packages/constants/src/lib/errors.ts index 33fdcb701..0fa34db4a 100644 --- a/packages/constants/src/lib/errors.ts +++ b/packages/constants/src/lib/errors.ts @@ -51,6 +51,10 @@ export const LIT_ERROR = { name: 'LocalStorageItemNotFoundException', code: 'local_storage_item_not_found_exception', }, + LOCAL_STORAGE_ITEM_NOT_SET_EXCEPTION: { + name: 'LocalStorageItemNotSetException', + code: 'local_storage_item_not_set_exception', + }, REMOVED_FUNCTION_ERROR: { name: 'RemovedFunctionError', code: 'removed_function_error', diff --git a/packages/constants/src/lib/interfaces/interfaces.ts b/packages/constants/src/lib/interfaces/interfaces.ts index 366c55f54..0008fb23c 100644 --- a/packages/constants/src/lib/interfaces/interfaces.ts +++ b/packages/constants/src/lib/interfaces/interfaces.ts @@ -85,7 +85,6 @@ export interface JsonAuthSig { derivedVia: string; signedMessage: string; address: string; - capabilities?: []; algo?: []; } @@ -94,7 +93,7 @@ export interface CheckAndSignAuthParams { chain: Chain; // Optional and only used with EVM chains. A list of resources to be passed to Sign In with Ethereum. These resources will be part of the Sign in with Ethereum signed message presented to the user. - resources?: any[]; + resources?: string[]; // ptional and only used with EVM chains right now. Set to true by default. Whether or not to ask Metamask or the user's wallet to switch chains before signing. This may be desired if you're going to have the user send a txn on that chain. On the other hand, if all you care about is the user's wallet signature, then you probably don't want to make them switch chains for no reason. Pass false here to disable this chain switching behavior. switchChain?: boolean; @@ -604,15 +603,6 @@ export interface SessionKeySignedMessage { nodeAddress: string; } -export interface SessionSigsProp { - expiration?: any; - chain: Chain; - resources: any[]; - sessionCapabilities?: any; - switchChain?: boolean; - litNodeClient: ILitNodeClient; -} - export interface SessionKeyPair { publicKey: string; secretKey: string; @@ -662,27 +652,6 @@ export interface GetSignSessionKeySharesProp { body: SessionRequestBody; } -export interface GetSessionSigsProps { - // When this session signature will expire. The user will have to reauthenticate after this time using whatever auth method you set up. This means you will have to call this signSessionKey function again to get a new session signature. This is a RFC3339 timestamp. The default is 24 hours from now. - expiration?: any; - - // The chain to use for the session signature. This is the chain that will be used to sign the session key. If you're using EVM then this probably doesn't matter at all. - chain: any; - - // These are the resources that will be signed with the session key. You may pass a wildcard that allows these session signatures to work with any resource on Lit. To see a list of resources, check out the docs: https://developer.litprotocol.com/sdk/explanation/walletsigs/sessionsigs/#resources-you-can-request - resources: any; - - // An optional list of capabilities that you want to request for this session. If you pass nothing, then this will default to a wildcard for each type of resource you're accessing. For example, if you passed ["litEncryptionCondition://123456"] then this would default to ["litEncryptionConditionCapability://*"], which would grant this session signature the ability to decrypt any resource. - sessionCapabilities?: any; - - // If you want to ask Metamask to try and switch the user's chain, you may pass true here. This will only work if the user is using Metamask. If the user is not using Metamask, then this will be ignored. - switchChain?: any; - - // This is a callback that will be called if the user needs to authenticate using a PKP. For example, if the user has no wallet, but owns a Lit PKP though something like Google Oauth, then you can use this callback to prompt the user to authenticate with their PKP. This callback should use the LitNodeClient.signSessionKey function to get a session signature for the user from their PKP. If you don't pass this callback, then the user will be prompted to authenticate with their wallet, like metamask. - authNeededCallback?: any; - sessionKey?: any; -} - /* body must include: pub session_key: String, pub auth_methods: Vec, diff --git a/packages/contracts-sdk/package.json b/packages/contracts-sdk/package.json index 1a1b2ebd4..49414c735 100644 --- a/packages/contracts-sdk/package.json +++ b/packages/contracts-sdk/package.json @@ -18,4 +18,4 @@ ], "main": "./dist/src/index.js", "typings": "./dist/src/index.d.ts" -} +} \ No newline at end of file diff --git a/packages/crypto/package.json b/packages/crypto/package.json index 43b4c9696..d09174484 100644 --- a/packages/crypto/package.json +++ b/packages/crypto/package.json @@ -12,4 +12,4 @@ ], "main": "./dist/src/index.js", "typings": "./dist/src/index.d.ts" -} +} \ No newline at end of file diff --git a/packages/ecdsa-sdk/package.json b/packages/ecdsa-sdk/package.json index 2315c590b..28c7803f0 100644 --- a/packages/ecdsa-sdk/package.json +++ b/packages/ecdsa-sdk/package.json @@ -12,4 +12,4 @@ ], "main": "./dist/src/index.js", "typings": "./dist/src/index.d.ts" -} +} \ No newline at end of file diff --git a/packages/encryption/package.json b/packages/encryption/package.json index bfd1d2f46..88591d523 100644 --- a/packages/encryption/package.json +++ b/packages/encryption/package.json @@ -12,4 +12,4 @@ ], "main": "./dist/src/index.js", "typings": "./dist/src/index.d.ts" -} +} \ No newline at end of file diff --git a/packages/lit-node-client/package.json b/packages/lit-node-client/package.json index ab49d3b5b..6443066cd 100644 --- a/packages/lit-node-client/package.json +++ b/packages/lit-node-client/package.json @@ -12,4 +12,4 @@ ], "main": "./dist/src/index.js", "typings": "./dist/src/index.d.ts" -} +} \ No newline at end of file diff --git a/packages/lit-node-client/src/index.ts b/packages/lit-node-client/src/index.ts index 30f4c4d1f..7bda4d20b 100644 --- a/packages/lit-node-client/src/index.ts +++ b/packages/lit-node-client/src/index.ts @@ -17,12 +17,7 @@ if (!globalThis.LitNodeClient) { export * from './lib/lit-node-client'; -export { - checkAndSignAuthMessage, - getSessionKeyUri, - getSessionSigs, - parseResource, -} from '@lit-protocol/auth-browser'; +export { checkAndSignAuthMessage } from '@lit-protocol/auth-browser'; export { decryptFile, diff --git a/packages/lit-node-client/src/lib/interfaces.ts b/packages/lit-node-client/src/lib/interfaces.ts new file mode 100644 index 000000000..336a0be1c --- /dev/null +++ b/packages/lit-node-client/src/lib/interfaces.ts @@ -0,0 +1,35 @@ +import { SessionCapabilityObject } from '@lit-protocol/auth'; +import { Chain } from '@lit-protocol/constants'; + +export interface GetSessionSigsProps { + // When this session signature will expire. The user will have to reauthenticate after this time using whatever auth method you set up. + // This means you will have to call this signSessionKey function again to get a new session signature. This is a RFC3339 timestamp. + // The default is 24 hours from now. + expiration?: any; + + // The chain to use for the session signature. This is the chain that will be used to sign the session key. If you're using EVM then + // this probably doesn't matter at all. + chain: Chain; + + // These are the resources that will be signed with the session key. You may pass a wildcard that allows these session signatures to work + // with any resource on Lit. To see a list of resources,check out the docs: + // https://developer.litprotocol.com/sdk/explanation/walletsigs/sessionsigs/#resources-you-can-request + resources: string[]; + + // An optional dictionary of capabilities that you want to request for this session. If this field is not provided, then this will default + // to a wildcard for each type of resource you're accessing. For example, if the resource provided is ["litEncryptionCondition://123456"] + // then this would default to ["litEncryptionConditionCapability://*"], which would grant this session signature the ability to decrypt + // any resource. This object MUST be compatible with EIP-5573 SIWE ReCap, see: https://ethereum-magicians.org/t/eip-5573-siwe-recap/10627. + sessionCapabilityObject?: SessionCapabilityObject; + + // If you want to ask Metamask to try and switch the user's chain, you may pass true here. This will only work if the user is using Metamask. + // If the user is not using Metamask, then this will be ignored. + switchChain?: boolean; + + // This is a callback that will be called if the user needs to authenticate using a PKP. For example, if the user has no wallet, but owns a + // Lit PKP though something like Google Oauth, then you can use this callback to prompt the user to authenticate with their PKP. This callback + // should use the LitNodeClient.signSessionKey function to get a session signature for the user from their PKP. If you don't pass this callback, + // then the user will be prompted to authenticate with their wallet, like metamask. + authNeededCallback?: any; + sessionKey?: any; +} diff --git a/packages/lit-node-client/src/lib/lit-node-client.spec.ts b/packages/lit-node-client/src/lib/lit-node-client.spec.ts index d328fddfb..d1896f4dd 100644 --- a/packages/lit-node-client/src/lib/lit-node-client.spec.ts +++ b/packages/lit-node-client/src/lib/lit-node-client.spec.ts @@ -13,7 +13,7 @@ import { nacl } from '@lit-protocol/nacl'; globalThis.nacl = nacl; import crypto, { createHash } from 'crypto'; -import { getSessionKeyUri } from '@lit-protocol/auth-browser'; +import { SessionCapabilityObject } from '@lit-protocol/auth'; Object.defineProperty(global.self, 'crypto', { value: { getRandomValues: (arr: any) => crypto.randomBytes(arr.length), @@ -133,8 +133,13 @@ describe('litNodeClient', () => { let resources = [`litSigningCondition://${hashedResourceId}`]; - let capabilities = litNodeClient.getSessionCapabilities([], resources); - expect(capabilities[0]).toBe('litSigningConditionCapability://*'); + let capabilityObject = litNodeClient.getSessionCapabilityObject( + resources, + new SessionCapabilityObject() + ); + expect(capabilityObject.getCapableActionsForAllResources()[0]).toBe( + 'litSigningCondition' + ); }); it('gets expiration', () => { diff --git a/packages/lit-node-client/src/lib/lit-node-client.ts b/packages/lit-node-client/src/lib/lit-node-client.ts index 3c36bf25a..1b186d66c 100644 --- a/packages/lit-node-client/src/lib/lit-node-client.ts +++ b/packages/lit-node-client/src/lib/lit-node-client.ts @@ -18,7 +18,6 @@ import { ExecuteJsProps, ExecuteJsResponse, FormattedMultipleAccs, - GetSessionSigsProps, GetSignSessionKeySharesProp, HandshakeWithSgx, JsonAuthSig, @@ -86,9 +85,11 @@ import { joinSignature, sha256 } from 'ethers/lib/utils'; import { LitThirdPartyLibs } from '@lit-protocol/lit-third-party-libs'; import { nacl } from '@lit-protocol/nacl'; -import { getStorageItem } from '@lit-protocol/misc-browser'; +import { getStorageItem, setStorageItem } from '@lit-protocol/misc-browser'; import { BigNumber } from 'ethers'; import { checkAndSignAuthMessage } from '@lit-protocol/auth-browser'; +import { SessionCapabilityObject } from '@lit-protocol/auth'; +import { GetSessionSigsProps } from './interfaces'; declare global { var litNodeClient: LitNodeClient; @@ -340,24 +341,39 @@ export class LitNodeClient { /** * - * Get session capabilities from user, it not, generates one - * @param { Array } capabilities - * @param { Array } resources - * @return { Array } + * Get session capability object from user, if not, generates one + * with wildcards so the user doesn't have to sign every time. + * + * @param { SessionCapabilityObject } sessionCapabilityObject + * @param { Array } resources + * @return { SessionCapabilityObject } */ - getSessionCapabilities = ( - capabilities: Array, - resources: Array - ): Array => { - if (!capabilities || capabilities.length == 0) { - capabilities = resources.map((resource: any) => { - const { protocol, resourceId } = this.parseResource({ resource }); - - return `${protocol}Capability://*`; - }); + getSessionCapabilityObject = ( + resources: Array, + sessionCapabilityObject?: SessionCapabilityObject + ): SessionCapabilityObject => { + // Return the capabilities if they are already set + if (sessionCapabilityObject && !sessionCapabilityObject.isEmpty()) { + return sessionCapabilityObject; } - return capabilities; + let capabilityObject: SessionCapabilityObject = + new SessionCapabilityObject(); + + let defaultActionsToAdd = new Set(); + + resources.forEach((resource) => { + const { protocol } = this.parseResource({ resource }); + + if (!defaultActionsToAdd.has(protocol)) { + defaultActionsToAdd.add(protocol); + } + }); + + capabilityObject.setCapableActionsForAllResources( + Array.from(defaultActionsToAdd) + ); + return capabilityObject; }; /** @@ -377,15 +393,15 @@ export class LitNodeClient { getWalletSig = async ({ authNeededCallback, chain, - capabilities, + sessionCapabilityObject, switchChain, expiration, sessionKeyUri, }: { authNeededCallback: any; chain: string; - capabilities: Array; - switchChain: boolean; + sessionCapabilityObject: SessionCapabilityObject; + switchChain?: boolean; expiration: string; sessionKeyUri: string; }): Promise => { @@ -397,7 +413,7 @@ export class LitNodeClient { // -- (TRY) to get it in the local storage if (storedWalletSigOrError.type === 'ERROR') { console.warn( - `Storage key "${storageKey}" is missing. Not a problem. Contiune...` + `Storage key "${storageKey}" is missing. Not a problem. Continue...` ); } else { walletSig = storedWalletSigOrError.result; @@ -408,7 +424,7 @@ export class LitNodeClient { if (authNeededCallback) { walletSig = await authNeededCallback({ chain, - resources: capabilities, + resources: [sessionCapabilityObject.encodeAsSiweResource()], switchChain, expiration, uri: sessionKeyUri, @@ -416,12 +432,23 @@ export class LitNodeClient { } else { walletSig = await checkAndSignAuthMessage({ chain, - resources: capabilities, + resources: [sessionCapabilityObject.encodeAsSiweResource()], switchChain, expiration, uri: sessionKeyUri, }); } + + // (TRY) to set walletSig to local storage + const storeNewWalletSigOrError = setStorageItem( + storageKey, + JSON.stringify(walletSig) + ); + if (storeNewWalletSigOrError.type === 'ERROR') { + console.warn( + `Unable to store walletSig in local storage. Not a problem. Continue...` + ); + } } else { try { walletSig = JSON.parse(storedWalletSigOrError.result); @@ -443,48 +470,43 @@ export class LitNodeClient { walletSignature, sessionKeyUri, resources, - sessionCapabilities, + sessionCapabilityObject, }: { siweMessage: SiweMessage; walletSignature: any; sessionKeyUri: any; - resources: any; - sessionCapabilities: Array; + resources: string[]; + sessionCapabilityObject: SessionCapabilityObject; }): Promise => { - let needToResign = false; - try { // @ts-ignore await siweMessage.verify({ signature: walletSignature }); } catch (e) { - needToResign = true; + return true; } // make sure the sig is for the correct session key if (siweMessage.uri !== sessionKeyUri) { - needToResign = true; + return true; } // make sure the sig has the session capabilities required to fulfill the resources requested for (let i = 0; i < resources.length; i++) { const resource = resources[i]; - const { protocol, resourceId } = this.parseResource({ resource }); // check if we have blanket permissions or if we authed the specific resource for the protocol - const permissionsFound = sessionCapabilities.some((capability: any) => { - const capabilityParts = this.parseResource({ resource: capability }); - return ( - capabilityParts.protocol === protocol && - (capabilityParts.resourceId === '*' || - capabilityParts.resourceId === resourceId) + const { protocol, resourceId } = this.parseResource({ resource }); + const permissionsFound = + sessionCapabilityObject.hasCapabilitiesForResource( + protocol, + resourceId ); - }); if (!permissionsFound) { - needToResign = true; + return true; } } - return needToResign; + return false; }; // ==================== SENDING COMMAND ==================== @@ -2414,10 +2436,10 @@ export class LitNodeClient { parseResource = ({ resource, }: { - resource: any; + resource: string; }): { - protocol: any; - resourceId: any; + protocol: string; + resourceId: string; } => { const [protocol, resourceId] = resource.split('://'); return { protocol, resourceId }; @@ -2438,10 +2460,10 @@ export class LitNodeClient { // Try to get it from local storage, if not generates one~ let sessionKey = this.getSessionKey(params.sessionKey); - let sessionKeyUri = LIT_SESSION_KEY_URI + sessionKey.publicKey; - let capabilities = this.getSessionCapabilities( - params.sessionCapabilities, - params.resources + let sessionKeyUri = this.getSessionKeyUri(sessionKey.publicKey); + let sessionCapabilityObject = this.getSessionCapabilityObject( + params.resources, + params.sessionCapabilityObject ); let expiration = params.expiration || this.getExpiration(); @@ -2449,7 +2471,7 @@ export class LitNodeClient { let walletSig = await this.getWalletSig({ authNeededCallback: params.authNeededCallback, chain: params.chain, - capabilities: capabilities, + sessionCapabilityObject, switchChain: params.switchChain, expiration: expiration, sessionKeyUri: sessionKeyUri, @@ -2462,7 +2484,7 @@ export class LitNodeClient { walletSignature: walletSig?.sig, sessionKeyUri, resources: params.resources, - sessionCapabilities: capabilities, + sessionCapabilityObject, }); // -- (CHECK) if we need to resign the session key @@ -2471,7 +2493,7 @@ export class LitNodeClient { if (params.authNeededCallback) { walletSig = await params.authNeededCallback({ chain: params.chain, - resources: capabilities, + resources: [sessionCapabilityObject.encodeAsSiweResource()], expiration, uri: sessionKeyUri, litNodeClient: this, @@ -2479,7 +2501,7 @@ export class LitNodeClient { } else { walletSig = await checkAndSignAuthMessage({ chain: params.chain, - resources: capabilities, + resources: [sessionCapabilityObject.encodeAsSiweResource()], switchChain: params.switchChain, expiration, uri: sessionKeyUri, @@ -2500,7 +2522,7 @@ export class LitNodeClient { return; } - // ===== AFTER we have Valid Signed Session Key ===== + // ===== AFTER we have Valid Wallet Signature containing Session Key ===== // - Let's sign the resources with the session key // - 5 minutes is the default expiration for a session signature // - Because we can generate a new session sig every time the user wants to access a resource without prompting them to sign with their wallet @@ -2545,4 +2567,15 @@ export class LitNodeClient { return signatures; }; + + /** + * + * Get Session Key URI eg. lit:session:0x1234 + * + * @param publicKey is the public key of the session key + * @returns { string } the session key uri + */ + getSessionKeyUri = (publicKey: string): string => { + return LIT_SESSION_KEY_URI + publicKey; + }; } diff --git a/packages/lit-third-party-libs/package.json b/packages/lit-third-party-libs/package.json index b544ab687..d5803c184 100644 --- a/packages/lit-third-party-libs/package.json +++ b/packages/lit-third-party-libs/package.json @@ -12,4 +12,4 @@ "gitHead": "0d7334c2c55f448e91fe32f29edc5db8f5e09e4b", "main": "./dist/src/index.js", "typings": "./dist/src/index.d.ts" -} +} \ No newline at end of file diff --git a/packages/misc-browser/package.json b/packages/misc-browser/package.json index a98ef5175..3638d00b3 100644 --- a/packages/misc-browser/package.json +++ b/packages/misc-browser/package.json @@ -12,4 +12,4 @@ ], "main": "./dist/src/index.js", "typings": "./dist/src/index.d.ts" -} +} \ No newline at end of file diff --git a/packages/misc-browser/src/lib/misc-browser.ts b/packages/misc-browser/src/lib/misc-browser.ts index 71bd0652f..a3ab27842 100644 --- a/packages/misc-browser/src/lib/misc-browser.ts +++ b/packages/misc-browser/src/lib/misc-browser.ts @@ -33,6 +33,25 @@ export const getStorageItem = (key: string): IEither => { return keyOrError; }; +/** + * + * Set the local storage item by key + * + * @param { string } key is the key to set + * @param { string } value is the value to set + */ +export const setStorageItem = (key: string, value: string): IEither => { + try { + localStorage.setItem(key, value); + return ERight(value); + } catch (e) { + return ELeft({ + message: `Failed to set ${key} in local storage`, + error: LIT_ERROR.LOCAL_STORAGE_ITEM_NOT_SET_EXCEPTION, + }); + } +}; + /** * Convert a Blob to a base64urlpad string. Note: This function returns a promise. * diff --git a/packages/misc/package.json b/packages/misc/package.json index 7fba49b16..37244f084 100644 --- a/packages/misc/package.json +++ b/packages/misc/package.json @@ -15,4 +15,4 @@ ], "main": "./dist/src/index.js", "typings": "./dist/src/index.d.ts" -} +} \ No newline at end of file diff --git a/packages/nacl/package.json b/packages/nacl/package.json index c4d008d7f..0de1f165f 100644 --- a/packages/nacl/package.json +++ b/packages/nacl/package.json @@ -12,4 +12,4 @@ }, "main": "./dist/src/index.js", "typings": "./dist/src/index.d.ts" -} +} \ No newline at end of file diff --git a/packages/uint8arrays/package.json b/packages/uint8arrays/package.json index e4bb7ef39..d9bfc2c99 100644 --- a/packages/uint8arrays/package.json +++ b/packages/uint8arrays/package.json @@ -12,4 +12,4 @@ ], "main": "./dist/src/index.js", "typings": "./dist/src/index.d.ts" -} +} \ No newline at end of file diff --git a/workspace.json b/workspace.json index b66058608..666dbb46b 100644 --- a/workspace.json +++ b/workspace.json @@ -3,6 +3,7 @@ "version": 2, "projects": { "access-control-conditions": "packages/access-control-conditions", + "auth": "packages/auth", "auth-browser": "packages/auth-browser", "bls-sdk": "packages/bls-sdk", "constants": "packages/constants", diff --git a/yarn.lock b/yarn.lock index 03022078b..a8fe35230 100644 --- a/yarn.lock +++ b/yarn.lock @@ -15598,6 +15598,11 @@ join-path@^1.1.1: url-join "0.0.1" valid-url "^1" +js-base64@^3.7.2: + version "3.7.4" + resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-3.7.4.tgz#af95b20f23efc8034afd2d1cc5b9d0adf7419037" + integrity sha512-wpM/wi20Tl+3ifTyi0RdDckS4YTD4Lf953mBRrpG8547T7hInHNPEj8+ck4gB8VDcGyeAWFK++Wb/fU1BeavKQ== + js-levenshtein@^1.1.6: version "1.1.6" resolved "https://registry.yarnpkg.com/js-levenshtein/-/js-levenshtein-1.1.6.tgz#c6cee58eb3550372df8deb85fad5ce66ce01d59d"