├── .eslintignore
├── .eslintrc
├── .github
├── CODEOWNERS
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ ├── feature_request.md
│ └── question.md
├── PULL_REQUEST_TEMPLATE.md
└── workflows
│ ├── publish.yml
│ └── test.yml
├── .gitignore
├── .nvmrc
├── .prettierrc.js
├── CHANGELOG.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── config
├── tsconfig.base.json
├── tsconfig.sdk.cjs.json
├── tsconfig.sdk.esm.json
└── tsconfig.test.json
├── jest.config.ts
├── package.json
├── scripts
├── build.sh
├── start.sh
└── test.sh
├── src
├── core
│ ├── sdk-exceptions.ts
│ └── sdk.ts
├── index.ts
├── modules
│ ├── base-module.ts
│ ├── token
│ │ └── index.ts
│ ├── users
│ │ └── index.ts
│ └── utils
│ │ ├── index.ts
│ │ └── ownershipABIs.ts
├── types
│ ├── didt-types.ts
│ ├── exception-types.ts
│ ├── index.ts
│ ├── sdk-types.ts
│ ├── utils-types.ts
│ └── wallet-types.ts
└── utils
│ ├── codec.ts
│ ├── ec-recover.ts
│ ├── fetch.ts
│ ├── issuer.ts
│ ├── parse-didt.ts
│ ├── rest.ts
│ └── type-guards.ts
├── test
├── lib
│ ├── constants.ts
│ └── factories.ts
├── spec
│ ├── core
│ │ ├── sdk-exceptions
│ │ │ ├── error-factories.spec.ts
│ │ │ └── magic-admin-sdk-error
│ │ │ │ └── constructor.spec.ts
│ │ └── sdk
│ │ │ └── constructor.spec.ts
│ ├── index.spec.ts
│ ├── modules
│ │ ├── base-module
│ │ │ └── constructor.spec.ts
│ │ ├── token
│ │ │ ├── decode.spec.ts
│ │ │ ├── getIssuer.spec.ts
│ │ │ ├── getPublicAddress.spec.ts
│ │ │ └── validate.spec.ts
│ │ ├── users
│ │ │ ├── getMetadataByIssuer.spec.ts
│ │ │ ├── getMetadataByPublicAddress.spec.ts
│ │ │ ├── getMetadataByToken.spec.ts
│ │ │ ├── logoutByIssuer.spec.ts
│ │ │ ├── logoutByPublicAddress.spec.ts
│ │ │ └── logoutByToken.spec.ts
│ │ └── utils
│ │ │ ├── parseAuthorizationHeader.spec.ts
│ │ │ └── validateTokenOwnership.spec.ts
│ └── utils
│ │ ├── codec.spec.ts
│ │ ├── issuer
│ │ ├── generateIssuerFromPublicAddress.spec.ts
│ │ └── parsePublicAddressFromIssuer.spec.ts
│ │ ├── parse-didt.spec.ts
│ │ ├── rest
│ │ ├── emitRequest.spec.ts
│ │ ├── get.spec.ts
│ │ └── post.spec.ts
│ │ └── type-guards
│ │ └── isDIDTClaim.spec.ts
└── tsconfig.json
├── tsconfig.json
└── yarn.lock
/.eslintignore:
--------------------------------------------------------------------------------
1 | jest.config.ts
2 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["@ikscodes/eslint-config"],
3 | "parserOptions": {
4 | "project": [
5 | "./tsconfig.json",
6 | "./test/tsconfig.json"
7 | ]
8 | },
9 | "rules": {
10 | "import/extensions": 0,
11 | "no-alert": 0,
12 | "@typescript-eslint/await-thenable": 0,
13 | "react/button-has-type": 0,
14 | "no-cond-assign": 0,
15 | "class-methods-use-this": 0,
16 | "no-underscore-dangle": 0,
17 | "no-useless-constructor": 0,
18 | // Note: you must disable the base rule as it can report incorrect errors
19 | "no-shadow": 0,
20 | "@typescript-eslint/no-shadow": "warn",
21 | "no-empty-function": 0
22 | },
23 | "settings": {
24 | "import/resolver": {
25 | "typescript": {
26 | "directory": [
27 | "./tsconfig.json",
28 | "./test/tsconfig.json"
29 | ]
30 | }
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | # Codeowners used to ensure Magic full-time employees review all PRs
2 | * @magiclabs/magic-engineering
3 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug Report
3 | about: Use this template to report a bug.
4 | title: "[DESCRIPTIVE BUG NAME]"
5 | labels: 🐛 Bug Report, 🔍 Needs Triage
6 | ---
7 |
8 | ### ✅ Prerequisites
9 |
10 | - [ ] Did you perform a cursory search of open issues? Is this bug already reported elsewhere?
11 | - [ ] Are you running the latest SDK version?
12 | - [ ] Are you reporting to the correct repository (`@magic-sdk/admin`)?
13 |
14 | ### 🐛 Description
15 |
16 | [Description of the bug.]
17 |
18 | ### 🧩 Steps to Reproduce
19 |
20 | 1. [First Step]
21 | 2. [Second Step]
22 | 3. [and so on...]
23 |
24 | ### 🤔 Expected behavior
25 |
26 | [What you expected to happen?]
27 |
28 | ### 😮 Actual behavior
29 |
30 | [What actually happened? Please include any error stack traces you encounter.]
31 |
32 | ### 💻 Code Sample
33 |
34 | [If possible, please provide a code repository, gist, code snippet or sample files to reproduce the issue.]
35 |
36 | ### 🌎 Environment
37 |
38 | | Software | Version(s) |
39 | | ------------------ | ---------- |
40 | | `@magic-sdk/admin` |
41 | | Node |
42 | | `yarn` |
43 | | Operating System |
44 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature Request
3 | about: Use this template to request a new feature.
4 | title: "[DESCRIPTIVE FEATURE NAME]"
5 | labels: ✨Feature Request
6 | ---
7 |
8 | ### ✅ Prerequisites
9 |
10 | - [ ] Did you perform a cursory search of open issues? Is this feature already requested elsewhere?
11 | - [ ] Are you reporting to the correct repository (`@magic-sdk/admin`)?
12 |
13 | ### ✨ Feature Request
14 |
15 | [Description of the feature.]
16 |
17 | ## 🧩 Context
18 |
19 | [Explain any additional context or rationale for this feature. What are you trying to accomplish?]
20 |
21 | ## 💻 Examples
22 |
23 | [Do you have any example(s) for the requested feature? If so, describe/demonstrate your example(s) here.]
24 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/question.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Question
3 | about: Use this template to request help or ask a question.
4 | title: "[WHAT'S YOUR QUESTION?]"
5 | labels: ❓Question
6 | ---
7 |
8 | ### ✅ Prerequisites
9 |
10 | - [ ] Did you perform a cursory search of open issues? Is this question already asked elsewhere?
11 | - [ ] Are you reporting to the correct repository (`@magic-sdk/admin`)?
12 |
13 | ### ❓ Question
14 |
15 | [Ask your question here, please be as detailed as possible!]
16 |
17 | ### 🌎 Environment
18 |
19 | | Software | Version(s) |
20 | | ------------------ | ---------- |
21 | | `@magic-sdk/admin` |
22 | | Node |
23 | | `yarn` |
24 | | Operating System |
25 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | ### 📦 Pull Request
2 |
3 |
4 |
5 | ### ✅ Fixed Issues
6 |
7 |
8 |
9 | ### 🚨 Test instructions
10 |
11 |
12 |
13 | ### ⚠️ Don't forget to add a [semver](https://semver.org/) label!
14 |
20 |
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | name: Publish
2 | on:
3 | push:
4 | branches:
5 | - "master"
6 |
7 | jobs:
8 | publish:
9 | runs-on: ubuntu-20.04
10 | name: Publish
11 | if: "!contains(github.event.head_commit.message, 'ci skip') && !contains(github.event.head_commit.message, 'skip ci')"
12 | steps:
13 | - uses: actions/checkout@v3
14 | with:
15 | token: ${{ secrets.ADMIN_TOKEN }}
16 |
17 | - name: Prepare repository
18 | run: git fetch --unshallow --tags
19 |
20 | - name: Setup node
21 | uses: actions/setup-node@v3
22 | with:
23 | node-version: 20
24 | cache: 'yarn'
25 | - run: |
26 | yarn -v
27 | yarn
28 |
29 | - name: Create Release
30 | env:
31 | GITHUB_TOKEN: ${{ secrets.ADMIN_TOKEN }}
32 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
33 | run: |
34 | yarn
35 | yarn build
36 | yarn auto shipit
37 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: Tests
2 | concurrency:
3 | group: tests-${{ github.ref }}
4 | cancel-in-progress: true
5 | on:
6 | push:
7 | branches:
8 | - "master"
9 | pull_request:
10 |
11 | jobs:
12 | test:
13 | runs-on: ubuntu-20.04
14 | name: Run style/security checks & tests
15 | container:
16 | image: node:16-slim
17 | steps:
18 | - uses: actions/checkout@v3
19 |
20 | - name: Setup node
21 | uses: actions/setup-node@v3
22 | with:
23 | node-version: 16
24 | cache: 'yarn'
25 | - run: |
26 | yarn -v
27 | yarn
28 |
29 | - name: Lint
30 | run: yarn run lint
31 |
32 | - name: Audit Prod Dependencies
33 | run: yarn audit --groups dependencies || true
34 |
35 | - name: Build
36 | run: yarn run build
37 |
38 | - name: Test
39 | run: yarn run test
40 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # Dependencies
4 | /node_modules
5 | /dist
6 |
7 | # Misc
8 | /.idea
9 | .DS_Store
10 | npm-debug.log*
11 | yarn-error.log*
12 | .DS_Store
13 | .vscode
14 |
15 | # Use Yarn!
16 | package-lock.json
17 |
18 | # Test artifacts
19 | /coverage
20 | /.nyc_output
21 |
22 | # ENV
23 | .env
--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------
1 | 20.10
2 |
--------------------------------------------------------------------------------
/.prettierrc.js:
--------------------------------------------------------------------------------
1 | module.exports = require('@ikscodes/prettier-config');
2 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # v2.4.1 (Tue Apr 23 2024)
2 |
3 | #### 🐛 Bug Fix
4 |
5 | - feat: add username data field to metadata call response [#127](https://github.com/magiclabs/magic-admin-js/pull/127) ([@am-hernandez](https://github.com/am-hernandez))
6 |
7 | #### Authors: 1
8 |
9 | - A.M. Hernandez ([@am-hernandez](https://github.com/am-hernandez))
10 |
11 | ---
12 |
13 | # v2.4.0 (Thu Apr 04 2024)
14 |
15 | #### 🚀 Enhancement
16 |
17 | - Fixing ABIs [#126](https://github.com/magiclabs/magic-admin-js/pull/126) ([@bengriffin1](https://github.com/bengriffin1))
18 |
19 | #### Authors: 1
20 |
21 | - Ben Griffin ([@bengriffin1](https://github.com/bengriffin1))
22 |
23 | ---
24 |
25 | # v2.3.0 (Thu Apr 04 2024)
26 |
27 | #### 🚀 Enhancement
28 |
29 | - Updating yarn.lock [#125](https://github.com/magiclabs/magic-admin-js/pull/125) ([@bengriffin1](https://github.com/bengriffin1))
30 | - Switching to use ethers and accept RPC url for token gating validation [#124](https://github.com/magiclabs/magic-admin-js/pull/124) ([@bengriffin1](https://github.com/bengriffin1) [@Ethella](https://github.com/Ethella))
31 |
32 | #### Authors: 2
33 |
34 | - Ben Griffin ([@bengriffin1](https://github.com/bengriffin1))
35 | - Jerry Liu ([@Ethella](https://github.com/Ethella))
36 |
37 | ---
38 |
39 | # v2.2.0 (Wed Apr 03 2024)
40 |
41 | #### 🚀 Enhancement
42 |
43 | - Adding type and abis [#122](https://github.com/magiclabs/magic-admin-js/pull/122) ([@bengriffin1](https://github.com/bengriffin1) [@Ethella](https://github.com/Ethella))
44 |
45 | #### 🐛 Bug Fix
46 |
47 | - Pdeexp 594 update admin sdk dependency versions to support web 3 [#123](https://github.com/magiclabs/magic-admin-js/pull/123) ([@Ethella](https://github.com/Ethella))
48 |
49 | #### Authors: 2
50 |
51 | - Ben Griffin ([@bengriffin1](https://github.com/bengriffin1))
52 | - Jerry Liu ([@Ethella](https://github.com/Ethella))
53 |
54 | ---
55 |
56 | # v2.1.0 (Fri Jan 19 2024)
57 |
58 | #### 🚀 Enhancement
59 |
60 | - Fix ESM bundle generation and imports [#119](https://github.com/magiclabs/magic-admin-js/pull/119) ([@romin-halltari](https://github.com/romin-halltari))
61 |
62 | #### Authors: 1
63 |
64 | - [@romin-halltari](https://github.com/romin-halltari)
65 |
66 | ---
67 |
68 | # v2.0.1 (Wed Oct 04 2023)
69 |
70 | #### 🐛 Bug Fix
71 |
72 | - update yarn.lock [#117](https://github.com/magiclabs/magic-admin-js/pull/117) ([@Ethella](https://github.com/Ethella))
73 | - Remove dependencies of atob [#116](https://github.com/magiclabs/magic-admin-js/pull/116) ([@Ethella](https://github.com/Ethella))
74 |
75 | #### Authors: 1
76 |
77 | - Jerry Liu ([@Ethella](https://github.com/Ethella))
78 |
79 | ---
80 |
81 | # v2.0.0 (Mon Jul 10 2023)
82 |
83 | #### 💥 Breaking Change
84 |
85 | - Validate 'aud' in DID Token [#111](https://github.com/magiclabs/magic-admin-js/pull/111) ([@magic-ravi](https://github.com/magic-ravi))
86 |
87 | #### Authors: 1
88 |
89 | - Ravi Bhankharia ([@magic-ravi](https://github.com/magic-ravi))
90 |
91 | ---
92 |
93 | # v2.0.0 (July 10, 2023)
94 |
95 | ## Summary
96 | - 🚀 **Added:** Magic Connect developers can now use the Admin SDK to validate DID tokens. [#111](https://github.com/magiclabs/magic-admin-js/pull/111) ([@magic-ravi](https://github.com/magic-ravi))
97 | - ⚠️ **Changed:** After creating the Magic instance, it is now necessary to call a new initialize method for Magic Connect developers that want to utilize the Admin SDK. [#111](https://github.com/magiclabs/magic-admin-js/pull/111) ([@magic-ravi](https://github.com/magic-ravi))
98 | - 🛡️ **Security:** Additional validation of `aud` (client ID) is now being done during initialization of the SDK. [#111](https://github.com/magiclabs/magic-admin-js/pull/111) ([@magic-ravi](https://github.com/magic-ravi))
99 |
100 | ## Developer Notes
101 |
102 | ### 🚀 Added
103 |
104 | #### Admin SDK for MC
105 | Magic Connect developers can now use the Admin SDK to validate DID tokens.
106 |
107 | **Details**
108 | There is full support for all `TokenResource` SDK methods for MC. This is intended to be used with client side `magic-js` SDK which will now emit an `id-token-created` event with a DID token upon login via the `connectWithUI` method.
109 |
110 | This functionality is replicated on our other SDKs on Python and Ruby.
111 |
112 | ### ⚠️ Changed
113 |
114 | #### Constructor initialization
115 |
116 | The existing constructor has been deprecated in place of a new async `init` method.
117 | The `init` method will pull clientId from Magic servers if one is not provided in the `options` parameter.
118 |
119 | **Previous Version**
120 | ```javascript
121 | const magic = new Magic(secretKey);
122 | try {
123 | magic.token.validate(DIDT);
124 | } catch (e) {
125 | console.log(e);
126 | }
127 | try {
128 | await magic.users.getMetadataByToken(DIDT);
129 | } catch (e) {
130 | console.log(e);
131 | }
132 | ```
133 |
134 | **Current Version**
135 | ```javascript
136 | const magic = await Magic.init(mcSecretKey);
137 | try {
138 | magic.token.validate(DIDT);
139 | } catch (e) {
140 | console.log(e);
141 | }
142 | try {
143 | await magic.users.getMetadataByToken(DIDT);
144 | } catch (e) {
145 | console.log(e);
146 | }
147 | ```
148 |
149 | #### Attachment Validation
150 |
151 | - Skip validation of attachment if 'none' is passed in `validate`.
152 |
153 | ### 🛡️ Security
154 |
155 | #### Client ID Validation
156 |
157 | Additional validation of `aud` (client ID) is now being done during initialization of the SDK. This is for both Magic Connect and Magic Auth developers.
158 |
159 |
160 | ### 🚨 Breaking
161 |
162 | None, all changes are fully backwards compatiable.
163 |
164 | ### Authors: 1
165 |
166 | - Ravi Bhankharia ([@magic-ravi](https://github.com/magic-ravi))
167 |
168 | # v1.10.1 (Fri Jul 07 2023)
169 |
170 | #### 🐛 Bug Fix
171 |
172 | - * add file extension [#112](https://github.com/magiclabs/magic-admin-js/pull/112) ([@Ethella](https://github.com/Ethella))
173 |
174 | #### Authors: 1
175 |
176 | - Jerry Liu ([@Ethella](https://github.com/Ethella))
177 |
178 | ---
179 |
180 | # v1.10.0 (Wed May 03 2023)
181 |
182 | #### 🚀 Enhancement
183 |
184 | - [FIX]: removing unneeded NFT module and related code [#108](https://github.com/magiclabs/magic-admin-js/pull/108) ([@bengriffin1](https://github.com/bengriffin1))
185 |
186 | #### Authors: 1
187 |
188 | - Ben Griffin ([@bengriffin1](https://github.com/bengriffin1))
189 |
190 | ---
191 |
192 | # v1.9.2 (Fri Mar 24 2023)
193 |
194 | #### 🐛 Bug Fix
195 |
196 | - [FIX]: adding json header [#107](https://github.com/magiclabs/magic-admin-js/pull/107) ([@bengriffin1](https://github.com/bengriffin1))
197 |
198 | #### Authors: 1
199 |
200 | - Ben Griffin ([@bengriffin1](https://github.com/bengriffin1))
201 |
202 | ---
203 |
204 | # v1.9.1 (Fri Mar 24 2023)
205 |
206 | #### 🐛 Bug Fix
207 |
208 | - [FIX]: updating response from API server to fit new schema [#106](https://github.com/magiclabs/magic-admin-js/pull/106) ([@bengriffin1](https://github.com/bengriffin1))
209 |
210 | #### Authors: 1
211 |
212 | - Ben Griffin ([@bengriffin1](https://github.com/bengriffin1))
213 |
214 | ---
215 |
216 | # v1.9.1 (Wed Mar 22 2023)
217 |
218 | #### 🐛 Bug Fix
219 |
220 | - Update modules/mint/index.ts to account for new API response format [#106](https://github.com/magiclabs/magic-admin-js/pull/106) ([@bengriffin1](https://github.com/bengriffin1))
221 |
222 | #### Authors: 1
223 |
224 | - Ben Griffin ([@bengriffin1](https://github.com/bengriffin1))
225 |
226 | ---
227 |
228 | # v1.9.0 (Wed Mar 22 2023)
229 |
230 | #### 🚀 Enhancement
231 |
232 | - Adding mint module for minting NFTs [#105](https://github.com/magiclabs/magic-admin-js/pull/105) ([@bengriffin1](https://github.com/bengriffin1))
233 |
234 | #### 🐛 Bug Fix
235 |
236 | - Update CHANGELOG.md [#104](https://github.com/magiclabs/magic-admin-js/pull/104) ([@justinnout](https://github.com/justinnout))
237 |
238 | #### Authors: 2
239 |
240 | - Ben Griffin ([@bengriffin1](https://github.com/bengriffin1))
241 | - Justin Herrera ([@justinnout](https://github.com/justinnout))
242 |
243 | ---
244 |
245 | # v1.9.0 (Mon Mar 20 2023)
246 |
247 | #### 🚀 Enhancement
248 |
249 | - New module for NFT minting + delivery [#105](https://github.com/magiclabs/magic-admin-js/pull/105) ([@bengriffin1](https://github.com/bengriffin1))
250 |
251 | #### Authors: 1
252 |
253 | - Ben Griffin ([@bengriffin1](https://github.com/bengriffin1))
254 |
255 | ---
256 | # v1.8.0 (Fri Feb 17 2023)
257 |
258 | #### 🚀 Enhancement
259 |
260 | - Support new multi-chain wallet functionality [#100](https://github.com/magiclabs/magic-admin-js/pull/100) ([@justinnout](https://github.com/justinnout))
261 |
262 | #### Authors: 1
263 |
264 | - Justin Herrera ([@justinnout](https://github.com/justinnout))
265 |
266 | ---
267 | # v1.4.1 (Sat Apr 16 2022)
268 |
269 | #### 🐛 Bug Fix
270 |
271 | - Add shims for atob [#88](https://github.com/magiclabs/magic-admin-js/pull/88) ([@Ethella](https://github.com/Ethella))
272 |
273 | #### Authors: 1
274 |
275 | - Jerry Liu ([@Ethella](https://github.com/Ethella))
276 |
277 | ---
278 |
279 | # v1.4.0 (Tue Mar 29 2022)
280 |
281 | #### 🚀 Enhancement
282 |
283 | - feature: make package environment agnostic [#81](https://github.com/magiclabs/magic-admin-js/pull/81) ([@f5io](https://github.com/f5io))
284 |
285 | #### ⚠️ Pushed to `master`
286 |
287 | - Update CircleCI to use Node 16 ([@smithki](https://github.com/smithki))
288 |
289 | #### 🏠 Internal
290 |
291 | - Converted all Ava and Sinon testing to Jest [#83](https://github.com/magiclabs/magic-admin-js/pull/83) ([@mushfichowdhury-magic](https://github.com/mushfichowdhury-magic))
292 |
293 | #### Authors: 3
294 |
295 | - [@mushfichowdhury-magic](https://github.com/mushfichowdhury-magic)
296 | - Ian K Smith ([@smithki](https://github.com/smithki))
297 | - Joe Harlow ([@f5io](https://github.com/f5io))
298 |
299 | ---
300 |
301 | # v1.3.4 (Thu Dec 23 2021)
302 |
303 | #### 🐛 Bug Fix
304 |
305 | - Fixed broken docs link in README [#72](https://github.com/magiclabs/magic-admin-js/pull/72) ([@lukecarr](https://github.com/lukecarr) [@smithki](https://github.com/smithki))
306 |
307 | #### 🔩 Dependency Updates
308 |
309 | - Bump path-parse from 1.0.6 to 1.0.7 [#63](https://github.com/magiclabs/magic-admin-js/pull/63) ([@dependabot[bot]](https://github.com/dependabot[bot]) [@smithki](https://github.com/smithki))
310 | - Bump glob-parent from 5.1.0 to 5.1.2 [#61](https://github.com/magiclabs/magic-admin-js/pull/61) ([@dependabot[bot]](https://github.com/dependabot[bot]) [@smithki](https://github.com/smithki))
311 | - Bump normalize-url from 4.5.0 to 4.5.1 [#60](https://github.com/magiclabs/magic-admin-js/pull/60) ([@dependabot[bot]](https://github.com/dependabot[bot]) [@smithki](https://github.com/smithki))
312 | - Bump hosted-git-info from 2.8.7 to 2.8.9 [#59](https://github.com/magiclabs/magic-admin-js/pull/59) ([@dependabot[bot]](https://github.com/dependabot[bot]) [@smithki](https://github.com/smithki))
313 | - Bump lodash from 4.17.19 to 4.17.21 [#58](https://github.com/magiclabs/magic-admin-js/pull/58) ([@dependabot[bot]](https://github.com/dependabot[bot]) [@smithki](https://github.com/smithki))
314 | - Bump gitlog from 4.0.3 to 4.0.4 [#57](https://github.com/magiclabs/magic-admin-js/pull/57) ([@dependabot[bot]](https://github.com/dependabot[bot]) [@smithki](https://github.com/smithki))
315 | - Bump y18n from 4.0.0 to 4.0.1 [#56](https://github.com/magiclabs/magic-admin-js/pull/56) ([@dependabot[bot]](https://github.com/dependabot[bot]) [@smithki](https://github.com/smithki))
316 | - Bump elliptic from 6.5.3 to 6.5.4 [#53](https://github.com/magiclabs/magic-admin-js/pull/53) ([@dependabot[bot]](https://github.com/dependabot[bot]) [@smithki](https://github.com/smithki))
317 | - Bump ini from 1.3.5 to 1.3.8 [#51](https://github.com/magiclabs/magic-admin-js/pull/51) ([@dependabot[bot]](https://github.com/dependabot[bot]) [@smithki](https://github.com/smithki))
318 |
319 | #### Authors: 3
320 |
321 | - [@dependabot[bot]](https://github.com/dependabot[bot])
322 | - Ian K Smith ([@smithki](https://github.com/smithki))
323 | - Luke Carr ([@lukecarr](https://github.com/lukecarr))
324 |
325 | ---
326 |
327 | # v1.3.3 (Thu Dec 23 2021)
328 |
329 | #### 🐛 Bug Fix
330 |
331 | - getMetadata calls now return phone number [#74](https://github.com/magiclabs/magic-admin-js/pull/74) ([@hcote](https://github.com/hcote))
332 |
333 | #### Authors: 1
334 |
335 | - Hunter Cote ([@hcote](https://github.com/hcote))
336 |
337 | ---
338 |
339 | # v1.3.2 (Fri Nov 12 2021)
340 |
341 | #### 🐛 Bug Fix
342 |
343 | - allow getMetadata calls to return social provider [#71](https://github.com/magiclabs/magic-admin-js/pull/71) ([@hcote](https://github.com/hcote))
344 |
345 | #### Authors: 1
346 |
347 | - Hunter Cote ([@hcote](https://github.com/hcote))
348 |
349 | ---
350 |
351 | # v1.3.1 (Tue Oct 05 2021)
352 |
353 | #### 🐛 Bug Fix
354 |
355 | - fix: prevent URI encoding [#69](https://github.com/magiclabs/magic-admin-js/pull/69) ([@chiefGui](https://github.com/chiefGui))
356 |
357 | #### 🏠 Internal
358 |
359 | - Cleanup internal scripts [#47](https://github.com/magiclabs/magic-admin-js/pull/47) ([@smithki](https://github.com/smithki))
360 |
361 | #### Authors: 2
362 |
363 | - Guilherme Oderdenge ([@chiefGui](https://github.com/chiefGui))
364 | - Ian K Smith ([@smithki](https://github.com/smithki))
365 |
366 | ---
367 |
368 | # v1.3.0 (Tue Nov 17 2020)
369 |
370 | #### 🚀 Enhancement
371 |
372 | - Add continuous delivery via 'auto' package [#46](https://github.com/magiclabs/magic-admin-js/pull/46) ([@smithki](https://github.com/smithki))
373 |
374 | #### 🐛 Bug Fix
375 |
376 | - Update validate function to support attachment feature [#15](https://github.com/magiclabs/magic-admin-js/pull/15) ([@smithki](https://github.com/smithki))
377 | - Fix misspelling of 'argumenet' [#43](https://github.com/magiclabs/magic-admin-js/pull/43) ([@brianrlewis](https://github.com/brianrlewis))
378 | - Add 'UtilsModule' & 'parseAuthorizationHeader' helper [#39](https://github.com/magiclabs/magic-admin-js/pull/39) ([@smithki](https://github.com/smithki))
379 | - Remove vulnerability badge [#38](https://github.com/magiclabs/magic-admin-js/pull/38) ([@FYJen](https://github.com/FYJen))
380 | - Add vulnerabilities and circleci badges [#35](https://github.com/magiclabs/magic-admin-js/pull/35) ([@FYJen](https://github.com/FYJen))
381 | - Add issue templates and contributing guide [#26](https://github.com/magiclabs/magic-admin-js/pull/26) ([@smithki](https://github.com/smithki))
382 | - Encode query URI components and implement logout v2 [#22](https://github.com/magiclabs/magic-admin-js/pull/22) ([@smithki](https://github.com/smithki))
383 | - Add 'getUserBy*' methods and generalize REST API services [#18](https://github.com/magiclabs/magic-admin-js/pull/18) ([@smithki](https://github.com/smithki))
384 | - Validate `nbf` field [#40](https://github.com/magiclabs/magic-admin-js/pull/40) ([@smithki](https://github.com/smithki))
385 | - fix name of logout request body [#14](https://github.com/magiclabs/magic-admin-js/pull/14) ([@Dizigen](https://github.com/Dizigen))
386 | - Add logoutByIssuer method [#13](https://github.com/magiclabs/magic-admin-js/pull/13) ([@smithki](https://github.com/smithki))
387 | - Remove middlewares [#12](https://github.com/magiclabs/magic-admin-js/pull/12) ([@smithki](https://github.com/smithki))
388 | - Add new header + fix decode type signature [#11](https://github.com/magiclabs/magic-admin-js/pull/11) ([@smithki](https://github.com/smithki))
389 | - Remove default exports [#10](https://github.com/magiclabs/magic-admin-js/pull/10) ([@smithki](https://github.com/smithki))
390 | - Add CircleCI config [#8](https://github.com/magiclabs/magic-admin-js/pull/8) ([@smithki](https://github.com/smithki))
391 | - Add unit tests [#7](https://github.com/magiclabs/magic-admin-js/pull/7) ([@smithki](https://github.com/smithki))
392 |
393 | #### ⚠️ Pushed to `master`
394 |
395 | - v0.1.0-beta.8 ([@smithki](https://github.com/smithki))
396 | - 1.2.2 ([@smithki](https://github.com/smithki))
397 | - Rename variable for clarity ([@smithki](https://github.com/smithki))
398 | - Fix logical error related to 'nbf' check ([@smithki](https://github.com/smithki))
399 | - 1.2.1 ([@smithki](https://github.com/smithki))
400 | - Add 'files' field to 'package.json' ([@smithki](https://github.com/smithki))
401 | - 1.2.0 ([@smithki](https://github.com/smithki))
402 | - Fix broken CircleCI badge ([@smithki](https://github.com/smithki))
403 | - 1.1.0 ([@smithki](https://github.com/smithki))
404 | - Update CONTRIBUTING.md ([@smithki](https://github.com/smithki))
405 | - Update README.md ([@smithki](https://github.com/smithki))
406 | - Update issue templates ([@smithki](https://github.com/smithki))
407 | - v1.0.0 ([@smithki](https://github.com/smithki))
408 | - v0.1.0-beta.10 ([@smithki](https://github.com/smithki))
409 | - v0.1.0-beta.9 ([@smithki](https://github.com/smithki))
410 | - Update CHANGELOG ([@smithki](https://github.com/smithki))
411 | - v0.1.0-beta.7 ([@smithki](https://github.com/smithki))
412 | - Remove console statement ([@smithki](https://github.com/smithki))
413 | - Fix bug in 'isDIDTClaim' ([@smithki](https://github.com/smithki))
414 | - v0.1.0-beta.6 ([@smithki](https://github.com/smithki))
415 | - 0.1.0-beta.5 ([@Dizigen](https://github.com/Dizigen))
416 | - Update TypeScript to 3.8.3 ([@smithki](https://github.com/smithki))
417 | - Remove node E2E ([@smithki](https://github.com/smithki))
418 | - v0.1.0-beta.4 ([@smithki](https://github.com/smithki))
419 | - Update README ([@smithki](https://github.com/smithki))
420 | - Fix test ([@smithki](https://github.com/smithki))
421 | - v0.1.0-beta.3 ([@smithki](https://github.com/smithki))
422 | - v0.1.0-beta.2 ([@smithki](https://github.com/smithki))
423 | - v0.1.0-beta.0 ([@smithki](https://github.com/smithki))
424 | - Add boilerplate code ([@smithki](https://github.com/smithki))
425 | - Initial commit ([@FYJen](https://github.com/FYJen))
426 |
427 | #### 🔩 Dependency Updates
428 |
429 | - Bump lodash from 4.17.15 to 4.17.19 [#41](https://github.com/magiclabs/magic-admin-js/pull/41) ([@dependabot[bot]](https://github.com/dependabot[bot]))
430 | - Bump elliptic from 6.5.2 to 6.5.3 [#42](https://github.com/magiclabs/magic-admin-js/pull/42) ([@dependabot[bot]](https://github.com/dependabot[bot]))
431 | - Bump node-fetch from 2.6.0 to 2.6.1 [#45](https://github.com/magiclabs/magic-admin-js/pull/45) ([@dependabot[bot]](https://github.com/dependabot[bot]))
432 | - Bump acorn from 7.1.0 to 7.1.1 [#36](https://github.com/magiclabs/magic-admin-js/pull/36) ([@dependabot[bot]](https://github.com/dependabot[bot]) [@smithki](https://github.com/smithki))
433 |
434 | #### Authors: 5
435 |
436 | - [@dependabot[bot]](https://github.com/dependabot[bot])
437 | - Arthur Jen ([@FYJen](https://github.com/FYJen))
438 | - Brian Lewis ([@brianrlewis](https://github.com/brianrlewis))
439 | - David He ([@Dizigen](https://github.com/Dizigen))
440 | - Ian K Smith ([@smithki](https://github.com/smithki))
441 |
442 | ---
443 |
444 | ## Upcoming Changes
445 |
446 | #### Fixed
447 |
448 | - ...
449 |
450 | #### Changed
451 |
452 | - ...
453 |
454 | #### Added
455 |
456 | - ...
457 |
458 | ## `1.2.2` - 06/11/2020
459 |
460 | #### Fixed
461 |
462 | - Fixed a logical error that would incorrectly parse the `nbf` field.
463 |
464 | ## `1.2.1` - 06/11/2020
465 |
466 | #### Changed
467 |
468 | - Added `"files"` field to `package.json` to make the NPM package less cumbersome.
469 |
470 | ## `1.2.0` - 06/11/2020
471 |
472 | #### Added
473 |
474 | - Enforce the `nbf` field for DID Tokens parsed via `TokenModule.validate`.
475 |
476 | ## `1.1.0` - 05/25/2020
477 |
478 | #### Added
479 |
480 | - Introduced `UtilsModule` along with a `parseAuthorizationHeader` helper method.
481 |
482 | ## `1.0.0` - 04/09/2020
483 |
484 | This is the first release our changelog records. Future updates will be logged in the following format:
485 |
486 | #### Fixed
487 |
488 | - Bug fixes and patches will be described here.
489 |
490 | #### Changed
491 |
492 | - Changes (breaking or otherwise) to current APIs will be described here.
493 |
494 | #### Added
495 |
496 | - New features or APIs will be described here.
497 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | When contributing to this repository, please first discuss the change you wish to make via an **issue**. This can be a feature request or a bug report. After a maintainer has triaged your issue, you are welcome to collaborate on a pull request. If your change is small or uncomplicated, you are welcome to open an issue and pull request simultaneously.
4 |
5 | Please note we have a **code of conduct**, please follow it in all your interactions with the project.
6 |
7 | ## Setting up for Local Development
8 |
9 | 1. Fork this repostiory.
10 | 2. Clone your fork.
11 | 3. Create a new branch in your local repository with the following pattern:
12 |
13 | - For bug fixes: `bug/#[issue_number]/[descriptive_bug_name]`
14 | - For features: `feature/#[issue_number]/[descriptive_feature_name]`
15 | - For chores/the rest: `chore/[descriptive_chore_name]`
16 |
17 | 4. Install dependencies with Yarn: `yarn install`
18 | 5. Start building for development: `yarn start`
19 |
20 | ## Opening a Pull Request
21 |
22 | 1. Open a pull request from your fork/branch to the upstream `master` branch of _this_ repository.
23 | 2. Add a label for the [semver](https://semver.org/) update corresponding to your changes: `patch`, `minor`, or `major`.
24 | 3. A maintainer will review your code changes and offer feedback or suggestions if necessary. Once your changes are approved, a maintainer will merge the pull request for you and publish a release.
25 |
26 | ## Cutting a release
27 |
28 | We use [`auto`](https://github.com/intuit/auto) as our continous delivery tool. Cutting a release is just a matter of merging to `master`. For pre-releases, you can create a `next` branch as the base for your experimental/W.I.P. feature. Please familiarize yourself with the [documentation for `auto`](https://intuit.github.io/auto/docs) if you are in a position to cut a release.
29 |
30 | ## Contributor Covenant Code of Conduct
31 |
32 | ### Our Pledge
33 |
34 | We as members, contributors, and leaders pledge to make participation in our
35 | community a harassment-free experience for everyone, regardless of age, body
36 | size, visible or invisible disability, ethnicity, sex characteristics, gender
37 | identity and expression, level of experience, education, socio-economic status,
38 | nationality, personal appearance, race, religion, or sexual identity
39 | and orientation.
40 |
41 | We pledge to act and interact in ways that contribute to an open, welcoming,
42 | diverse, inclusive, and healthy community.
43 |
44 | ### Our Standards
45 |
46 | Examples of behavior that contributes to a positive environment for our
47 | community include:
48 |
49 | - Demonstrating empathy and kindness toward other people
50 | - Being respectful of differing opinions, viewpoints, and experiences
51 | - Giving and gracefully accepting constructive feedback
52 | - Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience
53 | - Focusing on what is best not just for us as individuals, but for the overall community
54 |
55 | Examples of unacceptable behavior include:
56 |
57 | - The use of sexualized language or imagery, and sexual attention or advances of any kind
58 | - Trolling, insulting or derogatory comments, and personal or political attacks
59 | - Public or private harassment
60 | - Publishing others' private information, such as a physical or email address, without their explicit permission
61 | - Other conduct which could reasonably be considered inappropriate in a professional setting
62 |
63 | ### Enforcement Responsibilities
64 |
65 | Community leaders are responsible for clarifying and enforcing our standards of
66 | acceptable behavior and will take appropriate and fair corrective action in
67 | response to any behavior that they deem inappropriate, threatening, offensive,
68 | or harmful.
69 |
70 | Community leaders have the right and responsibility to remove, edit, or reject
71 | comments, commits, code, wiki edits, issues, and other contributions that are
72 | not aligned to this Code of Conduct, and will communicate reasons for moderation
73 | decisions when appropriate.
74 |
75 | ### Scope
76 |
77 | This Code of Conduct applies within all community spaces, and also applies when
78 | an individual is officially representing the community in public spaces.
79 | Examples of representing our community include using an official e-mail address,
80 | posting via an official social media account, or acting as an appointed
81 | representative at an online or offline event.
82 |
83 | ### Enforcement
84 |
85 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
86 | reported to the community leaders responsible for enforcement at [support@magic.link](mailto:support@magic.link).
87 | All complaints will be reviewed and investigated promptly and fairly.
88 |
89 | All community leaders are obligated to respect the privacy and security of the
90 | reporter of any incident.
91 |
92 | ### Enforcement Guidelines
93 |
94 | Community leaders will follow these Community Impact Guidelines in determining
95 | the consequences for any action they deem in violation of this Code of Conduct:
96 |
97 | #### 1. Correction
98 |
99 | **Community Impact**: Use of inappropriate language or other behavior deemed
100 | unprofessional or unwelcome in the community.
101 |
102 | **Consequence**: A private, written warning from community leaders, providing
103 | clarity around the nature of the violation and an explanation of why the
104 | behavior was inappropriate. A public apology may be requested.
105 |
106 | #### 2. Warning
107 |
108 | **Community Impact**: A violation through a single incident or series
109 | of actions.
110 |
111 | **Consequence**: A warning with consequences for continued behavior. No
112 | interaction with the people involved, including unsolicited interaction with
113 | those enforcing the Code of Conduct, for a specified period of time. This
114 | includes avoiding interactions in community spaces as well as external channels
115 | like social media. Violating these terms may lead to a temporary or
116 | permanent ban.
117 |
118 | #### 3. Temporary Ban
119 |
120 | **Community Impact**: A serious violation of community standards, including
121 | sustained inappropriate behavior.
122 |
123 | **Consequence**: A temporary ban from any sort of interaction or public
124 | communication with the community for a specified period of time. No public or
125 | private interaction with the people involved, including unsolicited interaction
126 | with those enforcing the Code of Conduct, is allowed during this period.
127 | Violating these terms may lead to a permanent ban.
128 |
129 | #### 4. Permanent Ban
130 |
131 | **Community Impact**: Demonstrating a pattern of violation of community
132 | standards, including sustained inappropriate behavior, harassment of an
133 | individual, or aggression toward or disparagement of classes of individuals.
134 |
135 | **Consequence**: A permanent ban from any sort of public interaction within
136 | the community.
137 |
138 | ### Attribution
139 |
140 | This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org),
141 | version 2.0, available at
142 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
143 |
144 | Community Impact Guidelines were inspired by [Mozilla's code of conduct
145 | enforcement ladder](https://github.com/mozilla/diversity).
146 |
147 | For answers to common questions about this code of conduct, see the FAQ at
148 | https://www.contributor-covenant.org/faq. Translations are available at
149 | https://www.contributor-covenant.org/translations.
150 |
151 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Fortmatic Inc.
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Magic Authentication Admin Javascript SDK
2 |
3 | [](https://github.com/magiclabs/magic-admin-js/actions/workflows/publish.yml)
4 | > The Magic Admin SDK lets developers secure endpoints, manage users, and create middlewares via easy-to-use utilities.
5 |
6 |
7 | License ·
8 | Changelog ·
9 | Contributing Guide
10 |
11 |
12 | ## 📖 Documentation
13 |
14 | See the [developer documentation](https://magic.link/docs/api-reference/server-side-sdks/node) to learn how you can master the Magic Admin SDK in a matter of minutes.
15 |
16 | ## 🔗 Installation
17 |
18 | Integrating your Node.js application with Magic will require our server-side NPM package:
19 |
20 | ```bash
21 | # Via NPM:
22 | npm install --save @magic-sdk/admin
23 |
24 | # Via Yarn:
25 | yarn add @magic-sdk/admin
26 | ```
27 |
28 | ## ⚡️ Quick Start
29 |
30 | Sign up or log in to the [developer dashboard](https://dashboard.magic.link) to receive API keys that will allow your application to interact with Magic's administration APIs.
31 |
32 | ```ts
33 | const { Magic } = require('@magic-sdk/admin');
34 |
35 | // In async function:
36 | const magic = await Magic.init('YOUR_SECRET_API_KEY');
37 | // OR
38 | Magic.init('YOUR_SECRET_API_KEY').then((magic) => {
39 | magic
40 | });
41 | // Validate a token
42 | try {
43 | magic.token.validate("DIDToken");
44 | } catch (e) {
45 | console.log(e);
46 | }
47 | // Magic Auth - Get User Email
48 | try {
49 | await magic.users.getMetadataByToken("DIDToken");
50 | } catch (e) {
51 | console.log(e);
52 | }
53 | ```
54 |
--------------------------------------------------------------------------------
/config/tsconfig.base.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": ["es2018", "dom"],
4 | "module": "commonjs",
5 | "moduleResolution": "node",
6 | "target": "es6",
7 | "strict": true,
8 | "allowSyntheticDefaultImports": true,
9 | "experimentalDecorators": true,
10 | "noImplicitReturns": true,
11 | "noImplicitThis": true,
12 | "esModuleInterop": true,
13 | "downlevelIteration": true,
14 | "resolveJsonModule": true,
15 | "allowJs": true,
16 | "sourceMap": true,
17 | "declaration": true,
18 | },
19 | "include": ["../src/**/*.ts"],
20 | "exclude": ["../node_modules"]
21 | }
22 |
--------------------------------------------------------------------------------
/config/tsconfig.sdk.cjs.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.base.json",
3 | "compilerOptions": {
4 | "outDir": "../dist/cjs",
5 | },
6 | "include": ["../src/**/*.ts"]
7 | }
8 |
--------------------------------------------------------------------------------
/config/tsconfig.sdk.esm.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.base.json",
3 | "compilerOptions": {
4 | "lib": ["es2020", "dom"],
5 | "target": "es2020",
6 | "module": "es2020",
7 | "outDir": "../dist/esm"
8 | },
9 | "include": ["../src/**/*.ts"]
10 | }
11 |
--------------------------------------------------------------------------------
/config/tsconfig.test.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.base.json",
3 | "compilerOptions": {
4 | "module": "commonjs",
5 | "target": "es6",
6 | "strict": false,
7 | "noImplicitAny": false,
8 | "downlevelIteration": true,
9 | "esModuleInterop": true
10 | },
11 | "include": ["../test/**/*.ts"]
12 | }
13 |
--------------------------------------------------------------------------------
/jest.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from '@jest/types';
2 |
3 | const config: Config.InitialOptions = {
4 | maxWorkers: 2,
5 | preset: 'ts-jest',
6 | coverageReporters: ['text-summary', 'html'],
7 | collectCoverageFrom: ['./src/**/*.{ts,tsx,}'],
8 | collectCoverage: true,
9 | testTimeout: 30000, // 30s
10 | coverageThreshold: {
11 | global: {
12 | lines: 99,
13 | statements: 99,
14 | functions: 99,
15 | branches: 99,
16 | }
17 | },
18 | globals: {
19 | 'ts-jest': {
20 | tsconfig: './test/tsconfig.json',
21 | isolatedModules: true,
22 | },
23 | },
24 | };
25 |
26 | export default config;
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@magic-sdk/admin",
3 | "version": "2.4.1",
4 | "description": "Magic Authentication Admin Javascript SDK.",
5 | "author": "Magic Labs ",
6 | "license": "MIT",
7 | "repository": "magiclabs/magic-admin-js",
8 | "main": "dist/cjs/index.js",
9 | "module": "dist/esm/index.js",
10 | "types": "dist/cjs/index.d.ts",
11 | "exports": {
12 | ".": {
13 | "import": "./dist/esm/index.js",
14 | "require": "./dist/cjs/index.js"
15 | }
16 | },
17 | "files": [
18 | "dist/**/*"
19 | ],
20 | "scripts": {
21 | "start": "npm run clean:build && ./scripts/start.sh",
22 | "build": "npm run clean:build && ./scripts/build.sh",
23 | "build-esm": "esbuild src/index.ts --bundle --outdir=dist/esm --platform=node --target=esnext",
24 | "test": "npm run clean:test-artifacts && ./scripts/test.sh",
25 | "lint": "eslint --fix src/**/*.ts",
26 | "clean": "npm-run-all -s clean:*",
27 | "clean:test-artifacts": "rimraf coverage",
28 | "clean:build": "rimraf dist",
29 | "clean_node_modules": "rimraf node_modules",
30 | "prepare": "husky install"
31 | },
32 | "devDependencies": {
33 | "@ikscodes/eslint-config": "^8.4.1",
34 | "@istanbuljs/nyc-config-typescript": "^1.0.1",
35 | "@types/jest": "^27.4.1",
36 | "@types/node": "^13.1.2",
37 | "@types/node-fetch": "^2.5.4",
38 | "@typescript-eslint/eslint-plugin": "^6.21.0",
39 | "auto": "11.0.5",
40 | "boxen-cli": "^1.0.0",
41 | "esbuild": "^0.14.54",
42 | "eslint": "^8.56.0",
43 | "eslint-import-resolver-typescript": "^2.0.0",
44 | "eslint-plugin-import": "^2.29.1",
45 | "eslint-plugin-jsx-a11y": "^6.8.0",
46 | "eslint-plugin-prettier": "^5.1.3",
47 | "husky": "^8.0.3",
48 | "jest": "^27.5.1",
49 | "lint-staged": "^10.0.8",
50 | "npm-run-all": "~4.1.5",
51 | "prettier": "^3.2.4",
52 | "rimraf": "~3.0.0",
53 | "ts-jest": "^27.1.3",
54 | "ts-node": "^10.2.0",
55 | "tslint": "~5.20.1",
56 | "typescript": "^5.3.3"
57 | },
58 | "dependencies": {
59 | "ethereum-cryptography": "^1.0.1",
60 | "ethers": "^6.11.1",
61 | "node-fetch": "^2.6.7"
62 | },
63 | "lint-staged": {
64 | "*.{ts,tsx}": "eslint --fix"
65 | },
66 | "auto": {
67 | "plugins": [
68 | "npm",
69 | "released"
70 | ]
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/scripts/build.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | echo
4 | boxen --border-color cyan --dim-border --padding 1 "🏗 Building Magic Admin SDK for production."
5 | echo
6 |
7 | # Increase memory limit for Node
8 | export NODE_OPTIONS=--max_old_space_size=4096
9 |
10 | export NODE_ENV=production
11 |
12 | # Generate type declarations
13 | npx tsc -p ./config/tsconfig.sdk.esm.json --declaration --emitDeclarationOnly
14 |
15 | # Build ESM bundle
16 | npm run build-esm
17 |
18 | #Build CJS bundle
19 | npx tsc -p ./config/tsconfig.sdk.cjs.json
20 |
--------------------------------------------------------------------------------
/scripts/start.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | echo
4 | boxen --border-color cyan --dim-border --padding 1 "🏗 Building Magic Admin SDK for development."
5 | echo
6 |
7 | # Increase memory limit for Node
8 | export NODE_OPTIONS=--max_old_space_size=4096
9 |
10 | export NODE_ENV=development
11 |
12 | npx tsc -w -p ./config/tsconfig.sdk.json
13 |
--------------------------------------------------------------------------------
/scripts/test.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | echo
4 | boxen --border-color cyan --dim-border --padding 1 "🚦 Running unit tests..."
5 | echo
6 |
7 | # Increase memory limit for Node
8 | export NODE_OPTIONS=--max_old_space_size=4096
9 |
10 | export NODE_ENV=test
11 |
12 | export TS_NODE_PROJECT="test/tsconfig.json"
13 |
14 | jest $input
15 |
--------------------------------------------------------------------------------
/src/core/sdk-exceptions.ts:
--------------------------------------------------------------------------------
1 | import { ErrorCode } from '../types';
2 |
3 | // --- Base SDK error class
4 |
5 | export class MagicAdminSDKError extends Error {
6 | __proto__ = Error;
7 |
8 | constructor(
9 | public code: ErrorCode,
10 | message: string,
11 | public data: any[] = [],
12 | ) {
13 | super(`Magic Admin SDK Error: [${code}] ${message}`);
14 | Object.setPrototypeOf(this, MagicAdminSDKError.prototype);
15 | }
16 | }
17 |
18 | // --- SDK error factories
19 |
20 | export function createTokenExpiredError() {
21 | return new MagicAdminSDKError(ErrorCode.TokenExpired, 'DID Token has expired. Request failed authentication.');
22 | }
23 |
24 | export function createTokenCannotBeUsedYetError() {
25 | return new MagicAdminSDKError(
26 | ErrorCode.TokenCannotBeUsedYet,
27 | 'Given DID Token cannot be used at this time. Please check the `nbf` field and regenerate a new token with a suitable value.',
28 | );
29 | }
30 |
31 | export function createIncorrectSignerAddressError() {
32 | return new MagicAdminSDKError(
33 | ErrorCode.IncorrectSignerAddress,
34 | 'Incorrect signer address for DID Token. Request failed authentication.',
35 | );
36 | }
37 |
38 | export function createFailedRecoveringProofError() {
39 | return new MagicAdminSDKError(
40 | ErrorCode.FailedRecoveryProof,
41 | 'Failed to recover proof. Request failed authentication.',
42 | );
43 | }
44 |
45 | export function createApiKeyMissingError() {
46 | return new MagicAdminSDKError(
47 | ErrorCode.ApiKeyMissing,
48 | 'Please provide a secret Magic API key that you acquired from the developer dashboard.',
49 | );
50 | }
51 |
52 | export function createMalformedTokenError() {
53 | return new MagicAdminSDKError(ErrorCode.MalformedTokenError, 'The DID token is malformed or failed to parse.');
54 | }
55 |
56 | export function createServiceError(...nestedErrors: any[]) {
57 | return new MagicAdminSDKError(
58 | ErrorCode.ServiceError,
59 | 'A service error occurred while communicating with the Magic API. Check the `data` key of this error object to see nested errors with additional context.',
60 | nestedErrors,
61 | );
62 | }
63 |
64 | export function createExpectedBearerStringError() {
65 | return new MagicAdminSDKError(
66 | ErrorCode.ExpectedBearerString,
67 | 'Expected argument to be a string in the `Bearer {token}` format.',
68 | );
69 | }
70 |
71 | export function createAudienceMismatchError() {
72 | return new MagicAdminSDKError(
73 | ErrorCode.AudienceMismatch,
74 | 'Audience does not match client ID. Please ensure your secret key matches the application which generated the DID token.',
75 | );
76 | }
77 |
--------------------------------------------------------------------------------
/src/core/sdk.ts:
--------------------------------------------------------------------------------
1 | import { createApiKeyMissingError } from './sdk-exceptions';
2 | import { TokenModule } from '../modules/token';
3 | import { UsersModule } from '../modules/users';
4 | import { UtilsModule } from '../modules/utils';
5 | import { MagicAdminSDKAdditionalConfiguration } from '../types';
6 | import { get } from '../utils/rest';
7 |
8 | export class MagicAdminSDK {
9 | public readonly apiBaseUrl: string;
10 |
11 | /**
12 | * Contains utilities for interacting with Decentralized Identity Tokens
13 | * (DIDTs).
14 | */
15 | public readonly token: TokenModule;
16 |
17 | /**
18 | * Contains utilities for interacting with your Magic Authentication user
19 | * model.
20 | */
21 | public readonly users: UsersModule;
22 |
23 | /**
24 | * Contains general utilities for Magic Admin SDK.
25 | */
26 | public readonly utils: UtilsModule;
27 |
28 | /**
29 | * Unique client identifier
30 | */
31 | public clientId: string | null;
32 |
33 | /**
34 | * Deprecated. Use `init` instead.
35 | * @param secretApiKey
36 | * @param options
37 | */
38 | constructor(
39 | public readonly secretApiKey?: string,
40 | options?: MagicAdminSDKAdditionalConfiguration,
41 | ) {
42 | const endpoint = options?.endpoint ?? 'https://api.magic.link';
43 | this.apiBaseUrl = endpoint.replace(/\/+$/, '');
44 | this.clientId = options?.clientId ?? null;
45 | // Assign API Modules
46 | this.token = new TokenModule(this);
47 | this.users = new UsersModule(this);
48 | this.utils = new UtilsModule(this);
49 | }
50 |
51 | public static async init(secretApiKey?: string, options?: MagicAdminSDKAdditionalConfiguration) {
52 | if (!secretApiKey) throw createApiKeyMissingError();
53 |
54 | let hydratedOptions = options ?? {};
55 |
56 | const endpoint = hydratedOptions.endpoint ?? 'https://api.magic.link';
57 | const apiBaseUrl = endpoint.replace(/\/+$/, '');
58 |
59 | if (!hydratedOptions.clientId) {
60 | const resp = await get<{
61 | client_id: string | null;
62 | app_scope: string | null;
63 | }>(`${apiBaseUrl}/v1/admin/client/get`, secretApiKey);
64 | hydratedOptions = { ...hydratedOptions, clientId: resp.client_id };
65 | }
66 |
67 | return new MagicAdminSDK(secretApiKey, hydratedOptions);
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | export { MagicAdminSDK as Magic } from './core/sdk';
2 | export { MagicAdminSDKError as SDKError } from './core/sdk-exceptions';
3 | export * from './types';
4 |
--------------------------------------------------------------------------------
/src/modules/base-module.ts:
--------------------------------------------------------------------------------
1 | import { MagicAdminSDK } from '../core/sdk';
2 |
3 | export abstract class BaseModule {
4 | constructor(protected readonly sdk: MagicAdminSDK) {}
5 | }
6 |
--------------------------------------------------------------------------------
/src/modules/token/index.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable prefer-destructuring */
2 | import { BaseModule } from '../base-module';
3 | import { ParsedDIDToken } from '../../types';
4 | import {
5 | createFailedRecoveringProofError,
6 | createIncorrectSignerAddressError,
7 | createTokenExpiredError,
8 | createMalformedTokenError,
9 | createTokenCannotBeUsedYetError,
10 | createAudienceMismatchError,
11 | } from '../../core/sdk-exceptions';
12 | import { ecRecover } from '../../utils/ec-recover';
13 | import { parseDIDToken } from '../../utils/parse-didt';
14 | import { parsePublicAddressFromIssuer } from '../../utils/issuer';
15 |
16 | export class TokenModule extends BaseModule {
17 | public validate(DIDToken: string, attachment = 'none') {
18 | let tokenSigner = '';
19 | let attachmentSigner: string | null = null;
20 | let claimedIssuer = '';
21 | let parsedClaim;
22 | let proof: string;
23 | let claim: string;
24 |
25 | try {
26 | const tokenParseResult = parseDIDToken(DIDToken);
27 | [proof, claim] = tokenParseResult.raw;
28 | parsedClaim = tokenParseResult.withParsedClaim[1];
29 | claimedIssuer = parsePublicAddressFromIssuer(parsedClaim.iss);
30 | } catch {
31 | throw createMalformedTokenError();
32 | }
33 |
34 | try {
35 | // Recover the token signer
36 | tokenSigner = ecRecover(claim, proof).toLowerCase();
37 |
38 | // Recover the attachment signer
39 | if (attachment && attachment !== 'none') {
40 | attachmentSigner = ecRecover(attachment, parsedClaim.add).toLowerCase();
41 | }
42 | } catch {
43 | throw createFailedRecoveringProofError();
44 | }
45 |
46 | // Assert the expected signer
47 | if (claimedIssuer !== tokenSigner || (attachmentSigner && claimedIssuer !== attachmentSigner)) {
48 | throw createIncorrectSignerAddressError();
49 | }
50 |
51 | const timeSecs = Math.floor(Date.now() / 1000);
52 | const nbfLeeway = 300; // 5 min grace period
53 |
54 | // Assert the token is not expired
55 | if (parsedClaim.ext < timeSecs) {
56 | throw createTokenExpiredError();
57 | }
58 |
59 | // Assert the token is not used before allowed.
60 | if (parsedClaim.nbf - nbfLeeway > timeSecs) {
61 | throw createTokenCannotBeUsedYetError();
62 | }
63 |
64 | // Assert the audience matches the client ID.
65 | if (this.sdk.clientId && parsedClaim.aud !== this.sdk.clientId) {
66 | throw createAudienceMismatchError();
67 | }
68 | }
69 |
70 | public decode(DIDToken: string): ParsedDIDToken {
71 | const parsedToken = parseDIDToken(DIDToken);
72 | return parsedToken.withParsedClaim;
73 | }
74 |
75 | public getPublicAddress(DIDToken: string): string {
76 | const claim = this.decode(DIDToken)[1];
77 | const claimedIssuer = claim.iss.split(':')[2];
78 | return claimedIssuer;
79 | }
80 |
81 | public getIssuer(DIDToken: string): string {
82 | return this.decode(DIDToken)[1].iss;
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/src/modules/users/index.ts:
--------------------------------------------------------------------------------
1 | import { BaseModule } from '../base-module';
2 | import { createApiKeyMissingError } from '../../core/sdk-exceptions';
3 | import { post, get } from '../../utils/rest';
4 | import { generateIssuerFromPublicAddress } from '../../utils/issuer';
5 | import { MagicUserMetadata, MagicWallet, WalletType } from '../../types';
6 |
7 | export class UsersModule extends BaseModule {
8 | // --- User logout endpoints
9 |
10 | public async logoutByIssuer(issuer: string): Promise {
11 | if (!this.sdk.secretApiKey) throw createApiKeyMissingError();
12 | const body = { issuer };
13 | await post(`${this.sdk.apiBaseUrl}/v2/admin/auth/user/logout`, this.sdk.secretApiKey, body);
14 | }
15 |
16 | public async logoutByPublicAddress(publicAddress: string): Promise {
17 | const issuer = generateIssuerFromPublicAddress(publicAddress);
18 | await this.logoutByIssuer(issuer);
19 | }
20 |
21 | public async logoutByToken(DIDToken: string): Promise {
22 | const issuer = this.sdk.token.getIssuer(DIDToken);
23 | await this.logoutByIssuer(issuer);
24 | }
25 |
26 | // --- User metadata endpoints
27 |
28 | public async getMetadataByIssuer(issuer: string): Promise {
29 | return this.getMetadataByIssuerAndWallet(issuer, WalletType.NONE);
30 | }
31 |
32 | public async getMetadataByToken(DIDToken: string): Promise {
33 | const issuer = this.sdk.token.getIssuer(DIDToken);
34 | return this.getMetadataByIssuer(issuer);
35 | }
36 |
37 | public async getMetadataByPublicAddress(publicAddress: string): Promise {
38 | const issuer = generateIssuerFromPublicAddress(publicAddress);
39 | return this.getMetadataByIssuer(issuer);
40 | }
41 |
42 | public async getMetadataByTokenAndWallet(DIDToken: string, walletType: WalletType): Promise {
43 | const issuer = this.sdk.token.getIssuer(DIDToken);
44 | return this.getMetadataByIssuerAndWallet(issuer, walletType);
45 | }
46 |
47 | public async getMetadataByPublicAddressAndWallet(
48 | publicAddress: string,
49 | walletType: WalletType,
50 | ): Promise {
51 | const issuer = generateIssuerFromPublicAddress(publicAddress);
52 | return this.getMetadataByIssuerAndWallet(issuer, walletType);
53 | }
54 |
55 | public async getMetadataByIssuerAndWallet(issuer: string, walletType: WalletType): Promise {
56 | if (!this.sdk.secretApiKey) throw createApiKeyMissingError();
57 |
58 | const data = await get<{
59 | issuer: string | null;
60 | public_address: string | null;
61 | email: string | null;
62 | oauth_provider: string | null;
63 | phone_number: string | null;
64 | username: string | null;
65 | wallets: MagicWallet[] | null;
66 | }>(`${this.sdk.apiBaseUrl}/v1/admin/auth/user/get`, this.sdk.secretApiKey, { issuer, wallet_type: walletType });
67 |
68 | return {
69 | issuer: data.issuer ?? null,
70 | publicAddress: data.public_address ?? null,
71 | email: data.email ?? null,
72 | oauthProvider: data.oauth_provider ?? null,
73 | phoneNumber: data.phone_number ?? null,
74 | username: data.username ?? null,
75 | wallets: data.wallets ?? null,
76 | };
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/src/modules/utils/index.ts:
--------------------------------------------------------------------------------
1 | import { ethers } from "ethers";
2 | import { BaseModule } from '../base-module';
3 | import {createExpectedBearerStringError} from '../../core/sdk-exceptions';
4 | import { ValidateTokenOwnershipResponse } from '../../types';
5 | import { ERC1155ContractABI, ERC721ContractABI } from './ownershipABIs';
6 | import { ErrorCode } from '../../types';
7 |
8 | export class UtilsModule extends BaseModule {
9 | /**
10 | * Parse a raw DID Token from the given Authorization header.
11 | */
12 | public parseAuthorizationHeader(header: string) {
13 | if (!header.toLowerCase().startsWith('bearer ')) {
14 | throw createExpectedBearerStringError();
15 | }
16 |
17 | return header.substring(7);
18 | }
19 |
20 | // Token Gating function validates user ownership of wallet + NFT
21 | public async validateTokenOwnership(
22 | didToken: string,
23 | contractAddress: string,
24 | contractType: 'ERC721' | 'ERC1155',
25 | rpcURL: string,
26 | tokenId?: string,
27 | ): Promise {
28 | // Make sure if ERC1155 has a tokenId
29 | if (contractType === 'ERC1155' && !tokenId) {
30 | throw new Error('ERC1155 requires a tokenId');
31 | }
32 | // Validate DID token
33 | let walletAddress;
34 | try {
35 | await this.sdk.token.validate(didToken);
36 | walletAddress = this.sdk.token.getPublicAddress(didToken);
37 | } catch (e: any) {
38 | // Check if code is malformed token
39 | if (e.code && e.code === 'ERROR_MALFORMED_TOKEN') {
40 | return {
41 | valid: false,
42 | error_code: 'UNAUTHORIZED',
43 | message: 'Invalid DID token: ' + ErrorCode.MalformedTokenError,
44 | };
45 | }
46 | if (e.code === ErrorCode.TokenExpired) {
47 | return {
48 | valid: false,
49 | error_code: 'UNAUTHORIZED',
50 | message: 'Invalid DID token: ' + ErrorCode.TokenExpired,
51 | };
52 | }
53 | throw new Error(e);
54 | }
55 |
56 |
57 | // Check on-chain if user owns NFT by calling contract with web3
58 | let balance = BigInt(0);
59 | const provider = new ethers.JsonRpcProvider(rpcURL);
60 | if (contractType === 'ERC721') {
61 | const contract = new ethers.Contract(contractAddress, ERC721ContractABI, provider);
62 | balance = BigInt(await contract.balanceOf(walletAddress));
63 | } else {
64 | const contract = new ethers.Contract(contractAddress, ERC1155ContractABI, provider);
65 | balance = BigInt(await contract.balanceOf(walletAddress, tokenId));
66 | }
67 | if (balance > BigInt(0)) {
68 | return {
69 | valid: true,
70 | error_code: '',
71 | message: '',
72 | };
73 | }
74 | return {
75 | valid: false,
76 | error_code: 'NO_OWNERSHIP',
77 | message: 'User does not own this token.',
78 | };
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/src/modules/utils/ownershipABIs.ts:
--------------------------------------------------------------------------------
1 | export const ERC721ContractABI = [
2 | "function balanceOf(address owner) view returns (uint balance)",
3 | ]
4 | export const ERC1155ContractABI = [
5 | "function balanceOf(address owner, uint id) view returns (uint balance)",
6 | ]
7 |
--------------------------------------------------------------------------------
/src/types/didt-types.ts:
--------------------------------------------------------------------------------
1 | /** The shape of metadata encoded within a DID Token. */
2 | export interface Claim {
3 | iat: number; // Issued At Timestamp
4 | ext: number; // Expiration Timestamp
5 | iss: string; // Issuer of DID Token
6 | sub: string; // Subject
7 | aud: string; // Audience
8 | nbf: number; // Not Before Timestamp
9 | tid: string; // DID Token ID
10 | add: string; // Encrypted signature of arbitrary data
11 | }
12 |
13 | export type ParsedDIDToken = [string, Claim];
14 |
--------------------------------------------------------------------------------
/src/types/exception-types.ts:
--------------------------------------------------------------------------------
1 | export enum ErrorCode {
2 | MissingAuthHeader = 'ERROR_MISSING_AUTH_HEADER',
3 | TokenExpired = 'ERROR_DIDT_EXPIRED',
4 | TokenCannotBeUsedYet = 'ERROR_DIDT_CANNOT_BE_USED_YET',
5 | IncorrectSignerAddress = 'ERROR_INCORRECT_SIGNER_ADDR',
6 | FailedRecoveryProof = 'ERROR_FAILED_RECOVERING_PROOF',
7 | ApiKeyMissing = 'ERROR_SECRET_API_KEY_MISSING',
8 | MalformedTokenError = 'ERROR_MALFORMED_TOKEN',
9 | ServiceError = 'SERVICE_ERROR',
10 | ExpectedBearerString = 'EXPECTED_BEARER_STRING',
11 | AudienceMismatch = 'ERROR_AUDIENCE_MISMATCH',
12 | }
13 |
--------------------------------------------------------------------------------
/src/types/index.ts:
--------------------------------------------------------------------------------
1 | export * from './didt-types';
2 | export * from './exception-types';
3 | export * from './sdk-types';
4 | export * from './wallet-types';
5 | export * from './utils-types';
6 |
--------------------------------------------------------------------------------
/src/types/sdk-types.ts:
--------------------------------------------------------------------------------
1 | export interface MagicAdminSDKAdditionalConfiguration {
2 | endpoint?: string;
3 | clientId?: string | null;
4 | }
5 |
6 | export interface MagicWallet {
7 | network: string | null;
8 | publicAddress: string | null;
9 | walletType: string | null;
10 | }
11 |
12 | export interface MagicUserMetadata {
13 | issuer: string | null;
14 | publicAddress: string | null;
15 | email: string | null;
16 | oauthProvider: string | null;
17 | phoneNumber: string | null;
18 | username: string | null;
19 | wallets: MagicWallet[] | null;
20 | }
21 |
--------------------------------------------------------------------------------
/src/types/utils-types.ts:
--------------------------------------------------------------------------------
1 | export interface ValidateTokenOwnershipResponse {
2 | valid: boolean;
3 | error_code: string;
4 | message: string;
5 | }
6 |
--------------------------------------------------------------------------------
/src/types/wallet-types.ts:
--------------------------------------------------------------------------------
1 | export enum WalletType {
2 | ETH = 'ETH',
3 | HARMONY = 'HARMONY',
4 | ICON = 'ICON',
5 | FLOW = 'FLOW',
6 | TEZOS = 'TEZOS',
7 | ZILLIQA = 'ZILLIQA',
8 | POLKADOT = 'POLKADOT',
9 | SOLANA = 'SOLANA',
10 | AVAX = 'AVAX',
11 | ALGOD = 'ALGOD',
12 | COSMOS = 'COSMOS',
13 | CELO = 'CELO',
14 | BITCOIN = 'BITCOIN',
15 | NEAR = 'NEAR',
16 | HELIUM = 'HELIUM',
17 | CONFLUX = 'CONFLUX',
18 | TERRA = 'TERRA',
19 | TAQUITO = 'TAQUITO',
20 | ED = 'ED',
21 | HEDERA = 'HEDERA',
22 | NONE = 'NONE',
23 | ANY = 'ANY',
24 | }
25 |
--------------------------------------------------------------------------------
/src/utils/codec.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Check if global object window is defined
3 | */
4 | export function isBrowser() {
5 | return typeof window !== 'undefined';
6 | }
7 |
8 | /*
9 | * Decode base64 value, returns string
10 | * @Params: string
11 | */
12 | export function decodeValue(value: string): string {
13 | if (!value) {
14 | return '';
15 | }
16 |
17 | const valueToString = value.toString();
18 |
19 | if (isBrowser()) {
20 | return atob(valueToString);
21 | }
22 |
23 | const buff = Buffer.from(valueToString, 'base64');
24 | return buff.toString('ascii');
25 | }
26 |
--------------------------------------------------------------------------------
/src/utils/ec-recover.ts:
--------------------------------------------------------------------------------
1 | import { keccak256 } from 'ethereum-cryptography/keccak.js';
2 | import { ecdsaRecover, publicKeyConvert } from 'ethereum-cryptography/secp256k1-compat.js';
3 | import { utf8ToBytes, bytesToHex, hexToBytes } from 'ethereum-cryptography/utils.js';
4 |
5 | function hashPersonalMessage(message: Uint8Array): Uint8Array {
6 | const prefix = utf8ToBytes(`\u0019Ethereum Signed Message:\n${message.length}`);
7 | const totalLength = prefix.length + message.length;
8 |
9 | const output = new Uint8Array(totalLength);
10 | output.set(prefix);
11 | output.set(message, prefix.length);
12 |
13 | return keccak256(output);
14 | }
15 |
16 | function getRecoveryBit(signature: Uint8Array): number {
17 | const bit = signature[64];
18 | return bit - 27;
19 | }
20 |
21 | function prepareSignature(signature: string): string {
22 | return signature.slice(2); // strip the `0x` prefix
23 | }
24 |
25 | function publicKeyToAddress(publicKey: Uint8Array): string {
26 | const address = keccak256(publicKey.slice(1)).slice(-20);
27 | return `0x${bytesToHex(address)}`;
28 | }
29 |
30 | /**
31 | * Recover the signer from an Elliptic Curve signature.
32 | */
33 | export function ecRecover(data: string, signature: string) {
34 | // Use ecdsaRecover on the Proof, to validate if it recovers to the expected
35 | // Claim, and expected Signer Address.
36 |
37 | const msg = utf8ToBytes(data);
38 | const sig = hexToBytes(prepareSignature(signature));
39 | const recovery = getRecoveryBit(sig);
40 | const hash = hashPersonalMessage(msg);
41 |
42 | const publicKey = ecdsaRecover(sig.slice(0, 64), recovery, hash, false);
43 | const assertPublicKey = publicKeyConvert(publicKey, false);
44 |
45 | return publicKeyToAddress(assertPublicKey);
46 | }
47 |
--------------------------------------------------------------------------------
/src/utils/fetch.ts:
--------------------------------------------------------------------------------
1 | import { RequestInit, Response } from 'node-fetch';
2 |
3 | type Fetch = (url: string, init?: RequestInit) => Promise;
4 |
5 | /* istanbul ignore next */
6 | export const fetch: Fetch = !(globalThis as any).fetch
7 | ? (url, init) => import('node-fetch').then(({ default: f }) => f(url, init))
8 | : (globalThis as any).fetch;
9 |
--------------------------------------------------------------------------------
/src/utils/issuer.ts:
--------------------------------------------------------------------------------
1 | export function generateIssuerFromPublicAddress(publicAddress: string, method = 'ethr') {
2 | return `did:${method}:${publicAddress}`;
3 | }
4 |
5 | export function parsePublicAddressFromIssuer(issuer: string) {
6 | return issuer.split(':')[2]?.toLowerCase() ?? '';
7 | }
8 |
--------------------------------------------------------------------------------
/src/utils/parse-didt.ts:
--------------------------------------------------------------------------------
1 | import { decodeValue } from './codec';
2 | import { isDIDTClaim } from './type-guards';
3 | import { createMalformedTokenError } from '../core/sdk-exceptions';
4 | import { Claim, ParsedDIDToken } from '../types';
5 |
6 | interface ParseDIDTokenResult {
7 | raw: [string, string];
8 | withParsedClaim: ParsedDIDToken;
9 | }
10 |
11 | /**
12 | * Parses a DID Token so that the encoded `claim` is in object form.
13 | */
14 | export function parseDIDToken(didToken: string): ParseDIDTokenResult {
15 | try {
16 | const [proof, claim] = JSON.parse(decodeValue(didToken)) as [string, string];
17 | const parsedClaim = JSON.parse(claim) as Claim;
18 | if (isDIDTClaim(parsedClaim)) return { raw: [proof, claim], withParsedClaim: [proof, parsedClaim] };
19 | throw new Error();
20 | } catch {
21 | throw createMalformedTokenError();
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/utils/rest.ts:
--------------------------------------------------------------------------------
1 | import { RequestInit } from 'node-fetch';
2 |
3 | import { fetch } from './fetch';
4 | import { createServiceError } from '../core/sdk-exceptions';
5 |
6 | interface MagicAPIResponse {
7 | data?: TData;
8 | error_code?: string;
9 | message?: string;
10 | status?: string | number;
11 | }
12 |
13 | /**
14 | * Performs a `fetch` to the given URL with the configured `init` object.
15 | */
16 | async function emitRequest(url: string, init?: RequestInit): Promise> {
17 | const json: MagicAPIResponse = await fetch(url, init)
18 | .then((res) => res.json())
19 | .catch((err) => {
20 | throw createServiceError(err);
21 | });
22 |
23 | if (json.status !== 'ok') {
24 | throw createServiceError(json);
25 | }
26 |
27 | return json.data ?? {};
28 | }
29 |
30 | /**
31 | * Generates an encoded URL with query string from a dictionary of values.
32 | */
33 | function generateQuery>(url: string, params?: T) {
34 | let query = '?';
35 | if (params) {
36 | for (const [key, value] of Object.entries(params)) query += `${key}=${value}&`;
37 | query = query.slice(0, -1); // Remove trailing "&"
38 | }
39 | return params ? `${url}${query}` : url;
40 | }
41 |
42 | /**
43 | * POSTs to Magic's API.
44 | */
45 | export function post, TResponse>(
46 | url: string,
47 | secretApiKey: string,
48 | body: TBody,
49 | ) {
50 | return emitRequest(url, {
51 | method: 'POST',
52 | headers: { 'X-Magic-Secret-key': secretApiKey },
53 | body: JSON.stringify(body),
54 | });
55 | }
56 |
57 | /**
58 | * GETs from Magic's API.
59 | */
60 | export function get(url: string, secretApiKey: string, params?: any) {
61 | const urlWithParams = generateQuery(url, params);
62 | return emitRequest(urlWithParams, {
63 | method: 'GET',
64 | headers: { 'X-Magic-Secret-key': secretApiKey },
65 | });
66 | }
67 |
--------------------------------------------------------------------------------
/src/utils/type-guards.ts:
--------------------------------------------------------------------------------
1 | /*
2 | This file contains our type guards.
3 |
4 | Type guards are a feature of TypeScript which narrow the type signature of
5 | intesection types (types that can be one thing or another).
6 |
7 | @see
8 | https://www.typescriptlang.org/docs/handbook/advanced-types.html#type-guards-and-differentiating-types
9 | */
10 |
11 | import { Claim } from '../types';
12 |
13 | /** Assert `value` is `undefined`. */
14 | function isUndefined(value: any): value is undefined {
15 | return typeof value === 'undefined';
16 | }
17 |
18 | /** Assert `value` is `null`. */
19 | function isNull(value: any): value is null {
20 | return value === null;
21 | }
22 |
23 | /** Assert `value` is `null` or `undefined`. */
24 | function isNil(value: any): value is null | undefined {
25 | return isNull(value) || isUndefined(value);
26 | }
27 |
28 | /** Assert `value` contains all required DID Token members. */
29 | export function isDIDTClaim(value: any): value is Claim {
30 | return (
31 | !isNil(value) &&
32 | !isNil(value.iat) &&
33 | !isNil(value.ext) &&
34 | !isNil(value.iss) &&
35 | !isNil(value.sub) &&
36 | !isNil(value.aud) &&
37 | !isNil(value.nbf) &&
38 | !isNil(value.tid) &&
39 | !isNil(value.add)
40 | );
41 | }
42 |
--------------------------------------------------------------------------------
/test/lib/constants.ts:
--------------------------------------------------------------------------------
1 | export const API_FULL_URL = 'https://api.magic.link';
2 | export const API_KEY = 'sk_test_123';
3 |
4 | export const VALID_DIDT =
5 | 'WyIweGUwMjQzNTVlNDI5ZGNhZDM1MTdhZDk5ZWEzNDEwYWJmZDQ1YjBiNjM5OGIwNjY1NGRiYTQxNzljODdlMTYyNzgxNTc1YjA5ODFjNjU4ZjcwMjYwZTQ5MjMwZGE5NDg4YTA0ZDk5NzBlYjM4ZTZmZGRlY2Q2NTA5YTAyN2IwOGI5MWIiLCJ7XCJpYXRcIjoxNTg1MDExMjA0LFwiZXh0XCI6MTkwMDQxMTIwNCxcImlzc1wiOlwiZGlkOmV0aHI6MHhCMmVjOWI2MTY5OTc2MjQ5MWI2NTQyMjc4RTlkRkVDOTA1MGY4MDg5XCIsXCJzdWJcIjpcIjZ0RlhUZlJ4eWt3TUtPT2pTTWJkUHJFTXJwVWwzbTNqOERReWNGcU8ydHc9XCIsXCJhdWRcIjpcImRpZDptYWdpYzpmNTQxNjhlOS05Y2U5LTQ3ZjItODFjOC03Y2IyYTk2YjI2YmFcIixcIm5iZlwiOjE1ODUwMTEyMDQsXCJ0aWRcIjpcIjJkZGY1OTgzLTk4M2ItNDg3ZC1iNDY0LWJjNWUyODNhMDNjNVwiLFwiYWRkXCI6XCIweDkxZmJlNzRiZTZjNmJmZDhkZGRkZDkzMDExYjA1OWI5MjUzZjEwNzg1NjQ5NzM4YmEyMTdlNTFlMGUzZGYxMzgxZDIwZjUyMWEzNjQxZjIzZWI5OWNjYjM0ZTNiYzVkOTYzMzJmZGViYzhlZmE1MGNkYjQxNWU0NTUwMDk1MmNkMWNcIn0iXQ==';
6 |
7 | export const DECODED_DIDT = `["0xe024355e429dcad3517ad99ea3410abfd45b0b6398b06654dba4179c87e162781575b0981c658f70260e49230da9488a04d9970eb38e6fddecd6509a027b08b91b","{\\"iat\\":1585011204,\\"ext\\":1900411204,\\"iss\\":\\"did:ethr:0xB2ec9b61699762491b6542278E9dFEC9050f8089\\",\\"sub\\":\\"6tFXTfRxykwMKOOjSMbdPrEMrpUl3m3j8DQycFqO2tw=\\",\\"aud\\":\\"did:magic:f54168e9-9ce9-47f2-81c8-7cb2a96b26ba\\",\\"nbf\\":1585011204,\\"tid\\":\\"2ddf5983-983b-487d-b464-bc5e283a03c5\\",\\"add\\":\\"0x91fbe74be6c6bfd8ddddd93011b059b9253f10785649738ba217e51e0e3df1381d20f521a3641f23eb99ccb34e3bc5d96332fdebc8efa50cdb415e45500952cd1c\\"}"]`;
8 | export const VALID_DIDT_WITH_INVALID_RECOVERY_BIT =
9 | 'WyIweGUwMjQzNTVlNDI5ZGNhZDM1MTdhZDk5ZWEzNDEwYWJmZDQ1YjBiNjM5OGIwNjY1NGRiYTQxNzljODdlMTYyNzgxNTc1YjA5ODFjNjU4ZjcwMjYwZTQ5MjMwZGE5NDg4YTA0ZDk5NzBlYjM4ZTZmZGRlY2Q2NTA5YTAyN2IwOGI5MjMiLCJ7XCJpYXRcIjoxNTg1MDExMjA0LFwiZXh0XCI6MTkwMDQxMTIwNCxcImlzc1wiOlwiZGlkOmV0aHI6MHhCMmVjOWI2MTY5OTc2MjQ5MWI2NTQyMjc4RTlkRkVDOTA1MGY4MDg5XCIsXCJzdWJcIjpcIjZ0RlhUZlJ4eWt3TUtPT2pTTWJkUHJFTXJwVWwzbTNqOERReWNGcU8ydHc9XCIsXCJhdWRcIjpcImRpZDptYWdpYzpmNTQxNjhlOS05Y2U5LTQ3ZjItODFjOC03Y2IyYTk2YjI2YmFcIixcIm5iZlwiOjE1ODUwMTEyMDQsXCJ0aWRcIjpcIjJkZGY1OTgzLTk4M2ItNDg3ZC1iNDY0LWJjNWUyODNhMDNjNVwiLFwiYWRkXCI6XCIweDkxZmJlNzRiZTZjNmJmZDhkZGRkZDkzMDExYjA1OWI5MjUzZjEwNzg1NjQ5NzM4YmEyMTdlNTFlMGUzZGYxMzgxZDIwZjUyMWEzNjQxZjIzZWI5OWNjYjM0ZTNiYzVkOTYzMzJmZGViYzhlZmE1MGNkYjQxNWU0NTUwMDk1MmNkMWNcIn0iXQ==';
10 |
11 | export const VALID_DIDT_PARSED_CLAIMS = {
12 | iat: 1585011204,
13 | ext: 1900411204,
14 | iss: 'did:ethr:0xB2ec9b61699762491b6542278E9dFEC9050f8089',
15 | sub: '6tFXTfRxykwMKOOjSMbdPrEMrpUl3m3j8DQycFqO2tw=',
16 | aud: 'did:magic:f54168e9-9ce9-47f2-81c8-7cb2a96b26ba',
17 | nbf: 1585011204,
18 | tid: '2ddf5983-983b-487d-b464-bc5e283a03c5',
19 | add:
20 | '0x91fbe74be6c6bfd8ddddd93011b059b9253f10785649738ba217e51e0e3df1381d20f521a3641f23eb99ccb34e3bc5d96332fdebc8efa50cdb415e45500952cd1c',
21 | };
22 |
23 | export const VALID_DIDT_DECODED = [
24 | '0xe024355e429dcad3517ad99ea3410abfd45b0b6398b06654dba4179c87e162781575b0981c658f70260e49230da9488a04d9970eb38e6fddecd6509a027b08b91b',
25 | VALID_DIDT_PARSED_CLAIMS,
26 | ];
27 |
28 | export const VALID_FUTURE_MARKED_DIDT =
29 | 'WyIweDkzZjRiNTViYzRlN2E1ZWJkZTdmMzVkYzczMWE5NWFmOGYwZjVlMWQyMWQ5ZDYwZWQxM2Y4YmYzMmNiN2UwOTQ1MDM0MGI1Y2IyNTIxODZkNWQ3OTFiOTAyODZhYmY1NzM3YzMxN2M5NzNhMmQzMGY0MWZmYmFlNGU0NTdmMjE4MWIiLCJ7XCJpYXRcIjoxNTkxOTE0NTgyLFwiZXh0XCI6MjIyMjcxNDU4MixcImlzc1wiOlwiZGlkOmV0aHI6MHg0YzMzMmQ5QzRhMmEwNjY1YzNmODg1MTU1YjlFOTFmZEIzMDBlRTc2XCIsXCJzdWJcIjpcIms4NUtaR09Ycl9vMTYxNGdFVGN6Yzlac0phTjV4cjF2TVFXSWhnbjQ1Slk9XCIsXCJhdWRcIjpcImRpZDptYWdpYzoyMWI4ZjRkZS02ZmIzLTQ0M2YtOGM0MC04ODcwODJjNDQ1MjNcIixcIm5iZlwiOjE5MDczMTQ1ODIsXCJ0aWRcIjpcIjVhMjhjMjQwLWRmYzYtNDg2Ni04ODk1LTVkYzBhOTVkNWJkN1wiLFwiYWRkXCI6XCIweGRlMmI1ODgyNjUyZGExOTY4YWNlZTIyYWUyNGI2OWYxNThlZjg1NDQzOGE0OTlmMThjZGZlZDU3MzEwOGIxNzExYjQ2OWQ3MzQ5NzdhNGQ4NGJlM2RiODc2OTBkZjFmZjk4MTVjN2Y3NDIxNjIxMGY4Y2JhMGJmYzQ2ZGIwYjhkMWNcIn0iXQ==';
30 |
31 | export const INVALID_DIDT_MALFORMED_CLAIM =
32 | 'WyIweDRiN2Y0ODgzNTczOGM2ZjViZmI5MTIyYmEzN2EwN2EyZDYwMTUwZjM2NGNhMDIwZjUyYWVlZjc3MmNlODk2ZWExYmJlYzc3OGZlMmFhNWNjODc3ZmVmOWQyOTJmN2U2MWZhMGZjY2M2NWIwZWZlZGVkMThmNzI0MzZkNjQ4MzdjMWMiLCJ7XCJ0aGlzSXNXUk9OR1wiOlwiSEVMTE8gV09STERcIn0iXQ==';
33 |
34 | export const MALFORMED_DIDT_PARSED_CLAIMS = {
35 | thisIsWRONG: 'HELLO WORLD',
36 | };
37 |
38 | export const INVALID_DIDT_MALFORMED_CLAIM_DECODED = [
39 | '0x4b7f48835738c6f5bfb9122ba37a07a2d60150f364ca020f52aeef772ce896ea1bbec778fe2aa5cc877fef9d292f7e61fa0fccc65b0efeded18f72436d64837c1c',
40 | MALFORMED_DIDT_PARSED_CLAIMS,
41 | ];
42 |
43 | export const INVALID_SIGNER_DIDT =
44 | 'WyIweDBhNTk4NmE1NDdiMzNhMDAxODIxNmRiNjk0YzNiMDg3YTU3MTk1Nzg4ZTZmMDc2NDg4NzA2ZTQ3ZmFhNjFhYzMzZDczZTM4ZmM5ZDA0YzU2YWVmZWNiMTAxMDA4OGEwNmFlOWFiZTE5ZDIyYWQ4MzNiMDhhM2VlNWNmZWM5ZDQ0MWMiLCJ7XCJpYXRcIjoxNTg1MDEwODIxLFwiZXh0XCI6MTkwMDQxMDgyMSxcImlzc1wiOlwiXFxcImRpZDpldGhyOjB4MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMFxcXCJcIixcInN1YlwiOlwiNnRGWFRmUnh5a3dNS09PalNNYmRQckVNcnBVbDNtM2o4RFF5Y0ZxTzJ0dz1cIixcImF1ZFwiOlwiZGlkOm1hZ2ljOjMzZjAxNGVlLTNkZDUtNGRmZi1iYzE2LTgxNTU3MTFiN2UwMlwiLFwibmJmXCI6MTU4NTAxMDgyMSxcInRpZFwiOlwiOGEzYjdkZDUtZTFjZi00OTY1LWFlMmItZDIwZjE4OGU2ZWMyXCIsXCJhZGRcIjpcIjB4OTFmYmU3NGJlNmM2YmZkOGRkZGRkOTMwMTFiMDU5YjkyNTNmMTA3ODU2NDk3MzhiYTIxN2U1MWUwZTNkZjEzODFkMjBmNTIxYTM2NDFmMjNlYjk5Y2NiMzRlM2JjNWQ5NjMzMmZkZWJjOGVmYTUwY2RiNDE1ZTQ1NTAwOTUyY2QxY1wifSJd';
45 |
46 | export const EXPIRED_DIDT =
47 | 'WyIweGE3MDUzYzg3OTI2ZjMzZDBjMTZiMjMyYjYwMWYxZDc2NmRiNWY3YWM4MTg2MzUyMzY4ZjAyMzIyMGEwNzJjYzkzM2JjYjI2MmU4ODQyNWViZDA0MzcyZGU3YTc0NzMwYjRmYWYzOGU0ZjgwNmYzOTJjMTVkNzY2YmVkMjVlZmUxMWIiLCJ7XCJpYXRcIjoxNTg1MDEwODM1LFwiZXh0XCI6MTU4NTAxMDgzNixcImlzc1wiOlwiZGlkOmV0aHI6MHhCMmVjOWI2MTY5OTc2MjQ5MWI2NTQyMjc4RTlkRkVDOTA1MGY4MDg5XCIsXCJzdWJcIjpcIjZ0RlhUZlJ4eWt3TUtPT2pTTWJkUHJFTXJwVWwzbTNqOERReWNGcU8ydHc9XCIsXCJhdWRcIjpcImRpZDptYWdpYzpkNGMwMjgxYi04YzViLTQ5NDMtODUwOS0xNDIxNzUxYTNjNzdcIixcIm5iZlwiOjE1ODUwMTA4MzUsXCJ0aWRcIjpcImFjMmE4YzFjLWE4OWEtNDgwOC1hY2QxLWM1ODg1ZTI2YWZiY1wiLFwiYWRkXCI6XCIweDkxZmJlNzRiZTZjNmJmZDhkZGRkZDkzMDExYjA1OWI5MjUzZjEwNzg1NjQ5NzM4YmEyMTdlNTFlMGUzZGYxMzgxZDIwZjUyMWEzNjQxZjIzZWI5OWNjYjM0ZTNiYzVkOTYzMzJmZGViYzhlZmE1MGNkYjQxNWU0NTUwMDk1MmNkMWNcIn0iXQ==';
48 |
49 | export const VALID_ATTACHMENT_DIDT =
50 | 'WyIweGVkMWMwNWRlMTVlMWFkY2Y5ZmEyZWNkNjVjZjg5NWMzYTgzMzQ2OGMwOGFhMmE3YjQ5ZDgyMjFiZWEyMWU1YjgzNDRiNWEwMzAzNmQxMzA5MzQyNTgzMWIxZTFjZGIwZWQ2NTgyMDI4MWU1NzhlMjU5ODJhYzdkYmNkZWJhN2I1MWMiLCJ7XCJpYXRcIjoxNjg4MDYzMTA4LFwiZXh0XCI6MS4wMDAwMDAwMDAwMDE2ODgxZSsyMSxcImlzc1wiOlwiZGlkOmV0aHI6MHhhMWI0YzA5NDI2NDdlNzkwY0ZEMmEwNUE1RkQyNkMwMmM0MjEzOWFlXCIsXCJzdWJcIjpcIjhaTUJnOXNwMFgwQ0FNanhzcVFaOGRzRTJwNVlZWm9lYkRPeWNPUFNNbDA9XCIsXCJhdWRcIjpcIjN3X216VmktaDNtUzc3cFZ4b19ydlJhWjR2WXpOZ0Vudm05ZGcwWnkzYzg9XCIsXCJuYmZcIjoxNjg4MDYzMTA4LFwidGlkXCI6XCJjM2U5ZWRiYy04MDU2LTQ3NGItOGFkMy1hOGI2MzM3NThlOTRcIixcImFkZFwiOlwiMHgzZGExZTM3MmU1ZWU5MjI4YzdlYjBkNmQwZDE2MTAxZjBkNjE5MDY0ODVhYjgzNDMzNWI3Y2YxOGE5ZDNmZWEzNjRmYzFjMTFiNzRlYzBhNTQ0ZTkzNmJkNjQ1Y2U3ZDdkZTIyMTRlNTJlYjZhOThjZTIyNzI1OTEwNDg0ZjJkOTFjXCJ9Il0';
51 |
--------------------------------------------------------------------------------
/test/lib/factories.ts:
--------------------------------------------------------------------------------
1 | import { API_FULL_URL, API_KEY } from './constants';
2 | import { MagicAdminSDK } from '../../src/core/sdk';
3 |
4 | export function createMagicAdminSDK(endpoint = API_FULL_URL, clientId = null) {
5 | return new MagicAdminSDK(API_KEY, { endpoint, clientId });
6 | }
7 |
--------------------------------------------------------------------------------
/test/spec/core/sdk-exceptions/error-factories.spec.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-underscore-dangle */
2 | import {
3 | MagicAdminSDKError,
4 | createTokenExpiredError,
5 | createIncorrectSignerAddressError,
6 | createFailedRecoveringProofError,
7 | createApiKeyMissingError,
8 | createServiceError,
9 | createExpectedBearerStringError,
10 | createTokenCannotBeUsedYetError,
11 | createAudienceMismatchError,
12 | } from '../../../../src/core/sdk-exceptions';
13 |
14 | function errorAssertions(
15 | error: MagicAdminSDKError,
16 | expectedCode: string,
17 | expectedMessage: string,
18 | expectedData: any[] = [],
19 | ) {
20 | expect(error instanceof MagicAdminSDKError).toBe(true);
21 | expect(error.code).toBe(expectedCode);
22 | expect(error.message).toBe(`Magic Admin SDK Error: [${expectedCode}] ${expectedMessage}`);
23 | expect(error.data).toEqual(expectedData);
24 | }
25 |
26 | test('Creates `ERROR_DIDT_EXPIRED` error', async () => {
27 | const error = createTokenExpiredError();
28 | errorAssertions(error, 'ERROR_DIDT_EXPIRED', 'DID Token has expired. Request failed authentication.');
29 | });
30 |
31 | test('Creates `ERROR_DIDT_CANNOT_BE_USED_YET` error', async () => {
32 | const error = createTokenCannotBeUsedYetError();
33 | errorAssertions(
34 | error,
35 | 'ERROR_DIDT_CANNOT_BE_USED_YET',
36 | 'Given DID Token cannot be used at this time. Please check the `nbf` field and regenerate a new token with a suitable value.',
37 | );
38 | });
39 |
40 | test('Creates `ERROR_INCORRECT_SIGNER_ADDR` error', async () => {
41 | const error = createIncorrectSignerAddressError();
42 | errorAssertions(
43 | error,
44 | 'ERROR_INCORRECT_SIGNER_ADDR',
45 | 'Incorrect signer address for DID Token. Request failed authentication.',
46 | );
47 | });
48 |
49 | test('Creates `ERROR_FAILED_RECOVERING_PROOF` error', async () => {
50 | const error = createFailedRecoveringProofError();
51 | errorAssertions(error, 'ERROR_FAILED_RECOVERING_PROOF', 'Failed to recover proof. Request failed authentication.');
52 | });
53 |
54 | test('Creates `ERROR_SECRET_API_KEY_MISSING` error', async () => {
55 | const error = createApiKeyMissingError();
56 | errorAssertions(
57 | error,
58 | 'ERROR_SECRET_API_KEY_MISSING',
59 | 'Please provide a secret Magic API key that you acquired from the developer dashboard.',
60 | );
61 | });
62 |
63 | test('Creates `SERVICE_ERROR` error with empty `data` property', async () => {
64 | const error = createServiceError();
65 | errorAssertions(
66 | error,
67 | 'SERVICE_ERROR',
68 | 'A service error occurred while communicating with the Magic API. Check the `data` key of this error object to see nested errors with additional context.',
69 | );
70 | });
71 |
72 | test('Creates `SERVICE_ERROR` error with non-empty `data` property', async () => {
73 | const error = createServiceError('hello', 'world');
74 | errorAssertions(
75 | error,
76 | 'SERVICE_ERROR',
77 | 'A service error occurred while communicating with the Magic API. Check the `data` key of this error object to see nested errors with additional context.',
78 | ['hello', 'world'],
79 | );
80 | });
81 |
82 | test('Creates `EXPECTED_BEARER_STRING` error', async () => {
83 | const error = createExpectedBearerStringError();
84 | errorAssertions(error, 'EXPECTED_BEARER_STRING', 'Expected argument to be a string in the `Bearer {token}` format.');
85 | });
86 |
87 | test('Creates `AUDIENCE_MISMATCH` error', async () => {
88 | const error = createAudienceMismatchError();
89 | errorAssertions(
90 | error,
91 | 'ERROR_AUDIENCE_MISMATCH',
92 | 'Audience does not match client ID. Please ensure your secret key matches the application which generated the DID token.',
93 | );
94 | });
95 |
--------------------------------------------------------------------------------
/test/spec/core/sdk-exceptions/magic-admin-sdk-error/constructor.spec.ts:
--------------------------------------------------------------------------------
1 | import { MagicAdminSDKError } from '../../../../../src/core/sdk-exceptions';
2 |
3 | test('Instantiates `MagicAdminSDKError` with empty `data` property', () => {
4 | const error = new MagicAdminSDKError('TEST_CODE' as any, 'test message');
5 | expect(error instanceof MagicAdminSDKError).toBe(true);
6 | expect(error.message).toBe('Magic Admin SDK Error: [TEST_CODE] test message');
7 | expect(error.code).toBe('TEST_CODE');
8 | expect(error.data).toEqual([]);
9 | });
10 |
11 | test('Instantiates `MagicAdminSDKError` with non-empty `data` property', () => {
12 | const error = new MagicAdminSDKError('TEST_CODE' as any, 'test message', ['hello world']);
13 | expect(error instanceof MagicAdminSDKError).toBe(true);
14 | expect(error.message).toBe('Magic Admin SDK Error: [TEST_CODE] test message');
15 | expect(error.code).toBe('TEST_CODE');
16 | expect(error.data).toEqual(['hello world']);
17 | });
18 |
--------------------------------------------------------------------------------
/test/spec/core/sdk/constructor.spec.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-new */
2 | import { Magic } from '../../../../src/index';
3 | import { API_FULL_URL, API_KEY } from '../../../lib/constants';
4 | import { TokenModule } from '../../../../src/modules/token';
5 | import { UsersModule } from '../../../../src/modules/users';
6 | import { UtilsModule } from '../../../../src/modules/utils';
7 | import { get } from '../../../../src/utils/rest';
8 | import { createApiKeyMissingError } from '../../../../src/core/sdk-exceptions';
9 |
10 | test('Initialize `MagicAdminSDK`', () => {
11 | const magic = new Magic(API_KEY);
12 |
13 | expect(magic.secretApiKey).toBe(API_KEY);
14 | expect(magic.apiBaseUrl).toBe(API_FULL_URL);
15 | expect(magic.token instanceof TokenModule).toBe(true);
16 | expect(magic.users instanceof UsersModule).toBe(true);
17 | });
18 |
19 | test('Initialize `MagicAdminSDK` with custom endpoint', () => {
20 | const magic = new Magic(API_KEY, { endpoint: 'https://example.com' });
21 |
22 | expect(magic.secretApiKey).toBe(API_KEY);
23 | expect(magic.apiBaseUrl).toBe('https://example.com');
24 | expect(magic.token instanceof TokenModule).toBe(true);
25 | expect(magic.users instanceof UsersModule).toBe(true);
26 | expect(magic.utils instanceof UtilsModule).toBe(true);
27 | });
28 |
29 | test('Strips trailing slash(es) from custom endpoint argument', () => {
30 | const magicA = new Magic(API_KEY, { endpoint: 'https://example.com/' });
31 | const magicB = new Magic(API_KEY, { endpoint: 'https://example.com//' });
32 | const magicC = new Magic(API_KEY, { endpoint: 'https://example.com///' });
33 |
34 | expect(magicA.apiBaseUrl).toBe('https://example.com');
35 | expect(magicB.apiBaseUrl).toBe('https://example.com');
36 | expect(magicC.apiBaseUrl).toBe('https://example.com');
37 | });
38 |
39 | test('Initialize `MagicAdminSDK` using static init and empty options', async () => {
40 | const successRes = Promise.resolve({
41 | client_id: 'foo',
42 | app_scope: 'GLOBAL',
43 | });
44 | (get as any) = jest.fn().mockImplementation(() => successRes);
45 |
46 | const magic = await Magic.init(API_KEY, {});
47 |
48 | expect(magic.secretApiKey).toBe(API_KEY);
49 | expect(magic.apiBaseUrl).toBe(API_FULL_URL);
50 | expect(magic.token instanceof TokenModule).toBe(true);
51 | expect(magic.users instanceof UsersModule).toBe(true);
52 | });
53 |
54 | test('Initialize `MagicAdminSDK` using static init and undefined options', async () => {
55 | const successRes = Promise.resolve({
56 | client_id: 'foo',
57 | app_scope: 'GLOBAL',
58 | });
59 | (get as any) = jest.fn().mockImplementation(() => successRes);
60 |
61 | const magic = await Magic.init(API_KEY);
62 |
63 | expect(magic.secretApiKey).toBe(API_KEY);
64 | expect(magic.apiBaseUrl).toBe(API_FULL_URL);
65 | expect(magic.token instanceof TokenModule).toBe(true);
66 | expect(magic.users instanceof UsersModule).toBe(true);
67 | });
68 |
69 | test('Initialize `MagicAdminSDK` using static init and client ID', async () => {
70 | const magic = await Magic.init(API_KEY, { clientId: '1234' });
71 |
72 | expect(magic.secretApiKey).toBe(API_KEY);
73 | expect(magic.apiBaseUrl).toBe(API_FULL_URL);
74 | expect(magic.token instanceof TokenModule).toBe(true);
75 | expect(magic.users instanceof UsersModule).toBe(true);
76 | });
77 |
78 | test('Initialize `MagicAdminSDK` using static init and endpoint', async () => {
79 | const successRes = Promise.resolve({
80 | client_id: 'foo',
81 | app_scope: 'GLOBAL',
82 | });
83 | (get as any) = jest.fn().mockImplementation(() => successRes);
84 |
85 | const magic = await Magic.init(API_KEY, { endpoint: 'https://example.com' });
86 |
87 | expect(magic.secretApiKey).toBe(API_KEY);
88 | expect(magic.apiBaseUrl).toBe('https://example.com');
89 | expect(magic.token instanceof TokenModule).toBe(true);
90 | expect(magic.users instanceof UsersModule).toBe(true);
91 | });
92 |
93 | test('Initialize `MagicAdminSDK` missing API Key', async () => {
94 | const expectedError = createApiKeyMissingError();
95 | expect(Magic.init(null, { clientId: '1234' })).rejects.toThrow(expectedError);
96 | });
97 |
--------------------------------------------------------------------------------
/test/spec/index.spec.ts:
--------------------------------------------------------------------------------
1 | import * as MagicAdmin from '../../src/index';
2 |
3 | describe('MagicAdmin', () => {
4 | it('should have exports', () => {
5 | expect(MagicAdmin).toEqual(expect.any(Object));
6 | });
7 |
8 | it('should not have undefined exports', () => {
9 | for (const k of Object.keys(MagicAdmin))
10 | expect(MagicAdmin).not.toHaveProperty(k, undefined);
11 | });
12 | });
--------------------------------------------------------------------------------
/test/spec/modules/base-module/constructor.spec.ts:
--------------------------------------------------------------------------------
1 | import { MagicAdminSDK } from '../../../../src/core/sdk';
2 | import { BaseModule } from '../../../../src/modules/base-module';
3 | import { createMagicAdminSDK } from '../../../lib/factories';
4 |
5 | test('Initializes `BaseModule`', () => {
6 | const sdk = createMagicAdminSDK();
7 |
8 | const baseModule: any = new (BaseModule as any)(sdk);
9 |
10 | expect(baseModule instanceof BaseModule).toBe(true);
11 | expect(baseModule.sdk instanceof MagicAdminSDK).toBe(true);
12 | });
13 |
--------------------------------------------------------------------------------
/test/spec/modules/token/decode.spec.ts:
--------------------------------------------------------------------------------
1 | import { createMagicAdminSDK } from '../../../lib/factories';
2 | import { VALID_DIDT, VALID_DIDT_DECODED, INVALID_DIDT_MALFORMED_CLAIM } from '../../../lib/constants';
3 | import { createMalformedTokenError } from '../../../../src/core/sdk-exceptions';
4 |
5 | test('Successfully decodes DIDT', async () => {
6 | const sdk = createMagicAdminSDK();
7 | const result = sdk.token.decode(VALID_DIDT);
8 | expect(result).toEqual(VALID_DIDT_DECODED);
9 | });
10 |
11 | test('Throws error if token is malformed', async () => {
12 | const sdk = createMagicAdminSDK();
13 | const expectedError = createMalformedTokenError();
14 | expect(() => sdk.token.decode(INVALID_DIDT_MALFORMED_CLAIM)).toThrow(expectedError);
15 | });
16 |
--------------------------------------------------------------------------------
/test/spec/modules/token/getIssuer.spec.ts:
--------------------------------------------------------------------------------
1 | import { createMagicAdminSDK } from '../../../lib/factories';
2 | import { VALID_DIDT, VALID_DIDT_PARSED_CLAIMS } from '../../../lib/constants';
3 |
4 | test('Successfully gets issuer from DIDT', () => {
5 | const sdk = createMagicAdminSDK();
6 | const result = sdk.token.getIssuer(VALID_DIDT);
7 | const expected = VALID_DIDT_PARSED_CLAIMS.iss;
8 | expect(result).toBe(expected);
9 | });
10 |
--------------------------------------------------------------------------------
/test/spec/modules/token/getPublicAddress.spec.ts:
--------------------------------------------------------------------------------
1 | import { createMagicAdminSDK } from '../../../lib/factories';
2 | import { VALID_DIDT, VALID_DIDT_PARSED_CLAIMS } from '../../../lib/constants';
3 |
4 | test('Successfully gets public address from DIDT', () => {
5 | const sdk = createMagicAdminSDK();
6 | const result = sdk.token.getPublicAddress(VALID_DIDT);
7 | const expected = VALID_DIDT_PARSED_CLAIMS.iss.split(':')[2];
8 | expect(result).toBe(expected);
9 | });
10 |
--------------------------------------------------------------------------------
/test/spec/modules/token/validate.spec.ts:
--------------------------------------------------------------------------------
1 | import { createMagicAdminSDK } from '../../../lib/factories';
2 | import {
3 | VALID_DIDT,
4 | VALID_DIDT_WITH_INVALID_RECOVERY_BIT,
5 | INVALID_SIGNER_DIDT,
6 | EXPIRED_DIDT,
7 | INVALID_DIDT_MALFORMED_CLAIM,
8 | VALID_FUTURE_MARKED_DIDT,
9 | VALID_ATTACHMENT_DIDT,
10 | } from '../../../lib/constants';
11 | import {
12 | createIncorrectSignerAddressError,
13 | createTokenExpiredError,
14 | createFailedRecoveringProofError,
15 | createMalformedTokenError,
16 | createTokenCannotBeUsedYetError,
17 | createAudienceMismatchError,
18 | } from '../../../../src/core/sdk-exceptions';
19 |
20 | test('Successfully validates DIDT', async () => {
21 | const sdk = createMagicAdminSDK(undefined, 'did:magic:f54168e9-9ce9-47f2-81c8-7cb2a96b26ba');
22 | expect(() => sdk.token.validate(VALID_DIDT)).not.toThrow();
23 | });
24 |
25 | test('Successfully validates DIDT without checking audience', async () => {
26 | const sdk = createMagicAdminSDK();
27 | expect(() => sdk.token.validate(VALID_DIDT)).not.toThrow();
28 | });
29 |
30 | test('Successfully validates DIDT with attachment', async () => {
31 | const sdk = createMagicAdminSDK();
32 | expect(() => sdk.token.validate(VALID_ATTACHMENT_DIDT, 'ravi@magic.link')).not.toThrow();
33 | });
34 |
35 | test('Fails when signer address mismatches signature', async () => {
36 | const sdk = createMagicAdminSDK();
37 | const expectedError = createIncorrectSignerAddressError();
38 | expect(() => sdk.token.validate(INVALID_SIGNER_DIDT)).toThrow(expectedError);
39 | });
40 |
41 | test('Fails when given expired token', async () => {
42 | const sdk = createMagicAdminSDK();
43 | const expectedError = createTokenExpiredError();
44 | expect(() => sdk.token.validate(EXPIRED_DIDT)).toThrow(expectedError);
45 | });
46 |
47 | test('Fails when given a token with a future `nbf` timestamp', async () => {
48 | const sdk = createMagicAdminSDK();
49 | const expectedError = createTokenCannotBeUsedYetError();
50 | expect(() => sdk.token.validate(VALID_FUTURE_MARKED_DIDT)).toThrow(expectedError);
51 | });
52 |
53 | test('Fails if signature recovery rejects', async () => {
54 | const sdk = createMagicAdminSDK();
55 | const expectedError = createFailedRecoveringProofError();
56 | expect(() => sdk.token.validate(VALID_DIDT_WITH_INVALID_RECOVERY_BIT)).toThrow(expectedError);
57 | });
58 |
59 | test('Fails if decoding token fails', async () => {
60 | const sdk = createMagicAdminSDK();
61 | const expectedError = createMalformedTokenError();
62 | expect(() => sdk.token.validate(INVALID_DIDT_MALFORMED_CLAIM)).toThrow(expectedError);
63 | });
64 |
65 | test('Fails if aud is incorrect', async () => {
66 | const sdk = createMagicAdminSDK(undefined, 'different');
67 | const expectedError = createAudienceMismatchError();
68 | expect(() => sdk.token.validate(VALID_DIDT)).toThrow(expectedError);
69 | });
70 |
--------------------------------------------------------------------------------
/test/spec/modules/users/getMetadataByIssuer.spec.ts:
--------------------------------------------------------------------------------
1 | import { createApiKeyMissingError } from '../../../../src/core/sdk-exceptions';
2 | import { WalletType } from '../../../../src/types/wallet-types';
3 | import { get } from '../../../../src/utils/rest';
4 | import { API_KEY } from '../../../lib/constants';
5 | import { createMagicAdminSDK } from '../../../lib/factories';
6 |
7 | const successRes = Promise.resolve({
8 | issuer: 'foo',
9 | public_address: 'bar',
10 | email: 'baz',
11 | oauth_provider: 'foo1',
12 | phone_number: '+1234',
13 | username: 'buzz',
14 | });
15 | const successResWithWallets = Promise.resolve({
16 | issuer: 'foo',
17 | public_address: 'bar',
18 | email: 'baz',
19 | oauth_provider: 'foo1',
20 | phone_number: '+1234',
21 | username: 'buzz',
22 | wallets: [
23 | {
24 | wallet_type: 'SOLANA',
25 | network: 'MAINNET',
26 | public_address: 'barxyz',
27 | },
28 | ],
29 | });
30 | const nullRes = Promise.resolve({});
31 |
32 | test('Successfully GETs to metadata endpoint via issuer', async () => {
33 | const sdk = createMagicAdminSDK('https://example.com');
34 |
35 | const getStub = jest.fn().mockImplementation(() => successRes);
36 | (get as any) = getStub;
37 |
38 | const result = await sdk.users.getMetadataByIssuer('did:ethr:0x1234');
39 |
40 | console.log(result);
41 |
42 | const getArguments = getStub.mock.calls[0];
43 | expect(getArguments).toEqual([
44 | 'https://example.com/v1/admin/auth/user/get',
45 | API_KEY,
46 | { issuer: 'did:ethr:0x1234', wallet_type: 'NONE' },
47 | ]);
48 |
49 | expect(result).toEqual({
50 | issuer: 'foo',
51 | publicAddress: 'bar',
52 | email: 'baz',
53 | oauthProvider: 'foo1',
54 | phoneNumber: '+1234',
55 | username: 'buzz',
56 | wallets: null,
57 | });
58 | });
59 |
60 | test('Successfully GETs `null` metadata endpoint via issuer', async () => {
61 | const sdk = createMagicAdminSDK('https://example.com');
62 |
63 | const getStub = jest.fn().mockImplementation(() => nullRes);
64 | (get as any) = getStub;
65 |
66 | const result = await sdk.users.getMetadataByIssuer('did:ethr:0x1234');
67 |
68 | const getArguments = getStub.mock.calls[0];
69 | expect(getArguments).toEqual([
70 | 'https://example.com/v1/admin/auth/user/get',
71 | API_KEY,
72 | { issuer: 'did:ethr:0x1234', wallet_type: 'NONE' },
73 | ]);
74 | expect(result).toEqual({
75 | issuer: null,
76 | publicAddress: null,
77 | email: null,
78 | oauthProvider: null,
79 | phoneNumber: null,
80 | username: null,
81 | wallets: null,
82 | });
83 | });
84 |
85 | test('Fails GET if API key is missing', async () => {
86 | const sdk = createMagicAdminSDK('https://example.com');
87 | (sdk as any).secretApiKey = undefined;
88 |
89 | const getStub = jest.fn().mockImplementation();
90 | (get as any) = getStub;
91 |
92 | const expectedError = createApiKeyMissingError();
93 | expect(sdk.users.getMetadataByIssuer('did:ethr:0x1234')).rejects.toThrow(expectedError);
94 |
95 | expect(getStub).not.toBeCalled();
96 | });
97 |
98 | test('Successfully GETs to metadata endpoint via issuer and wallet type', async () => {
99 | const sdk = createMagicAdminSDK('https://example.com');
100 |
101 | const getStub = jest.fn().mockImplementation(() => successResWithWallets);
102 | (get as any) = getStub;
103 |
104 | const result = await sdk.users.getMetadataByIssuerAndWallet('did:ethr:0x1234', WalletType.SOLANA);
105 |
106 | const getArguments = getStub.mock.calls[0];
107 | expect(getArguments).toEqual([
108 | 'https://example.com/v1/admin/auth/user/get',
109 | API_KEY,
110 | { issuer: 'did:ethr:0x1234', wallet_type: 'SOLANA'},
111 | ]);
112 | expect(result).toEqual({
113 | issuer: 'foo',
114 | publicAddress: 'bar',
115 | email: 'baz',
116 | oauthProvider: 'foo1',
117 | phoneNumber: '+1234',
118 | username: 'buzz',
119 | wallets: [
120 | {
121 | wallet_type: 'SOLANA',
122 | network: 'MAINNET',
123 | public_address: 'barxyz',
124 | },
125 | ],
126 | });
127 | });
128 |
--------------------------------------------------------------------------------
/test/spec/modules/users/getMetadataByPublicAddress.spec.ts:
--------------------------------------------------------------------------------
1 | import { WalletType } from '../../../../src/types/wallet-types';
2 | import { createMagicAdminSDK } from '../../../lib/factories';
3 |
4 | test('Successfully GETs to metadata endpoint via public address', async () => {
5 | const sdk = createMagicAdminSDK('https://example.com');
6 |
7 | const getMetadataStub = jest.fn().mockImplementation(() => Promise.resolve());
8 | (sdk.users.getMetadataByIssuer as any) = getMetadataStub;
9 |
10 | await expect(sdk.users.getMetadataByPublicAddress('0x1234')).resolves.not.toThrow();
11 |
12 | const getMetadataArguments = getMetadataStub.mock.calls[0];
13 | expect(getMetadataArguments).toEqual(['did:ethr:0x1234']);
14 | });
15 |
16 | test('Successfully GETs to metadata endpoint via public address and wallet type', async () => {
17 | const sdk = createMagicAdminSDK('https://example.com');
18 |
19 | const getMetadataStub = jest.fn().mockImplementation(() => Promise.resolve());
20 | (sdk.users.getMetadataByIssuerAndWallet as any) = getMetadataStub;
21 |
22 | await expect(sdk.users.getMetadataByPublicAddressAndWallet('0x1234', WalletType.ANY)).resolves.not.toThrow();
23 |
24 | const getMetadataArguments = getMetadataStub.mock.calls[0];
25 | expect(getMetadataArguments).toEqual(['did:ethr:0x1234', 'ANY']);
26 | });
27 |
--------------------------------------------------------------------------------
/test/spec/modules/users/getMetadataByToken.spec.ts:
--------------------------------------------------------------------------------
1 | import { createMagicAdminSDK } from '../../../lib/factories';
2 | import { VALID_DIDT, VALID_DIDT_PARSED_CLAIMS } from '../../../lib/constants';
3 | import { WalletType } from '../../../../src/types/wallet-types';
4 |
5 | test('Successfully GETs to metadata endpoint via public address', async () => {
6 | const sdk = createMagicAdminSDK('https://example.com');
7 |
8 | const getMetadataStub = jest.fn().mockImplementation(() => Promise.resolve());
9 | (sdk.users.getMetadataByIssuer as any) = getMetadataStub;
10 |
11 | await expect(sdk.users.getMetadataByToken(VALID_DIDT)).resolves.not.toThrow();
12 |
13 | const getMetadataArguments = getMetadataStub.mock.calls[0];
14 | expect(getMetadataArguments).toEqual([VALID_DIDT_PARSED_CLAIMS.iss]);
15 | });
16 |
17 | test('Successfully GETs to metadata endpoint via public address and wallet type', async () => {
18 | const sdk = createMagicAdminSDK('https://example.com');
19 |
20 | const getMetadataStub = jest.fn().mockImplementation(() => Promise.resolve());
21 | (sdk.users.getMetadataByIssuerAndWallet as any) = getMetadataStub;
22 |
23 | await expect(sdk.users.getMetadataByTokenAndWallet(VALID_DIDT, WalletType.ALGOD)).resolves.not.toThrow();
24 |
25 | const getMetadataArguments = getMetadataStub.mock.calls[0];
26 | expect(getMetadataArguments).toEqual([VALID_DIDT_PARSED_CLAIMS.iss, 'ALGOD']);
27 | });
28 |
29 | test('Successfully GETs to metadata endpoint via public address and none wallet type', async () => {
30 | const sdk = createMagicAdminSDK('https://example.com');
31 |
32 | const getMetadataStub = jest.fn().mockImplementation(() => Promise.resolve());
33 | (sdk.users.getMetadataByIssuerAndWallet as any) = getMetadataStub;
34 |
35 | await expect(sdk.users.getMetadataByTokenAndWallet(VALID_DIDT, WalletType.NONE)).resolves.not.toThrow();
36 |
37 | const getMetadataArguments = getMetadataStub.mock.calls[0];
38 | expect(getMetadataArguments).toEqual([VALID_DIDT_PARSED_CLAIMS.iss, 'NONE']);
39 | });
40 |
--------------------------------------------------------------------------------
/test/spec/modules/users/logoutByIssuer.spec.ts:
--------------------------------------------------------------------------------
1 | import { createMagicAdminSDK } from '../../../lib/factories';
2 | import { API_KEY } from '../../../lib/constants';
3 | import { createApiKeyMissingError } from '../../../../src/core/sdk-exceptions';
4 | import { post } from '../../../../src/utils/rest';
5 |
6 | test('Successfully POSTs to logout endpoint via DIDT', async () => {
7 | const sdk = createMagicAdminSDK('https://example.com');
8 |
9 | const postStub = jest.fn();
10 | (post as any) = postStub;
11 |
12 | await expect(sdk.users.logoutByIssuer('did:ethr:0x1234')).resolves.not.toThrow();
13 |
14 | const postArguments = postStub.mock.calls[0];
15 | expect(postArguments).toEqual([
16 | 'https://example.com/v2/admin/auth/user/logout',
17 | API_KEY,
18 | { issuer: 'did:ethr:0x1234' },
19 | ]);
20 | });
21 |
22 | test('Fails POST if API key is missing', async () => {
23 | const sdk = createMagicAdminSDK('https://example.com');
24 | (sdk as any).secretApiKey = undefined;
25 |
26 | const postStub = jest.fn();
27 | (post as any) = postStub;
28 |
29 | const expectedError = createApiKeyMissingError();
30 |
31 | await expect(sdk.users.logoutByIssuer('did:ethr:0x1234')).rejects.toThrow(expectedError);
32 |
33 | expect(postStub).not.toBeCalled();
34 | });
35 |
--------------------------------------------------------------------------------
/test/spec/modules/users/logoutByPublicAddress.spec.ts:
--------------------------------------------------------------------------------
1 | import { createMagicAdminSDK } from '../../../lib/factories';
2 |
3 | test('Successfully GETs to metadata endpoint via public address', async () => {
4 | const sdk = createMagicAdminSDK('https://example.com');
5 |
6 | const logoutStub = jest.fn().mockImplementation(() => Promise.resolve());
7 | (sdk.users.logoutByIssuer as any) = logoutStub;
8 |
9 | await expect(sdk.users.logoutByPublicAddress('0x1234')).resolves.not.toThrow();
10 |
11 | const logoutArguments = logoutStub.mock.calls[0];
12 | expect(logoutArguments).toEqual(['did:ethr:0x1234']);
13 | });
14 |
--------------------------------------------------------------------------------
/test/spec/modules/users/logoutByToken.spec.ts:
--------------------------------------------------------------------------------
1 | import { createMagicAdminSDK } from '../../../lib/factories';
2 | import { VALID_DIDT, VALID_DIDT_PARSED_CLAIMS } from '../../../lib/constants';
3 |
4 | test('Successfully GETs to metadata endpoint via public address', async () => {
5 | const sdk = createMagicAdminSDK('https://example.com');
6 |
7 | const logoutStub = jest.fn().mockImplementation(() => Promise.resolve());
8 | (sdk.users.logoutByIssuer as any) = logoutStub;
9 |
10 | await expect(sdk.users.logoutByToken(VALID_DIDT)).resolves.not.toThrow();
11 |
12 | const logoutArguments = logoutStub.mock.calls[0];
13 | expect(logoutArguments).toEqual([VALID_DIDT_PARSED_CLAIMS.iss]);
14 | });
15 |
--------------------------------------------------------------------------------
/test/spec/modules/utils/parseAuthorizationHeader.spec.ts:
--------------------------------------------------------------------------------
1 | import { createMagicAdminSDK } from '../../../lib/factories';
2 | import { VALID_DIDT } from '../../../lib/constants';
3 | import { createExpectedBearerStringError } from '../../../../src/core/sdk-exceptions';
4 |
5 | test('Successfully parses raw DIDT from `Bearer` authorization header', async () => {
6 | const sdk = createMagicAdminSDK();
7 | const result = sdk.utils.parseAuthorizationHeader(`Bearer ${VALID_DIDT}`);
8 | expect(result).toEqual(VALID_DIDT);
9 | });
10 |
11 | test('Raises error if header is in the wrong format', async () => {
12 | const sdk = createMagicAdminSDK();
13 | const expectedError = createExpectedBearerStringError();
14 | expect(() => sdk.utils.parseAuthorizationHeader(`Ooops ${VALID_DIDT}`)).toThrow(expectedError);
15 | });
16 |
--------------------------------------------------------------------------------
/test/spec/modules/utils/validateTokenOwnership.spec.ts:
--------------------------------------------------------------------------------
1 | import { createMagicAdminSDK } from '../../../lib/factories';
2 |
3 | jest.mock('ethers', () => {
4 | const originalModule = jest.requireActual('ethers');
5 | return {
6 | ...originalModule,
7 | ethers: {
8 | ...originalModule.ethers,
9 | Contract: jest.fn(() => ({
10 | balanceOf: jest.fn().mockImplementation((walletAddress: string, tokenId?: string) => {
11 | if (tokenId === '2') {
12 | return BigInt(1); // User owns token
13 | } else {
14 | return BigInt(0); // User doesn't own token
15 | }
16 | }),
17 | })),
18 | },
19 | };
20 | });
21 |
22 |
23 | test('Throws an error if ERC1155 and no token provided', async () => {
24 | const sdk = createMagicAdminSDK('https://example.com');
25 |
26 | await expect(sdk.utils.validateTokenOwnership('did:ethr:0x123', '0xfoo', 'ERC1155', 'https://example.com')).rejects.toThrow(
27 | 'ERC1155 requires a tokenId',
28 | );
29 | });
30 |
31 | test('Returns an error if DID token is malformed', async () => {
32 | const sdk = createMagicAdminSDK('https://example.com');
33 |
34 | // Mock the magic token validation by setting the code to ERROR_MALFORMED_TOKEN
35 | sdk.token.validate = jest.fn().mockRejectedValue({ code: 'ERROR_MALFORMED_TOKEN' });
36 |
37 | await expect(sdk.utils.validateTokenOwnership('did:ethr:0x123', '0xfoo', 'ERC1155', 'https://example.com', '1')).resolves.toEqual({
38 | valid: false,
39 | error_code: 'UNAUTHORIZED',
40 | message: 'Invalid DID token: ERROR_MALFORMED_TOKEN',
41 | });
42 | });
43 |
44 | test('Returns an error if DID token is expired', async () => {
45 | const sdk = createMagicAdminSDK('https://example.com');
46 |
47 | // Mock the magic token validation by setting the code to ERROR_DIDT_EXPIRED
48 | sdk.token.validate = jest.fn().mockRejectedValue({ code: 'ERROR_DIDT_EXPIRED' });
49 |
50 | await expect(sdk.utils.validateTokenOwnership('did:ethr:0x123', '0xfoo', 'ERC1155', 'https://example.com', '1')).resolves.toEqual({
51 | valid: false,
52 | error_code: 'UNAUTHORIZED',
53 | message: 'Invalid DID token: ERROR_DIDT_EXPIRED',
54 | });
55 | });
56 |
57 | test('Throws an error if DID token validation returns unexpected error code', async () => {
58 | const sdk = createMagicAdminSDK('https://example.com');
59 |
60 | // Mock the magic token validation by setting the code to ERROR_MALFORMED_TOKEN
61 | sdk.token.validate = jest.fn().mockRejectedValue({ code: 'UNKNOWN' });
62 |
63 | await expect(sdk.utils.validateTokenOwnership('did:ethr:0x123', '0xfoo', 'ERC1155', 'https://example.com', '1')).rejects.toThrow();
64 | });
65 |
66 | test('Returns an error if ERC721 token is not owned by user', async () => {
67 | const sdk = createMagicAdminSDK('https://example.com');
68 |
69 | // Mock the magic token validation to return ok
70 | sdk.token.validate = jest.fn().mockResolvedValue({});
71 | // Mock the getPublicAddress to return valid email and wallet
72 | sdk.token.getPublicAddress = jest.fn().mockReturnValue('0x610dcb8fd5cf7f544b85290889a456916fbeaba2');
73 |
74 | await expect(
75 | sdk.utils.validateTokenOwnership(
76 | 'did:ethr:0x123',
77 | '0x610dcb8fd5cf7f544b85290889a456916fbeaba2',
78 | 'ERC721',
79 | 'https://example.com',
80 | '1',
81 | ),
82 | ).resolves.toEqual({
83 | valid: false,
84 | error_code: 'NO_OWNERSHIP',
85 | message: 'User does not own this token.',
86 | });
87 | });
88 |
89 | test('Returns an error if ERC1155 token is not owned by user', async () => {
90 | const sdk = createMagicAdminSDK('https://example.com');
91 |
92 | // Mock the magic token validation to return ok
93 | sdk.token.validate = jest.fn().mockResolvedValue({});
94 | // Mock the getPublicAddress to return valid email and wallet
95 | sdk.token.getPublicAddress = jest.fn().mockReturnValue('0x610dcb8fd5cf7f544b85290889a456916fbeaba2');
96 |
97 | await expect(
98 | sdk.utils.validateTokenOwnership(
99 | 'did:ethr:0x123',
100 | '0x610dcb8fd5cf7f544b85290889a456916fbeaba2',
101 | 'ERC1155',
102 | 'https://example.com',
103 | '1',
104 | ),
105 | ).resolves.toEqual({
106 | valid: false,
107 | error_code: 'NO_OWNERSHIP',
108 | message: 'User does not own this token.',
109 | });
110 | });
111 |
112 | test('Returns success if ERC1155 token is owned by user', async () => {
113 | const sdk = createMagicAdminSDK('https://example.com');
114 |
115 | // Mock the magic token validation to return ok
116 | sdk.token.validate = jest.fn().mockResolvedValue({});
117 | // Mock the getPublicAddress to return valid email and wallet
118 | sdk.token.getPublicAddress = jest.fn().mockReturnValue('0x610dcb8fd5cf7f544b85290889a456916fbeaba2');
119 |
120 | await expect(
121 | sdk.utils.validateTokenOwnership(
122 | 'did:ethr:0x123',
123 | '0x610dcb8fd5cf7f544b85290889a456916fbeaba2',
124 | 'ERC1155',
125 | 'https://example.com',
126 | '2',
127 | ),
128 | ).resolves.toEqual({
129 | valid: true,
130 | error_code: '',
131 | message: '',
132 | });
133 | });
134 |
--------------------------------------------------------------------------------
/test/spec/utils/codec.spec.ts:
--------------------------------------------------------------------------------
1 | import * as codec from '../../../src/utils/codec';
2 | import { DECODED_DIDT, VALID_DIDT } from '../../lib/constants';
3 |
4 | test('Decoding', async () => {
5 | const decodedString = codec.decodeValue(VALID_DIDT);
6 |
7 | expect(decodedString).toBe(DECODED_DIDT);
8 | });
9 |
10 | test('Decoding malformed string', async () => {
11 | const decodedString = codec.decodeValue(`tRollGoat${VALID_DIDT}`);
12 |
13 | expect(decodedString).not.toBe(DECODED_DIDT);
14 | });
15 |
16 | test('Decoding null value', async () => {
17 | const decodedString = codec.decodeValue(null);
18 |
19 | expect(decodedString).toBe('');
20 | });
21 |
22 | test('Decoding undefined', async () => {
23 | const decodedString = codec.decodeValue(undefined);
24 |
25 | expect(decodedString).toBe('');
26 | });
27 |
28 | it('should return decoded value if running in browser', () => {
29 | // Mocking the window object
30 | (global as any).window = {
31 | atob: jest.fn().mockReturnValue(DECODED_DIDT) // Mocking atob method
32 | };
33 |
34 | jest.spyOn(codec, 'isBrowser').mockReturnValue(true);
35 |
36 | expect(codec.decodeValue(VALID_DIDT)).toBe(DECODED_DIDT);
37 | });
--------------------------------------------------------------------------------
/test/spec/utils/issuer/generateIssuerFromPublicAddress.spec.ts:
--------------------------------------------------------------------------------
1 | import { generateIssuerFromPublicAddress } from '../../../../src/utils/issuer';
2 |
3 | test('Successfully builds issuer string from public address', async () => {
4 | const result = generateIssuerFromPublicAddress('0x1234');
5 | expect(result).toBe('did:ethr:0x1234');
6 | });
7 |
8 | test('Successfully builds issuer string from public address with overrided method', async () => {
9 | const result = generateIssuerFromPublicAddress('0x1234', 'test');
10 | expect(result).toBe('did:test:0x1234');
11 | });
12 |
--------------------------------------------------------------------------------
/test/spec/utils/issuer/parsePublicAddressFromIssuer.spec.ts:
--------------------------------------------------------------------------------
1 | import { parsePublicAddressFromIssuer } from '../../../../src/utils/issuer';
2 |
3 | test('Successfully parses public address from issuer string', async () => {
4 | const result = parsePublicAddressFromIssuer('did:ethr:0x1234');
5 | expect(result).toBe('0x1234');
6 | });
7 |
8 | test('Returns empty string if public address fails to parse', async () => {
9 | // Notice that public address should be undefined
10 | const result = parsePublicAddressFromIssuer('did:ethr');
11 | expect(result).toBe('');
12 | });
13 |
--------------------------------------------------------------------------------
/test/spec/utils/parse-didt.spec.ts:
--------------------------------------------------------------------------------
1 | import { VALID_DIDT, VALID_DIDT_DECODED, INVALID_DIDT_MALFORMED_CLAIM } from '../../lib/constants';
2 | import { createMalformedTokenError } from '../../../src/core/sdk-exceptions';
3 | import { parseDIDToken } from '../../../src/utils/parse-didt';
4 |
5 | test('Successfully parses DIDT', async () => {
6 | const result = parseDIDToken(VALID_DIDT);
7 | expect(result.withParsedClaim).toEqual(VALID_DIDT_DECODED);
8 | });
9 |
10 | test('Throws error if token is malformed', async () => {
11 | const expectedError = createMalformedTokenError();
12 | expect(() => parseDIDToken(INVALID_DIDT_MALFORMED_CLAIM)).toThrow(expectedError);
13 | });
14 |
--------------------------------------------------------------------------------
/test/spec/utils/rest/emitRequest.spec.ts:
--------------------------------------------------------------------------------
1 | import { fetch } from '../../../../src/utils/fetch';
2 | import { API_KEY } from '../../../lib/constants';
3 | import { get } from '../../../../src/utils/rest';
4 | import { createServiceError } from '../../../../src/core/sdk-exceptions';
5 |
6 | /*
7 | We test remaining code paths to the private function `emitRequest` via the
8 | public `get` function.
9 | */
10 |
11 | const URL = 'https://example.com/hello/world';
12 |
13 | const failWithTypeError = Promise.resolve({
14 | json: undefined,
15 | });
16 |
17 | const failWithBadStatus = Promise.resolve({
18 | json: () =>
19 | Promise.resolve({
20 | status: 'qwerty', // Only 'ok' with succeed
21 | }),
22 | });
23 |
24 | const failWithEmptyStatus = Promise.resolve({
25 | json: () =>
26 | Promise.resolve({
27 | // No status defined will assume non-'ok'
28 | }),
29 | });
30 |
31 | const successResEmptyData = Promise.resolve({
32 | json: () =>
33 | Promise.resolve({
34 | status: 'ok',
35 | }),
36 | });
37 |
38 | test('Fails with TypeError if `res.json` is undefined', async () => {
39 | // This test allow us to force `fetch` to catch. This test is primarily for
40 | // coverage purposes. This case should likely never happen.
41 |
42 | const fetchStub = jest.fn().mockImplementation(() => failWithTypeError);
43 | (fetch as any) = fetchStub;
44 |
45 | const expectedError = createServiceError({ status: 'qwerty' });
46 |
47 | await expect(get(URL, API_KEY)).rejects.toThrow(expectedError);
48 | });
49 |
50 | test('Fails with non-OK status in response JSON', async () => {
51 | const fetchStub = jest.fn().mockImplementation(() => failWithBadStatus);
52 | (fetch as any) = fetchStub;
53 |
54 | const expectedError = createServiceError({ status: 'qwerty' });
55 |
56 | await expect(get(URL, API_KEY)).rejects.toThrow(expectedError);
57 | });
58 |
59 | test('Succeeds with empty data in response JSON, returning `{}` as fallback', async () => {
60 | const fetchStub = jest.fn().mockImplementation(() => successResEmptyData);
61 | (fetch as any) = fetchStub;
62 | await expect(get(URL, API_KEY)).resolves.toEqual({});
63 | });
64 |
65 | test('Fails with empty status in response', async () => {
66 | const fetchStub = jest.fn().mockImplementation(() => failWithEmptyStatus);
67 | (fetch as any) = fetchStub;
68 |
69 | const expectedError = createServiceError({ status: 'qwerty' });
70 |
71 | expect(get(URL, API_KEY)).rejects.toThrow(expectedError);
72 | });
73 |
--------------------------------------------------------------------------------
/test/spec/utils/rest/get.spec.ts:
--------------------------------------------------------------------------------
1 | import { fetch } from '../../../../src/utils/fetch';
2 | import { API_KEY } from '../../../lib/constants';
3 | import { get } from '../../../../src/utils/rest';
4 |
5 | const successRes = Promise.resolve({
6 | json: () =>
7 | Promise.resolve({
8 | data: 'hello world',
9 | status: 'ok',
10 | }),
11 | });
12 |
13 | test('Successfully GETs to the given endpoint & stringifies query params', async () => {
14 | const fetchStub = jest.fn().mockImplementation(() => successRes);
15 | (fetch as any) = fetchStub;
16 | await expect(get('https://example.com/hello/world', API_KEY, { foo: 'hello', bar: 'world' })).resolves.toBe(
17 | 'hello world',
18 | );
19 |
20 | const fetchArguments = fetchStub.mock.calls[0];
21 | expect(fetchArguments).toEqual([
22 | 'https://example.com/hello/world?foo=hello&bar=world',
23 | {
24 | method: 'GET',
25 | headers: { 'X-Magic-Secret-key': API_KEY },
26 | },
27 | ]);
28 | });
29 |
30 | test('Successfully GETs to the given endpoint with no query params', async () => {
31 | const fetchStub = jest.fn().mockImplementation(() => successRes);
32 | (fetch as any) = fetchStub;
33 |
34 | await expect(get('https://example.com/hello/world', API_KEY)).resolves.toBe('hello world');
35 |
36 | const fetchArguments = fetchStub.mock.calls[0];
37 | expect(fetchArguments).toEqual([
38 | 'https://example.com/hello/world',
39 | {
40 | method: 'GET',
41 | headers: { 'X-Magic-Secret-key': API_KEY },
42 | },
43 | ]);
44 | });
45 |
--------------------------------------------------------------------------------
/test/spec/utils/rest/post.spec.ts:
--------------------------------------------------------------------------------
1 | import fetch from 'node-fetch';
2 | import { API_KEY } from '../../../lib/constants';
3 | import { post } from '../../../../src/utils/rest';
4 |
5 | const successRes = Promise.resolve({
6 | json: () =>
7 | Promise.resolve({
8 | data: 'hello world',
9 | status: 'ok',
10 | }),
11 | });
12 |
13 | test('Successfully POSTs to the given endpoint & stringifies body', async () => {
14 | const fetchStub = jest.fn().mockImplementation(() => successRes);
15 | (fetch as any) = fetchStub;
16 |
17 | await expect(post('https://example.com/hello/world', API_KEY, { public_address: '0x0123' })).resolves.toBe(
18 | 'hello world',
19 | );
20 |
21 | const fetchArguments = fetchStub.mock.calls[0];
22 | expect(fetchArguments).toEqual([
23 | 'https://example.com/hello/world',
24 | {
25 | method: 'POST',
26 | headers: { 'X-Magic-Secret-key': API_KEY },
27 | body: '{"public_address":"0x0123"}',
28 | },
29 | ]);
30 | });
31 |
--------------------------------------------------------------------------------
/test/spec/utils/type-guards/isDIDTClaim.spec.ts:
--------------------------------------------------------------------------------
1 | import { isDIDTClaim } from '../../../../src/utils/type-guards';
2 |
3 | test('Returns false given `undefined`', async () => {
4 | expect(isDIDTClaim(undefined)).toBe(false);
5 | });
6 |
7 | test('Returns false given `null`', async () => {
8 | expect(isDIDTClaim(null)).toBe(false);
9 | });
10 |
11 | test('Returns false given without `Claim.iat`', async () => {
12 | expect(isDIDTClaim({ ext: 123, iss: 'asdf', sub: 'asdf', aud: 'asdf', nbf: 123, tid: 'asdf', add: '0x0123' })).toBe(
13 | false,
14 | );
15 | });
16 |
17 | test('Returns false given without `Claim.ext`', async () => {
18 | expect(isDIDTClaim({ iat: 123, iss: 'asdf', sub: 'asdf', aud: 'asdf', nbf: 123, tid: 'asdf', add: '0x0123' })).toBe(
19 | false,
20 | );
21 | });
22 |
23 | test('Returns false given without `Claim.iss`', async () => {
24 | expect(isDIDTClaim({ iat: 123, ext: 123, sub: 'asdf', aud: 'asdf', nbf: 123, tid: 'asdf', add: '0x0123' })).toBe(
25 | false,
26 | );
27 | });
28 |
29 | test('Returns false given without `Claim.sub`', async () => {
30 | expect(isDIDTClaim({ iat: 123, ext: 123, iss: 'asdf', aud: 'asdf', nbf: 123, tid: 'asdf', add: '0x0123' })).toBe(
31 | false,
32 | );
33 | });
34 |
35 | test('Returns false given without `Claim.aud`', async () => {
36 | expect(isDIDTClaim({ iat: 123, ext: 123, iss: 'asdf', sub: 'asdf', nbf: 123, tid: 'asdf', add: '0x0123' })).toBe(
37 | false,
38 | );
39 | });
40 |
41 | test('Returns false given without `Claim.nbf`', async () => {
42 | expect(isDIDTClaim({ iat: 123, ext: 123, iss: 'asdf', sub: 'asdf', aud: 'asdf', tid: 'asdf', add: '0x0123' })).toBe(
43 | false,
44 | );
45 | });
46 |
47 | test('Returns false given without `Claim.tid`', async () => {
48 | expect(isDIDTClaim({ iat: 123, ext: 123, iss: 'asdf', sub: 'asdf', aud: 'asdf', nbf: 123, add: '0x0123' })).toBe(
49 | false,
50 | );
51 | });
52 |
53 | test('Returns false given without `Claim.add`', async () => {
54 | expect(isDIDTClaim({ iat: 123, ext: 123, iss: 'asdf', sub: 'asdf', aud: 'asdf', nbf: 123, tid: 'asdf' })).toBe(false);
55 | });
56 |
57 | test('Returns true given with all required properties', async () => {
58 | expect(
59 | isDIDTClaim({ iat: 123, ext: 123, iss: 'asdf', sub: 'asdf', aud: 'asdf', nbf: 123, tid: 'asdf', add: '0x0123' }),
60 | ).toBe(true);
61 | });
62 |
--------------------------------------------------------------------------------
/test/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../config/tsconfig.test.json",
3 | "compilerOptions": {
4 | "target": "es5"
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./config/tsconfig.base.json",
3 | "compilerOptions": {
4 | "target": "es5"
5 | }
6 | }
7 |
--------------------------------------------------------------------------------