-
-
Notifications
You must be signed in to change notification settings - Fork 277
feat: added authenticated user storage #8260
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
Open
dovydas55
wants to merge
29
commits into
main
Choose a base branch
from
MCA-83
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
29 commits
Select commit
Hold shift + click to select a range
c10189d
feat: added authenticated user storage
dovydas55 16163c3
feat: updated changelog
dovydas55 38b17aa
feat: updated changelog
dovydas55 2af31ef
feat: fixing linting errors
dovydas55 782b5ce
feat: fixing linting errors
dovydas55 27911b3
Merge branch 'main' into MCA-83
dovydas55 91701fc
feat: updated API contract
dovydas55 eedd6cc
Merge branch 'MCA-83' of github.com:MetaMask/core into MCA-83
dovydas55 ce5671d
feat: cleaning up
dovydas55 cccf519
Merge branch 'main' of github.com:MetaMask/core into MCA-83
dovydas55 e907cac
feat: added new package authenticated user storage
dovydas55 f79f384
feat: added missing ASU types
dovydas55 55f7144
feat: added auth-engineers team
dovydas55 235b0e5
feat: fixing tests
dovydas55 0f2599a
feat: fixing tests
dovydas55 b7918e5
feat: fixing tests
dovydas55 72e9c2a
feat: fixing tests
dovydas55 acf46c4
Merge branch 'main' of github.com:MetaMask/core into MCA-83
dovydas55 3dbc9af
feat: fixing tests
dovydas55 d534083
Merge branch 'main' of github.com:MetaMask/core into MCA-83
dovydas55 f2dd2d3
feat: added simple README
dovydas55 803f104
feat: added simple README
dovydas55 d415546
feat: added simple README
dovydas55 00707c6
feat: fixing changelog
dovydas55 3800ca8
feat: fixing linter
dovydas55 4d0d255
feat: fixing linter
dovydas55 c70f646
feat: fixing linter
dovydas55 d81b087
Merge branch 'main' into MCA-83
dovydas55 d319cb3
feat: updating docs
dovydas55 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| # Changelog | ||
|
|
||
| All notable changes to this project will be documented in this file. | ||
|
|
||
| The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), | ||
| and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). | ||
|
|
||
| ## [Unreleased] | ||
|
|
||
| ### Added | ||
|
|
||
| - Initial release ([#8260](https://github.com/MetaMask/core/pull/8260)) | ||
| - `AuthenticatedUserStorage` class with namespaced domain accessors: `delegations` (list, create, revoke) and `preferences` (getNotifications, putNotifications) | ||
|
|
||
| [Unreleased]: https://github.com/MetaMask/core/ |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| MIT License | ||
|
|
||
| Copyright (c) 2026 MetaMask | ||
|
|
||
| Permission is hereby granted, free of charge, to any person obtaining a copy | ||
| of this software and associated documentation files (the "Software"), to deal | ||
| in the Software without restriction, including without limitation the rights | ||
| to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
| copies of the Software, and to permit persons to whom the Software is | ||
| furnished to do so, subject to the following conditions: | ||
|
|
||
| The above copyright notice and this permission notice shall be included in all | ||
| copies or substantial portions of the Software. | ||
|
|
||
| THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
| IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
| FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
| AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
| LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
| OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,115 @@ | ||
| # `@metamask/authenticated-user-storage` | ||
|
|
||
| A TypeScript SDK for MetaMask's Authenticated User Storage API. Unlike E2EE user-storage, authenticated user storage holds **structured JSON** scoped to the authenticated user. The server can read and validate the contents, which allows other backend services to consume the data (e.g. delegation execution, notification delivery). | ||
|
|
||
| The SDK currently supports two domains: | ||
|
|
||
| - **Delegations** -- immutable, EIP-712 signed delegation records (list, create, revoke). | ||
| - **Notification Preferences** -- mutable per-user notification settings (get, put). | ||
|
|
||
| ## Installation | ||
|
|
||
| `yarn add @metamask/authenticated-user-storage` | ||
|
|
||
| or | ||
|
|
||
| `npm install @metamask/authenticated-user-storage` | ||
|
|
||
| ## Usage | ||
|
|
||
| ### Creating a client | ||
|
|
||
| The constructor requires two options: | ||
|
|
||
| - **`env`** -- selects the backend environment (`DEV`, `UAT`, or `PRD`). | ||
| - **`getAccessToken`** -- an async callback that returns a valid JWT access token for the current user. In MetaMask clients this is wired through the messenger to `AuthenticationController:getBearerToken`, which handles the full SRP-based OIDC login flow internally. | ||
|
|
||
| ```typescript | ||
| import { | ||
| AuthenticatedUserStorage, | ||
| Env, | ||
| } from '@metamask/authenticated-user-storage'; | ||
|
|
||
| // Inside a controller that has access to the messenger: | ||
| const storage = new AuthenticatedUserStorage({ | ||
| env: Env.PRD, | ||
| getAccessToken: () => | ||
| this.messenger.call('AuthenticationController:getBearerToken'), | ||
| }); | ||
| ``` | ||
|
|
||
| The `env` option selects the backend environment: | ||
|
|
||
| | `Env` value | Server | | ||
| | ----------- | ------------------------------------- | | ||
| | `Env.DEV` | `user-storage.dev-api.cx.metamask.io` | | ||
| | `Env.UAT` | `user-storage.uat-api.cx.metamask.io` | | ||
| | `Env.PRD` | `user-storage.api.cx.metamask.io` | | ||
|
|
||
| The `AuthenticationController` manages the full authentication lifecycle (SRP key derivation, nonce signing, backend authentication, OIDC token exchange, and session caching). Callers do not need to handle tokens directly -- the `getBearerToken` action returns a cached access token or transparently re-authenticates when the session has expired. | ||
|
|
||
| ### Delegations | ||
|
|
||
| Delegations are immutable once stored. They can only be revoked (deleted), not updated. | ||
|
|
||
| ```typescript | ||
| import type { Hex, DelegationSubmission } from '@metamask/authenticated-user-storage'; | ||
|
|
||
| // List all delegations for the authenticated user | ||
| const delegations = await storage.delegations.list(); | ||
|
|
||
| // Submit a new signed delegation | ||
| const submission: DelegationSubmission = { | ||
| signedDelegation: { ... }, | ||
| metadata: { ... }, | ||
| }; | ||
| await storage.delegations.create(submission, 'extension'); | ||
|
|
||
| // Revoke a delegation by its hash | ||
| await storage.delegations.revoke('0xdae6d1...'); | ||
| ``` | ||
|
|
||
| ### Notification preferences | ||
|
|
||
| Preferences are mutable. The first call creates the record; subsequent calls update it. | ||
|
|
||
| ```typescript | ||
| import type { NotificationPreferences, Hex } from '@metamask/authenticated-user-storage'; | ||
|
|
||
| // Retrieve current preferences (returns null if none have been set) | ||
| const prefs = await storage.preferences.getNotifications(); | ||
|
|
||
| // Create or update preferences | ||
| const updated: NotificationPreferences = { | ||
| walletActivity: { ... }, | ||
| marketing: { ... }, | ||
| perps: { ... }, | ||
| socialAI: { ... }, | ||
| }; | ||
| await storage.preferences.putNotifications(updated, 'extension'); | ||
| ``` | ||
|
|
||
| ## Response validation | ||
|
|
||
| All API responses are validated at runtime using [`@metamask/superstruct`](https://github.com/MetaMask/superstruct) schemas before being returned to callers. If the server returns data that doesn't match the expected shape, the SDK throws an `AuthenticatedUserStorageError` with details about the structural mismatch rather than silently returning malformed data. | ||
|
|
||
| ## Error handling | ||
|
|
||
| All methods throw `AuthenticatedUserStorageError` on failure. This covers HTTP errors, response validation failures, and network issues. The error message includes the HTTP status code and the server's error response when available. | ||
|
|
||
| ```typescript | ||
| import { AuthenticatedUserStorageError } from '@metamask/authenticated-user-storage'; | ||
|
|
||
| try { | ||
| await storage.delegations.create(submission); | ||
| } catch (error) { | ||
| if (error instanceof AuthenticatedUserStorageError) { | ||
| console.error(error.message); | ||
| // e.g. "failed to create delegation. HTTP 409 message: delegation already exists, error: Conflict" | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| ## Contributing | ||
|
|
||
| This package is part of a monorepo. Instructions for contributing can be found in the [monorepo README](https://github.com/MetaMask/core#readme). |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| /* | ||
| * For a detailed explanation regarding each configuration property and type check, visit: | ||
| * https://jestjs.io/docs/configuration | ||
| */ | ||
|
|
||
| const merge = require('deepmerge'); | ||
| const path = require('path'); | ||
|
|
||
| const baseConfig = require('../../jest.config.packages'); | ||
|
|
||
| const displayName = path.basename(__dirname); | ||
|
|
||
| module.exports = merge(baseConfig, { | ||
| // The display name when running multiple projects | ||
| displayName, | ||
|
|
||
| // An object that configures minimum threshold enforcement for coverage results | ||
| coverageThreshold: { | ||
| global: { | ||
| branches: 50, | ||
| functions: 50, | ||
| lines: 50, | ||
| statements: 50, | ||
| }, | ||
| }, | ||
|
|
||
| coveragePathIgnorePatterns: [ | ||
| ...baseConfig.coveragePathIgnorePatterns, | ||
| '/__fixtures__/', | ||
| '/mocks/', | ||
| 'index.ts', | ||
| ], | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,71 @@ | ||
| { | ||
| "name": "@metamask/authenticated-user-storage", | ||
| "version": "0.0.0", | ||
| "description": "SDK for authenticated (non-encrypted) user storage endpoints", | ||
| "keywords": [ | ||
| "MetaMask", | ||
| "Ethereum" | ||
| ], | ||
| "homepage": "https://github.com/MetaMask/core/tree/main/packages/authenticated-user-storage#readme", | ||
| "bugs": { | ||
| "url": "https://github.com/MetaMask/core/issues" | ||
| }, | ||
| "repository": { | ||
| "type": "git", | ||
| "url": "https://github.com/MetaMask/core.git" | ||
| }, | ||
| "license": "MIT", | ||
| "sideEffects": false, | ||
| "exports": { | ||
| ".": { | ||
| "import": { | ||
| "types": "./dist/index.d.mts", | ||
| "default": "./dist/index.mjs" | ||
| }, | ||
| "require": { | ||
| "types": "./dist/index.d.cts", | ||
| "default": "./dist/index.cjs" | ||
| } | ||
| }, | ||
| "./package.json": "./package.json" | ||
| }, | ||
| "main": "./dist/index.cjs", | ||
| "types": "./dist/index.d.cts", | ||
| "files": [ | ||
| "dist/" | ||
| ], | ||
| "scripts": { | ||
| "build": "ts-bridge --project tsconfig.build.json --verbose --clean --no-references", | ||
| "build:all": "ts-bridge --project tsconfig.build.json --verbose --clean", | ||
| "build:docs": "typedoc", | ||
| "changelog:update": "../../scripts/update-changelog.sh @metamask/authenticated-user-storage", | ||
| "changelog:validate": "../../scripts/validate-changelog.sh @metamask/authenticated-user-storage", | ||
| "since-latest-release": "../../scripts/since-latest-release.sh", | ||
| "test": "NODE_OPTIONS=--experimental-vm-modules jest --reporters=jest-silent-reporter", | ||
| "test:clean": "NODE_OPTIONS=--experimental-vm-modules jest --clearCache", | ||
| "test:verbose": "NODE_OPTIONS=--experimental-vm-modules jest --verbose", | ||
| "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch" | ||
| }, | ||
| "dependencies": { | ||
| "@metamask/superstruct": "^3.1.0" | ||
| }, | ||
| "devDependencies": { | ||
| "@metamask/auto-changelog": "^3.4.4", | ||
| "@ts-bridge/cli": "^0.6.4", | ||
| "@types/jest": "^29.5.14", | ||
| "deepmerge": "^4.2.2", | ||
| "jest": "^29.7.0", | ||
| "nock": "^13.3.1", | ||
| "ts-jest": "^29.2.5", | ||
| "typedoc": "^0.25.13", | ||
| "typedoc-plugin-missing-exports": "^2.0.0", | ||
| "typescript": "~5.3.3" | ||
| }, | ||
| "engines": { | ||
| "node": "^18.18 || >=20" | ||
| }, | ||
| "publishConfig": { | ||
| "access": "public", | ||
| "registry": "https://registry.npmjs.org/" | ||
| } | ||
| } |
75 changes: 75 additions & 0 deletions
75
packages/authenticated-user-storage/src/__fixtures__/authenticated-userstorage.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,75 @@ | ||
| import nock from 'nock'; | ||
|
|
||
| import { | ||
| MOCK_DELEGATIONS_URL, | ||
| MOCK_DELEGATION_RESPONSE, | ||
| MOCK_NOTIFICATION_PREFERENCES, | ||
| MOCK_NOTIFICATION_PREFERENCES_URL, | ||
| } from '../mocks/authenticated-userstorage'; | ||
|
|
||
| type MockReply = { | ||
| status: nock.StatusCode; | ||
| body?: nock.Body; | ||
| }; | ||
|
|
||
| export function handleMockListDelegations(mockReply?: MockReply): nock.Scope { | ||
| const reply = mockReply ?? { | ||
| status: 200, | ||
| body: [MOCK_DELEGATION_RESPONSE], | ||
| }; | ||
| return nock(MOCK_DELEGATIONS_URL) | ||
| .persist() | ||
| .get('') | ||
| .reply(reply.status, reply.body); | ||
| } | ||
|
|
||
| export function handleMockCreateDelegation( | ||
| mockReply?: MockReply, | ||
| callback?: (uri: string, requestBody: nock.Body) => Promise<void>, | ||
| ): nock.Scope { | ||
| const reply = mockReply ?? { status: 200 }; | ||
| const interceptor = nock(MOCK_DELEGATIONS_URL).persist().post(''); | ||
|
|
||
| if (callback) { | ||
| return interceptor.reply(reply.status, async (uri, requestBody) => { | ||
| await callback(uri, requestBody); | ||
| }); | ||
| } | ||
| return interceptor.reply(reply.status, reply.body); | ||
| } | ||
dovydas55 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| export function handleMockRevokeDelegation(mockReply?: MockReply): nock.Scope { | ||
| const reply = mockReply ?? { status: 204 }; | ||
| return nock(MOCK_DELEGATIONS_URL) | ||
| .persist() | ||
| .delete(/.*/u) | ||
| .reply(reply.status, reply.body); | ||
| } | ||
|
|
||
| export function handleMockGetNotificationPreferences( | ||
| mockReply?: MockReply, | ||
| ): nock.Scope { | ||
| const reply = mockReply ?? { | ||
| status: 200, | ||
| body: MOCK_NOTIFICATION_PREFERENCES, | ||
| }; | ||
| return nock(MOCK_NOTIFICATION_PREFERENCES_URL) | ||
| .persist() | ||
| .get('') | ||
| .reply(reply.status, reply.body); | ||
| } | ||
|
|
||
| export function handleMockPutNotificationPreferences( | ||
| mockReply?: MockReply, | ||
| callback?: (uri: string, requestBody: nock.Body) => Promise<void>, | ||
| ): nock.Scope { | ||
| const reply = mockReply ?? { status: 200 }; | ||
| const interceptor = nock(MOCK_NOTIFICATION_PREFERENCES_URL).persist().put(''); | ||
|
|
||
| if (callback) { | ||
| return interceptor.reply(reply.status, async (uri, requestBody) => { | ||
| await callback(uri, requestBody); | ||
| }); | ||
| } | ||
| return interceptor.reply(reply.status, reply.body); | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.