Skip to content

Commit 5ed8798

Browse files
shamilovtimLiranCohen
andauthoredAug 13, 2024··
introduce wallet connect (#713)
* add back examples * add back authServer.js * add back json-rpc changes * add back rpcserver * add back utils * add back agent exports * add back ws-rpc-server * add back auth-api * add back level.js * add back crypto randompin * add back test for randomPin * simplify request-uri * make testharness setup a little more readable * add pollWithTTL * add abort to pollWithTTL * add jsdoc to pollWithTTL * WIP * fix a couple of docs * param style nit * fix signing and migrate kms code to newer agent code * use the word "claims" rather than payload * format authServer and update code * push up latest WIP * remove dead code * bump types/node * cleanup * fix eslint * format test file * format with eslint * cleanup * format eslint config * Update authServer.js * Update tsconfig.json * check in wip * add correct wallet uri * comment * updates * cleanup * cleanup * add nearly finished latest wip * cleanup. add walletUri. * feedback * feedback * feedback * refactor out nonce * feedback * feedback: add better docs for walletUri * remove unnecessary clientUri * feedback * resolve merge conflict * feedback: client_id should contain the did * improve comment about walletconnectoptions * feedback: unabstract nonce creation * remove unused imports * feedback: slim down queryparams * merged lockfile * clarify comment about the grants * feedback: fail fast and let users catch errors * feedback * push up finalizations * bump crypto * examples * bump sinon * fix dependabot mess * try to fix builds * try to fix builds * use v9 lockfile * Revert "try to fix builds" This reverts commit d63c468. * Revert "try to fix builds" This reverts commit 3a58665. * Create flat-students-compare.md * fix ci * fix lockfile * Revert "fix lockfile" This reverts commit 775d15a. * fix lockfile * cleanup * fix ci * Delete authServer.js * test ci * fix build order * fix cve * fix eslint * bump lockfile * feedback * use dwn server default port * Update flat-students-compare.md * Update docs-ci.yml * Update flat-students-compare.md * stub globalthis fetch * Update tests-ci.yml * fix regex * cleanup * cleanup * add some patch tests * cleanup some changes * satisfy codecov patch * fix codecov bot * Update tests-ci.yml * latest * prettier fmt * Update wallet-connect.html * Update codecov.yml * add wallet connect example. change to portableDid data structure and delegateDid naming. * add connectedDid * cleanup example * cleanup * Update wallet-connect.html * add word wrap and viewport sizing * remove conditional returns in buildOidcUrl * timeout * feedback * Update oidc.ts * Update connect.ts * Update connect.ts * only one did for selection * feedback * Update packages/crypto/tests/utils.spec.ts Co-authored-by: Liran Cohen <c.liran.c@gmail.com> * Update packages/crypto/tests/utils.spec.ts Co-authored-by: Liran Cohen <c.liran.c@gmail.com> * fix flakes * Update connect.ts * Update connect.ts * run prettier and eslint * remove corepack * delegate did should use a did jwk * client should use a did jwk * better comments * Update oidc.ts * add comments * Update oidc.ts * cleanup comments * add some coverage * reorganize * Update web5.spec.ts * feedback: dont encrypt with the code challenge * feedback disable code challenge * clean out todo * Update connect.spec.ts * feedback didjwk * cleanup crypto utils (#830) * cleanup crypto utils * changeset * Update index.ts * finish: delete package.json utils export * add docs errors back * disable rule until typedoc is bumped * Revert "cleanup crypto utils (#830)" This reverts commit fc234c3. * renable typedoc * Update docs-ci.yml * update codeowners * Update CODEOWNERS --------- Co-authored-by: Liran Cohen <c.liran.c@gmail.com>
1 parent 1fee7a2 commit 5ed8798

35 files changed

+3671
-2121
lines changed
 

‎.changeset/flat-students-compare.md

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
"@web5/agent": minor
3+
"@web5/api": patch
4+
"@web5/credentials": patch
5+
"@web5/crypto": patch
6+
"@web5/crypto-aws-kms": patch
7+
"@web5/dids": patch
8+
"@web5/identity-agent": patch
9+
"@web5/proxy-agent": patch
10+
"@web5/user-agent": patch
11+
---
12+
13+
introduce initial web5 connect implementation
14+
bump crypto

‎.github/workflows/docs-ci.yml

+2-2
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,8 @@ jobs:
3636
with:
3737
token: ${{ secrets.GITHUB_TOKEN }}
3838
report_changed_scope_only: false
39-
fail_on_warnings: true
40-
fail_on_error: true
39+
fail_on_warnings: false
40+
fail_on_error: false
4141
group_docs: true
4242
entry_points: |
4343
- file: packages/api/src/index.ts

‎.github/workflows/reports.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ jobs:
3434
echo "DWN_SERVER_BACKGROUND_PROCESS=$!" >> $GITHUB_ENV
3535
3636
- name: Build tests for all packages
37-
run: pnpm --recursive --stream --sequential build:tests:node
37+
run: pnpm --recursive --stream build:tests:node
3838

3939
- name: Run tests for all packages
4040
run: pnpm --recursive --stream exec c8 mocha -- --color --reporter mocha-junit-reporter --reporter-options mochaFile=./results.xml

‎.github/workflows/tests-ci.yml

+1-7
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ jobs:
5757

5858
- name: Run dwn-server (background)
5959
run: |
60-
node node_modules/@web5/dwn-server/dist/esm/src/main.js &
60+
npx @web5/dwn-server &
6161
echo "DWN_SERVER_BACKGROUND_PROCESS=$!" >> $GITHUB_ENV
6262
6363
- name: Build tests for all packages
@@ -106,12 +106,6 @@ jobs:
106106
with:
107107
cache: "true"
108108

109-
- name: Print Node.js, npm, & pnpm versions for debugging if needed
110-
run: |
111-
node -v
112-
npm -v
113-
pnpm -v
114-
115109
- name: Install dependencies
116110
run: pnpm install --no-frozen-lockfile
117111

‎.vscode/settings.json

+8-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
11
{
2-
"npm.packageManager": "pnpm"
2+
"npm.packageManager": "pnpm",
3+
"editor.formatOnSave":true,
4+
"eslint.useFlatConfig": true,
5+
"eslint.lintTask.enable": true,
6+
"eslint.workingDirectories": [{ "mode": "auto" }],
7+
"eslint.format.enable": true,
8+
"[javascript]": { "editor.defaultFormatter": "dbaeumer.vscode-eslint" },
9+
"[typescript]": { "editor.defaultFormatter": "dbaeumer.vscode-eslint" },
310
}

‎CODEOWNERS

+11-11
Original file line numberDiff line numberDiff line change
@@ -10,20 +10,20 @@
1010

1111
# These are owners who can approve folders under the root directory and other CICD and QoL directories.
1212
# Should be the union list of all owners of sub-directories, optionally minus the default owners.
13-
/* @csuwildcat @lirancohen @thehenrytsai @diehuxx @shamilovtim @nitro-neal
14-
/.changeset @csuwildcat @lirancohen @thehenrytsai @diehuxx @shamilovtim @nitro-neal
15-
/.codesandbox @csuwildcat @lirancohen @thehenrytsai @diehuxx @shamilovtim @nitro-neal
16-
/.github @csuwildcat @lirancohen @thehenrytsai @diehuxx @shamilovtim @nitro-neal
17-
/.vscode @csuwildcat @lirancohen @thehenrytsai @diehuxx @shamilovtim @nitro-neal
18-
/scripts @csuwildcat @lirancohen @thehenrytsai @diehuxx @shamilovtim @nitro-neal
13+
/* @csuwildcat @lirancohen @thehenrytsai @shamilovtim @nitro-neal
14+
/.changeset @csuwildcat @lirancohen @thehenrytsai @shamilovtim @nitro-neal
15+
/.codesandbox @csuwildcat @lirancohen @thehenrytsai @shamilovtim @nitro-neal
16+
/.github @csuwildcat @lirancohen @thehenrytsai @shamilovtim @nitro-neal
17+
/.vscode @csuwildcat @lirancohen @thehenrytsai @shamilovtim @nitro-neal
18+
/scripts @csuwildcat @lirancohen @thehenrytsai @shamilovtim @nitro-neal
1919

2020
# These are owners of any file in the `common`, `crypto`, `crypto-aws-kms`, `dids`, and
2121
# `credentials` packages and their sub-directories.
22-
/packages/common @csuwildcat @diehuxx @thehenrytsai @nitro-neal
23-
/packages/crypto @csuwildcat @diehuxx @thehenrytsai
24-
/packages/crypto-aws-kms @csuwildcat @diehuxx @thehenrytsai
25-
/packages/dids @csuwildcat @diehuxx @thehenrytsai @nitro-neal
26-
/packages/credentials @csuwildcat @diehuxx @thehenrytsai @nitro-neal
22+
/packages/common @csuwildcat @thehenrytsai @nitro-neal
23+
/packages/crypto @csuwildcat @thehenrytsai @nitro-neal
24+
/packages/crypto-aws-kms @csuwildcat @thehenrytsai @nitro-neal
25+
/packages/dids @csuwildcat @thehenrytsai @nitro-neal
26+
/packages/credentials @csuwildcat @thehenrytsai @nitro-neal
2727

2828
# These are owners of any file in the `agent`, `user-agent`, `proxy-agent`, `identity-agent`, and
2929
# `api` packages and their sub-directories.

‎bin/corepack

-1
This file was deleted.

‎codecov.yml

+2-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ component_management:
88
target: auto # auto compares coverage to the previous base commit
99
threshold: 5% # allows a 5% drop from the previous base commit coverage
1010
- type: patch
11-
target: 90 # every PR opened should strive for at least 90% coverage
11+
target: 90
1212

1313
individual_components:
1414
- component_id: package-agent
@@ -59,3 +59,4 @@ coverage:
5959
patch:
6060
default:
6161
informational: true # Don't gate PRs based on Codecov passing thresholds
62+
if_ci_failed: success

‎eslint.config.cjs

+76-82
Original file line numberDiff line numberDiff line change
@@ -1,92 +1,86 @@
1-
const eslint = require('@eslint/js');
2-
const globals = require('globals');
3-
const tsParser = require('@typescript-eslint/parser');
4-
const tsPlugin = require('@typescript-eslint/eslint-plugin');
5-
const mochaPlugin = require('eslint-plugin-mocha');
1+
const eslint = require("@eslint/js");
2+
const globals = require("globals");
3+
const tsParser = require("@typescript-eslint/parser");
4+
const tsPlugin = require("@typescript-eslint/eslint-plugin");
5+
const mochaPlugin = require("eslint-plugin-mocha");
66

77
/** @type {import('eslint').ESLint.ConfigData} */
88
module.exports = [
99
eslint.configs.recommended,
1010
mochaPlugin.configs.flat.recommended,
1111
// tsPlugin.configs.flat.recommended, // @typescript-eslint v7.9.0 doesn't have a recommended config yet, v8 alpha build has it, so should be available soon.
1212
{
13-
// extends : ['eslint:recommended', 'plugin:@typescript-eslint/recommended'],
14-
languageOptions: {
15-
parser: tsParser,
16-
parserOptions: {
17-
ecmaFeatures: { modules: true },
18-
ecmaVersion: '2022',
19-
project: [
20-
'tests/tsconfig.json'// this is the config that includes both `src` and `tests` directories
21-
]
13+
// extends : ['eslint:recommended', 'plugin:@typescript-eslint/recommended'],
14+
languageOptions: {
15+
parser: tsParser,
16+
parserOptions: {
17+
ecmaFeatures: { modules: true },
18+
ecmaVersion: "2022",
19+
project: [
20+
"tests/tsconfig.json", // this is the config that includes both `src` and `tests` directories
21+
],
22+
},
23+
globals: {
24+
...globals.node,
25+
...globals.es2021,
26+
...globals.browser,
27+
console: "readonly",
28+
},
29+
},
30+
plugins: {
31+
"@typescript-eslint": tsPlugin,
32+
mocha: mochaPlugin,
33+
},
34+
files: ["**/*.ts"],
35+
// IMPORTANT and confusing: `ignores` only exclude files from the `files` setting.
36+
// To exclude *.js files entirely, you need to have a separate config object altogether. (See another `ignores` below.)
37+
ignores: ["**/*.d.ts"],
38+
rules: {
39+
"no-undef": "off",
40+
"no-redeclare": "off",
41+
"key-spacing": [
42+
"error",
43+
{
44+
align: {
45+
afterColon: true,
46+
beforeColon: true,
47+
on: "colon",
48+
},
49+
},
50+
],
51+
quotes: ["error", "single", { allowTemplateLiterals: true }],
52+
semi: ["error", "always"],
53+
indent: ["error", 2, { SwitchCase: 1 }],
54+
"no-unused-vars": "off",
55+
"prefer-const": "off",
56+
"@typescript-eslint/no-unused-vars": [
57+
"error",
58+
{
59+
vars: "all",
60+
args: "after-used",
61+
ignoreRestSiblings: true,
62+
argsIgnorePattern: "^_",
63+
varsIgnorePattern: "^_",
64+
},
65+
],
66+
"no-dupe-class-members": "off",
67+
"no-trailing-spaces": ["error"],
68+
"@typescript-eslint/no-explicit-any": "off",
69+
"@typescript-eslint/no-non-null-assertion": "off",
70+
"@typescript-eslint/ban-ts-comment": "off",
71+
"@typescript-eslint/no-unused-vars": "off",
72+
// TODO: Revisit new default mocha rules that were disabled in #579 - https://github.com/TBD54566975/web5-js/issues/580
73+
"mocha/no-exclusive-tests": "warn",
74+
"mocha/no-setup-in-describe": "off",
75+
"mocha/no-mocha-arrows": "off",
76+
"mocha/max-top-level-suites": "off",
77+
"mocha/no-identical-title": "off",
78+
"mocha/no-pending-tests": "off",
79+
"mocha/no-skipped-tests": "off",
80+
"mocha/no-sibling-hooks": "off",
2281
},
23-
globals: {
24-
...globals.node,
25-
...globals.es2021,
26-
...globals.browser
27-
}
2882
},
29-
plugins: {
30-
'@typescript-eslint': tsPlugin,
31-
'mocha': mochaPlugin
83+
{
84+
ignores: ["**/*.js", "**/*.cjs", "**/*.mjs"],
3285
},
33-
files: [
34-
'**/*.ts'
35-
],
36-
// IMPORTANT and confusing: `ignores` only exclude files from the `files` setting.
37-
// To exclude *.js files entirely, you need to have a separate config object altogether. (See another `ignores` below.)
38-
ignores: [
39-
'**/*.d.ts',
40-
],
41-
rules: {
42-
'key-spacing': [
43-
'error',
44-
{
45-
'align': {
46-
'afterColon' : true,
47-
'beforeColon' : true,
48-
'on' : 'colon'
49-
}
50-
}
51-
],
52-
'quotes': [
53-
'error',
54-
'single',
55-
{ 'allowTemplateLiterals': true }
56-
],
57-
'semi' : ['error', 'always'],
58-
'indent' : ['error', 2, { 'SwitchCase': 1 }],
59-
'no-unused-vars' : 'off',
60-
'prefer-const' : 'off',
61-
'@typescript-eslint/no-unused-vars' : [
62-
'error',
63-
{
64-
'vars' : 'all',
65-
'args' : 'after-used',
66-
'ignoreRestSiblings' : true,
67-
'argsIgnorePattern' : '^_',
68-
'varsIgnorePattern' : '^_'
69-
}
70-
],
71-
'no-dupe-class-members' : 'off',
72-
'no-trailing-spaces' : ['error'],
73-
'@typescript-eslint/no-explicit-any' : 'off',
74-
'@typescript-eslint/no-non-null-assertion' : 'off',
75-
'@typescript-eslint/ban-ts-comment' : 'off',
76-
// TODO: Revisit new default mocha rules that were disabled in #579 - https://github.com/TBD54566975/web5-js/issues/580
77-
'mocha/no-exclusive-tests' : 'warn',
78-
'mocha/no-setup-in-describe' : 'off',
79-
'mocha/no-mocha-arrows' : 'off',
80-
'mocha/max-top-level-suites' : 'off',
81-
'mocha/no-identical-title' : 'off',
82-
'mocha/no-pending-tests' : 'off',
83-
'mocha/no-skipped-tests' : 'off',
84-
'mocha/no-sibling-hooks' : 'off',
85-
}
86-
}, {
87-
ignores: [
88-
'**/*.js',
89-
'**/*.cjs',
90-
'**/*.mjs',
91-
],
92-
}];
86+
];

‎examples/wallet-connect.html

+224
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
4+
<head>
5+
<meta charset="UTF-8" />
6+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
7+
<title>Web5 Connect Example</title>
8+
<style>
9+
html,
10+
body {
11+
margin: 0;
12+
padding: 0;
13+
width: 100%;
14+
overflow-x: hidden;
15+
}
16+
17+
/* Ensure all elements respect the box model */
18+
* {
19+
box-sizing: border-box;
20+
}
21+
22+
/* Additional styles to make sure no element causes overflow */
23+
.container {
24+
max-width: 100%;
25+
padding: 16px;
26+
overflow-x: hidden;
27+
}
28+
29+
/* Make text wrap correctly */
30+
p,
31+
a {
32+
word-wrap: break-word;
33+
overflow-wrap: break-word;
34+
white-space: normal;
35+
}
36+
37+
/* Hide screens initially */
38+
#qrCodeScreen,
39+
#pinScreen,
40+
#endScreen {
41+
display: none;
42+
}
43+
44+
.loader {
45+
width: 40px;
46+
height: 40px;
47+
border: 4px solid #ccc;
48+
border-top: 4px solid #3498db;
49+
border-radius: 50%;
50+
animation: spin 1s linear infinite;
51+
}
52+
53+
@keyframes spin {
54+
0% {
55+
transform: rotate(0deg);
56+
}
57+
58+
100% {
59+
transform: rotate(360deg);
60+
}
61+
}
62+
</style>
63+
</head>
64+
65+
<body>
66+
<div class="container">
67+
<!-- Loading Screen -->
68+
<div id="loadingScreen">
69+
<h1>Loading...</h1>
70+
<div class="loader"></div>
71+
</div>
72+
73+
<!-- QR Code Screen -->
74+
<div id="qrCodeScreen">
75+
<h1>Scan with a web5 compatible wallet</h1>
76+
<div id="qrCode"></div>
77+
<div>
78+
<a id="qrCodeText" target="_blank" href=""></a>
79+
</div>
80+
</div>
81+
82+
<!-- Pin Screen -->
83+
<div id="pinScreen">
84+
<h1>Pin Entry</h1>
85+
<form id="pinForm">
86+
<label for="pinInput">Enter Pin:</label>
87+
<input type="text" id="pinInput" name="pinInput" required />
88+
<button type="button" id="submitButton">Send</button>
89+
</form>
90+
</div>
91+
92+
<!-- End Screen -->
93+
<div id="endScreen">
94+
<h1>Success</h1>
95+
<p>You have connected the DID from your wallet.</p>
96+
<p id="didInformation"></p>
97+
</div>
98+
99+
<!-- Error message -->
100+
<p id="errorMessage"></p>
101+
</div>
102+
103+
<script src="https://cdn.jsdelivr.net/npm/qrcodejs/qrcode.min.js"></script>
104+
105+
<script type="module">
106+
import { Web5 } from "/packages/api/dist/browser.mjs";
107+
108+
initListeners();
109+
110+
const profileProtocol = {
111+
protocol: "http://profile-protocol.xyz",
112+
published: true,
113+
types: {
114+
profile: {
115+
schema: "http://profile-protocol.xyz/schema/profile",
116+
dataFormats: ["application/json"],
117+
},
118+
},
119+
structure: {
120+
profile: {
121+
$actions: [
122+
{
123+
who: "anyone",
124+
can: ["create", "update"],
125+
},
126+
],
127+
},
128+
},
129+
};
130+
131+
const scopes = [
132+
{
133+
interface: "Records",
134+
method: "Write",
135+
protocol: "http://profile-protocol.xyz",
136+
},
137+
{
138+
interface: "Records",
139+
method: "Query",
140+
protocol: "http://profile-protocol.xyz",
141+
},
142+
];
143+
144+
try {
145+
const { delegateDid } = await Web5.connect({
146+
walletConnectOptions: {
147+
walletUri: "web5://connect",
148+
connectServerUrl: "http://localhost:3000/connect",
149+
permissionRequests: [
150+
{
151+
protocolDefinition: profileProtocol,
152+
permissionScopes: scopes,
153+
},
154+
],
155+
onWalletUriReady: generateQRCode,
156+
validatePin: async () => {
157+
goToPinScreen();
158+
159+
const pin = await waitForPin();
160+
return pin;
161+
},
162+
},
163+
});
164+
165+
goToEndScreen(delegateDid);
166+
} catch (e) {
167+
document.getElementById(
168+
"errorMessage"
169+
).innerText = `Wallet connect failed. ${e.message}`;
170+
console.error(e.message);
171+
}
172+
173+
function generateQRCode(text) {
174+
new QRCode(document.getElementById("qrCode"), text);
175+
document.getElementById("qrCodeText").setAttribute("href", text);
176+
document.getElementById("qrCodeText").innerText = text;
177+
goToQRCodeScreen();
178+
}
179+
180+
function waitForPin() {
181+
return new Promise((resolve) => {
182+
const handlePinEntered = (event) => {
183+
const pin = event.detail.pin;
184+
resolve(pin);
185+
window.removeEventListener("pinEntered", handlePinEntered);
186+
};
187+
window.addEventListener("pinEntered", handlePinEntered);
188+
});
189+
}
190+
191+
function onSubmitPinClicked(event) {
192+
event.preventDefault();
193+
const pin = document.getElementById("pinInput").value;
194+
const eventObj = new CustomEvent("pinEntered", { detail: { pin } });
195+
window.dispatchEvent(eventObj);
196+
}
197+
198+
function goToQRCodeScreen() {
199+
document.getElementById("loadingScreen").style.display = "none";
200+
document.getElementById("qrCodeScreen").style.display = "block";
201+
}
202+
203+
function goToPinScreen() {
204+
document.getElementById("qrCodeScreen").style.display = "none";
205+
document.getElementById("pinScreen").style.display = "block";
206+
}
207+
208+
function goToEndScreen(delegateDid) {
209+
document.getElementById("didInformation").innerText = `${JSON.stringify(
210+
delegateDid
211+
)}`;
212+
213+
document.getElementById("pinScreen").style.display = "none";
214+
document.getElementById("endScreen").style.display = "block";
215+
}
216+
217+
function initListeners() {
218+
const submitButton = document.getElementById("submitButton");
219+
submitButton.addEventListener("click", onSubmitPinClicked);
220+
}
221+
</script>
222+
</body>
223+
224+
</html>

‎package.json

+4-3
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@
1717
"scripts": {
1818
"clean": "pnpm npkill -d $(pwd)/packages -t dist && pnpm npkill -d $(pwd) -t node_modules",
1919
"build": "pnpm --recursive --stream build",
20-
"dwn-server": "DWN_SERVER_PACKAGE_JSON=node_modules/@web5/dwn-server/package.json node node_modules/@web5/dwn-server/dist/esm/src/main.js || true",
2120
"test:node": "pnpm --recursive test:node",
22-
"audit-ci": "audit-ci --config ./audit-ci.json"
21+
"audit-ci": "audit-ci --config ./audit-ci.json",
22+
"wallet:connect:example": "npx http-server & HTTP_SERVER_PID=$! && sleep 2 && open 'http://localhost:8080/examples/wallet-connect.html' && wait $HTTP_SERVER_PID"
2323
},
2424
"repository": {
2525
"type": "git",
@@ -31,9 +31,10 @@
3131
"@changesets/cli": "^2.27.5",
3232
"@npmcli/package-json": "5.0.0",
3333
"@typescript-eslint/eslint-plugin": "7.9.0",
34-
"@web5/dwn-server": "0.4.3",
34+
"@web5/dwn-server": "0.4.4",
3535
"audit-ci": "^7.0.1",
3636
"eslint-plugin-mocha": "10.4.3",
37+
"globals": "^13.24.0",
3738
"npkill": "0.11.3"
3839
},
3940
"pnpm": {

‎packages/agent/.mocharc.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
"enable-source-maps": true,
33
"exit": true,
44
"spec": ["tests/compiled/**/*.spec.js"]
5-
}
5+
}

‎packages/agent/package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@
7373
"@scure/bip39": "1.2.2",
7474
"@tbd54566975/dwn-sdk-js": "0.4.4",
7575
"@web5/common": "1.0.0",
76-
"@web5/crypto": "1.0.0",
76+
"@web5/crypto": "workspace:*",
7777
"@web5/dids": "1.1.0",
7878
"abstract-level": "1.0.4",
7979
"ed25519-keygen": "0.4.11",
@@ -90,7 +90,7 @@
9090
"@types/eslint": "8.56.10",
9191
"@types/mocha": "10.0.1",
9292
"@types/ms": "0.7.31",
93-
"@types/node": "20.11.19",
93+
"@types/node": "20.14.8",
9494
"@types/sinon": "17.0.3",
9595
"@typescript-eslint/eslint-plugin": "7.9.0",
9696
"@typescript-eslint/parser": "7.14.1",

‎packages/agent/src/connect.ts

+182
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
import { CryptoUtils } from '@web5/crypto';
2+
import { DwnProtocolDefinition, DwnRecordsPermissionScope } from './index.js';
3+
import {
4+
Web5ConnectAuthResponse,
5+
Oidc,
6+
type PushedAuthResponse,
7+
} from './oidc.js';
8+
import { pollWithTtl } from './utils.js';
9+
import { DidJwk } from '@web5/dids';
10+
import { Convert } from '@web5/common';
11+
12+
/**
13+
* Initiates the wallet connect process. Used when a client wants to obtain
14+
* a did from a provider.
15+
*/
16+
async function initClient({
17+
connectServerUrl,
18+
walletUri,
19+
permissionRequests,
20+
onWalletUriReady,
21+
validatePin,
22+
}: WalletConnectOptions) {
23+
// ephemeral client did for ECDH, signing, verification
24+
// TODO: use separate keys for ECDH vs. sign/verify. could maybe use secp256k1.
25+
const clientDid = await DidJwk.create();
26+
27+
// TODO: properly implement PKCE. this implementation is lacking server side validations and more.
28+
// https://github.com/TBD54566975/web5-js/issues/829
29+
// Derive the code challenge based on the code verifier
30+
// const { codeChallengeBytes, codeChallengeBase64Url } =
31+
// await Oidc.generateCodeChallenge();
32+
const encryptionKey = CryptoUtils.randomBytes(32);
33+
34+
// build callback URL to pass into the auth request
35+
const callbackEndpoint = Oidc.buildOidcUrl({
36+
baseURL : connectServerUrl,
37+
endpoint : 'callback',
38+
});
39+
40+
// build the PAR request
41+
const request = await Oidc.createAuthRequest({
42+
client_id : clientDid.uri,
43+
scope : 'openid did:jwk',
44+
// code_challenge : codeChallengeBase64Url,
45+
// code_challenge_method : 'S256',
46+
permissionRequests : permissionRequests,
47+
redirect_uri : callbackEndpoint,
48+
});
49+
50+
// Sign the Request Object using the Client DID's signing key.
51+
const requestJwt = await Oidc.signJwt({
52+
did : clientDid,
53+
data : request,
54+
});
55+
56+
if (!requestJwt) {
57+
throw new Error('Unable to sign requestObject');
58+
}
59+
// Encrypt the Request Object JWT using the code challenge.
60+
const requestObjectJwe = await Oidc.encryptAuthRequest({
61+
jwt: requestJwt,
62+
encryptionKey,
63+
});
64+
65+
// Convert the encrypted Request Object to URLSearchParams for form encoding.
66+
const formEncodedRequest = new URLSearchParams({
67+
request: requestObjectJwe,
68+
});
69+
70+
const pushedAuthorizationRequestEndpoint = Oidc.buildOidcUrl({
71+
baseURL : connectServerUrl,
72+
endpoint : 'pushedAuthorizationRequest',
73+
});
74+
75+
const parResponse = await fetch(pushedAuthorizationRequestEndpoint, {
76+
body : formEncodedRequest,
77+
method : 'POST',
78+
headers : {
79+
'Content-Type': 'application/x-www-form-urlencoded',
80+
},
81+
});
82+
83+
if (!parResponse.ok) {
84+
throw new Error(`${parResponse.status}: ${parResponse.statusText}`);
85+
}
86+
87+
const parData: PushedAuthResponse = await parResponse.json();
88+
89+
// a deeplink to a web5 compatible wallet. if the wallet scans this link it should receive
90+
// a route to its web5 connect provider flow and the params of where to fetch the auth request.
91+
const generatedWalletUri = new URL(walletUri);
92+
generatedWalletUri.searchParams.set('request_uri', parData.request_uri);
93+
generatedWalletUri.searchParams.set('encryption_key', Convert.uint8Array(encryptionKey).toBase64Url());
94+
95+
// call user's callback so they can send the URI to the wallet as they see fit
96+
onWalletUriReady(generatedWalletUri.toString());
97+
98+
const tokenUrl = Oidc.buildOidcUrl({
99+
baseURL : connectServerUrl,
100+
endpoint : 'token',
101+
tokenParam : request.state,
102+
});
103+
104+
// subscribe to receiving a response from the wallet with default TTL. receive ciphertext of {@link Web5ConnectAuthResponse}
105+
const authResponse = await pollWithTtl(() => fetch(tokenUrl));
106+
107+
if (authResponse) {
108+
const jwe = await authResponse?.text();
109+
110+
// get the pin from the user and use it as AAD to decrypt
111+
const pin = await validatePin();
112+
const jwt = await Oidc.decryptAuthResponse(clientDid, jwe, pin);
113+
const verifiedAuthResponse = (await Oidc.verifyJwt({
114+
jwt,
115+
})) as Web5ConnectAuthResponse;
116+
117+
return {
118+
delegateGrants : verifiedAuthResponse.delegateGrants,
119+
delegateDid : verifiedAuthResponse.delegateDid,
120+
connectedDid : verifiedAuthResponse.iss,
121+
};
122+
}
123+
}
124+
125+
/**
126+
* Initiates the wallet connect process. Used when a client wants to obtain
127+
* a did from a provider.
128+
*/
129+
export type WalletConnectOptions = {
130+
/** The URL of the intermediary server which relays messages between the client and provider */
131+
connectServerUrl: string;
132+
133+
/**
134+
* The URI of the Provider (wallet).The `onWalletUriReady` will take this wallet
135+
* uri and add a payload to it which will be used to obtain and decrypt from the `request_uri`.
136+
* @example `web5://` or `http://localhost:3000/`.
137+
*/
138+
walletUri: string;
139+
140+
/**
141+
* The protocols of permissions requested, along with the definition and
142+
* permission scopes for each protocol. The key is the protocol URL and
143+
* the value is an object with the protocol definition and the permission scopes.
144+
*/
145+
permissionRequests: ConnectPermissionRequest[];
146+
147+
/**
148+
* The Web5 API provides a URI to the wallet based on the `walletUri` plus a query params payload valid for 5 minutes.
149+
* The link can either be used as a deep link on the same device or a QR code for cross device or both.
150+
* The query params are `{ request_uri: string; encryption_key: string; }`
151+
* The wallet will use the `request_uri to contact the intermediary server's `authorize` endpoint
152+
* and pull down the {@link Web5ConnectAuthRequest} and use the `encryption_key` to decrypt it.
153+
*
154+
* @param uri - The URI returned by the web5 connect API to be passed to a provider.
155+
*/
156+
onWalletUriReady: (uri: string) => void;
157+
158+
/**
159+
* Function that must be provided to submit the pin entered by the user on the client.
160+
* The pin is used to decrypt the {@link Web5ConnectAuthResponse} that was retrieved from the
161+
* token endpoint by the client inside of web5 connect.
162+
*
163+
* @returns A promise that resolves to the PIN as a string.
164+
*/
165+
validatePin: () => Promise<string>;
166+
};
167+
168+
/**
169+
* The protocols of permissions requested, along with the definition and permission scopes for each protocol.
170+
*/
171+
export type ConnectPermissionRequest = {
172+
/**
173+
* The definition of the protocol the permissions are being requested for.
174+
* In the event that the protocol is not already installed, the wallet will install this given protocol definition.
175+
*/
176+
protocolDefinition: DwnProtocolDefinition;
177+
178+
/** The scope of the permissions being requested for the given protocol */
179+
permissionScopes: DwnRecordsPermissionScope[];
180+
};
181+
182+
export const WalletConnect = { initClient };

‎packages/agent/src/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,5 @@ export * from './sync-api.js';
2424
export * from './sync-engine-level.js';
2525
export * from './test-harness.js';
2626
export * from './utils.js';
27+
export * from './connect.js';
28+
export * from './oidc.js';

‎packages/agent/src/oidc.ts

+729
Large diffs are not rendered by default.

‎packages/agent/src/prototyping/clients/json-rpc.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ export enum JsonRpcErrorCodes {
2929
// App defined errors
3030
BadRequest = -50400, // equivalent to HTTP Status 400
3131
Unauthorized = -50401, // equivalent to HTTP Status 401
32-
Forbidden = -50403, // equivalent to HTTP Status 403
32+
Forbidden = -50403, // equivalent to HTTP Status 403,
33+
Conflict = -50409, // equivalent to HTTP Status 409
3334
}
3435

3536
export type JsonRpcResponse = JsonRpcSuccessResponse | JsonRpcErrorResponse;

‎packages/agent/src/test-harness.ts

+5-9
Original file line numberDiff line numberDiff line change
@@ -43,12 +43,6 @@ type PlatformAgentTestHarnessParams = {
4343
}
4444
}
4545

46-
type PlatformAgentTestHarnessSetupParams = {
47-
agentClass: new (params: any) => Web5PlatformAgent<LocalKeyManager>
48-
agentStores?: 'dwn' | 'memory';
49-
testDataLocation?: string;
50-
}
51-
5246
export class PlatformAgentTestHarness {
5347
public agent: Web5PlatformAgent<LocalKeyManager>;
5448

@@ -176,9 +170,11 @@ export class PlatformAgentTestHarness {
176170
await this.didResolverCache.set(didUri, resolutionResult);
177171
}
178172

179-
public static async setup({ agentClass, agentStores, testDataLocation }:
180-
PlatformAgentTestHarnessSetupParams
181-
): Promise<PlatformAgentTestHarness> {
173+
public static async setup({ agentClass, agentStores, testDataLocation }: {
174+
agentClass: new (params: any) => Web5PlatformAgent<LocalKeyManager>
175+
agentStores?: 'dwn' | 'memory';
176+
testDataLocation?: string;
177+
}): Promise<PlatformAgentTestHarness> {
182178
agentStores ??= 'memory';
183179
testDataLocation ??= '__TESTDATA__';
184180

‎packages/agent/src/utils.ts

+70-2
Original file line numberDiff line numberDiff line change
@@ -87,9 +87,77 @@ export function webReadableToIsomorphicNodeReadable(webReadable: ReadableStream<
8787
}
8888

8989
/**
90-
* Concatenates a base URL and a path, ensuring that there is exactly one slash between them.
91-
* TODO: Move this function to a more common shared utility library across pacakges.
90+
* Polling function with interval, TTL accepting a custom fetch function
91+
* @template T - the return you expect from the fetcher
92+
* @param fetchFunction an http fetch function
93+
* @param [interval=3000] how frequently to poll
94+
* @param [ttl=300_000] how long until polling stops
95+
* @returns T - the result of fetch
9296
*/
97+
export function pollWithTtl(
98+
fetchFunction: () => Promise<Response>,
99+
interval = 3000,
100+
ttl = 300_000,
101+
abortSignal?: AbortSignal
102+
): Promise<Response | null> {
103+
const endTime = Date.now() + ttl;
104+
let timeoutId: NodeJS.Timeout | null = null;
105+
let isPolling = true;
106+
return new Promise((resolve, reject) => {
107+
if (abortSignal) {
108+
abortSignal.addEventListener('abort', () => {
109+
isPolling = false;
110+
if (timeoutId !== null) {
111+
clearTimeout(timeoutId);
112+
}
113+
console.log('Polling aborted by user');
114+
resolve(null);
115+
});
116+
}
117+
118+
async function poll() {
119+
if (!isPolling) return;
120+
121+
const remainingTime = endTime - Date.now();
122+
123+
if (remainingTime <= 0) {
124+
isPolling = false;
125+
console.log('Polling stopped: TTL reached');
126+
resolve(null);
127+
return;
128+
}
129+
130+
console.log(`Polling... (Remaining time: ${Math.ceil(remainingTime / 1000)}s)`);
131+
132+
try {
133+
const response = await fetchFunction();
134+
135+
if (response.ok) {
136+
isPolling = false;
137+
138+
if (timeoutId !== null) {
139+
clearTimeout(timeoutId);
140+
}
141+
142+
console.log('Polling stopped: Success condition met');
143+
resolve(response);
144+
return;
145+
}
146+
} catch (error) {
147+
console.error('Error fetching data:', error);
148+
reject(error);
149+
}
150+
151+
if (isPolling) {
152+
timeoutId = setTimeout(poll, interval);
153+
}
154+
}
155+
156+
poll();
157+
});
158+
}
159+
160+
/** Concatenates a base URL and a path ensuring that there is exactly one slash between them */
93161
export function concatenateUrl(baseUrl: string, path: string): string {
94162
// Remove trailing slash from baseUrl if it exists
95163
if (baseUrl.endsWith('/')) {

‎packages/agent/tests/connect.spec.ts

+467
Large diffs are not rendered by default.

‎packages/api/package.json

+4-4
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@
7979
"dependencies": {
8080
"@web5/agent": "workspace:*",
8181
"@web5/common": "1.0.0",
82-
"@web5/crypto": "1.0.0",
82+
"@web5/crypto": "workspace:*",
8383
"@web5/dids": "1.1.0",
8484
"@web5/user-agent": "workspace:*"
8585
},
@@ -89,8 +89,8 @@
8989
"@types/chai": "4.3.6",
9090
"@types/eslint": "8.56.10",
9191
"@types/mocha": "10.0.1",
92-
"@types/node": "20.11.19",
93-
"@types/sinon": "17.0.2",
92+
"@types/node": "20.14.8",
93+
"@types/sinon": "17.0.3",
9494
"@typescript-eslint/eslint-plugin": "7.9.0",
9595
"@typescript-eslint/parser": "7.14.1",
9696
"@web/test-runner": "0.18.2",
@@ -105,7 +105,7 @@
105105
"node-stdlib-browser": "1.2.0",
106106
"playwright": "1.45.3",
107107
"rimraf": "4.4.0",
108-
"sinon": "16.1.3",
108+
"sinon": "18.0.0",
109109
"source-map-loader": "4.0.2",
110110
"typescript": "5.1.6"
111111
}

‎packages/api/src/web5.ts

+95-114
Original file line numberDiff line numberDiff line change
@@ -1,73 +1,35 @@
1-
import type { BearerIdentity, HdIdentityVault, Web5Agent } from '@web5/agent';
2-
1+
import {
2+
WalletConnect,
3+
type BearerIdentity,
4+
type HdIdentityVault,
5+
type WalletConnectOptions,
6+
type Web5Agent,
7+
} from '@web5/agent';
8+
import { Web5UserAgent } from '@web5/user-agent';
39
import { DidApi } from './did-api.js';
410
import { DwnApi } from './dwn-api.js';
5-
import { DwnRecordsPermissionScope, DwnProtocolDefinition, DwnRegistrar } from '@web5/agent';
11+
import { DwnRegistrar } from '@web5/agent';
612
import { VcApi } from './vc-api.js';
7-
import { Web5UserAgent } from '@web5/user-agent';
13+
import { PortableDid } from '@web5/dids';
814

915
/** Override defaults configured during the technical preview phase. */
1016
export type TechPreviewOptions = {
1117
/** Override default dwnEndpoints provided for technical preview. */
1218
dwnEndpoints?: string[];
13-
}
19+
};
1420

1521
/** Override defaults for DID creation. */
1622
export type DidCreateOptions = {
1723
/** Override default dwnEndpoints provided during DID creation. */
1824
dwnEndpoints?: string[];
1925
}
2026

21-
/**
22-
* Options to provide when the initiating app wants to import a delegated identity/DID from an external wallet.
23-
*/
24-
export type WalletConnectOptions = {
25-
/**
26-
* The URL of the wallet connect server to use for relaying messages between the app and the wallet.
27-
*/
28-
connectServerUrl: string;
29-
30-
/**
31-
* The protocols of permissions requested, along with the definition and permission copes for each protocol.
32-
* The key is the protocol URL and the value is an object with the protocol definition and the permission scopes.
33-
*/
34-
requestedProtocolsAndScopes: Map<
35-
string,
36-
{
37-
/**
38-
* The definition of the protocol the permissions are being requested for.
39-
* In the event that the protocol is not already installed, the wallet will install this given protocol definition.
40-
*/
41-
protocolDefinition: DwnProtocolDefinition;
42-
43-
/**
44-
* The scope of the permissions being requested for the given protocol.
45-
*/
46-
permissionScopes: DwnRecordsPermissionScope[];
47-
}
48-
>;
49-
50-
/**
51-
* A handler to be called when the request URL is ready to be used to fetch the permission request by the wallet.
52-
* This method should be used by the calling app to pass the request URL to the wallet via a QR code or a deep link.
53-
*
54-
* @param requestUrl - The request URL for the wallet to fetch the permission request.
55-
*/
56-
onRequestReady: (requestUrl: string) => void;
57-
58-
/**
59-
* An async method to get the PIN from the user to decrypt the response from the wallet.
60-
*
61-
* @returns A promise that resolves to the PIN as a string.
62-
*/
63-
pinCapture: () => Promise<string>;
64-
}
65-
6627
/** Optional overrides that can be provided when calling {@link Web5.connect}. */
6728
export type Web5ConnectOptions = {
68-
6929
/**
70-
* When specified, wallet connect flow interacting with an external wallet would be triggered.
30+
* When specified, external wallet connect flow is triggered.
31+
* This param currently will not work in apps that are currently connected.
32+
* It must only be invoked at registration with a reset and empty DWN and agent.
7133
*/
7234
walletConnectOptions?: WalletConnectOptions;
7335

@@ -180,6 +142,12 @@ export type Web5ConnectResult = {
180142
* and should be stored securely by the user.
181143
*/
182144
recoveryPhrase?: string;
145+
146+
/**
147+
* The resulting did of a successful wallet connect. Only returned on success if
148+
* {@link WalletConnectOptions} was provided.
149+
*/
150+
delegateDid?: PortableDid
183151
};
184152

185153
/**
@@ -237,7 +205,16 @@ export class Web5 {
237205
* @returns A promise that resolves to a {@link Web5} instance and the connected DID.
238206
*/
239207
static async connect({
240-
agent, agentVault, connectedDid, password, recoveryPhrase, sync, techPreview, didCreateOptions, registration
208+
agent,
209+
agentVault,
210+
connectedDid,
211+
password,
212+
recoveryPhrase,
213+
sync,
214+
techPreview,
215+
didCreateOptions,
216+
registration,
217+
walletConnectOptions,
241218
}: Web5ConnectOptions = {}): Promise<Web5ConnectResult> {
242219
if (agent === undefined) {
243220
// A custom Web5Agent implementation was not specified, so use default managed user agent.
@@ -263,69 +240,74 @@ export class Web5 {
263240
}
264241
await userAgent.start({ password });
265242

266-
// TODO: Replace stubbed connection attempt once Connect Protocol has been implemented.
267-
// Attempt to Connect to localhost agent or via Connect Server.
268-
// userAgent.connect();
269-
270-
const notConnected = true;
271-
if (/* !userAgent.isConnected() */ notConnected) {
272-
// Connect attempt failed or was rejected so fallback to local user agent.
273-
let identity: BearerIdentity;
274-
275-
// Query the Agent's DWN tenant for identity records.
276-
const identities = await userAgent.identity.list();
277-
278-
// If an existing identity is not found found, create a new one.
279-
const existingIdentityCount = identities.length;
280-
if (existingIdentityCount === 0) {
281-
// Use the specified DWN endpoints or the latest TBD hosted DWN
282-
const serviceEndpointNodes = techPreview?.dwnEndpoints ?? didCreateOptions?.dwnEndpoints ?? ['https://dwn.tbddev.org/beta'];
283-
284-
// Generate a new Identity for the end-user.
285-
identity = await userAgent.identity.create({
286-
didMethod : 'dht',
287-
metadata : { name: 'Default' },
288-
didOptions : {
289-
services: [
290-
{
291-
id : 'dwn',
292-
type : 'DecentralizedWebNode',
293-
serviceEndpoint : serviceEndpointNodes,
294-
enc : '#enc',
295-
sig : '#sig',
296-
}
297-
],
298-
verificationMethods: [
299-
{
300-
algorithm : 'Ed25519',
301-
id : 'sig',
302-
purposes : ['assertionMethod', 'authentication']
303-
},
304-
{
305-
algorithm : 'secp256k1',
306-
id : 'enc',
307-
purposes : ['keyAgreement']
308-
}
309-
]
310-
}
311-
});
312-
313-
// The User Agent will manage the Identity, which ensures it will be available on future
314-
// sessions.
315-
await userAgent.identity.manage({ portableIdentity: await identity.export() });
316-
317-
} else if (existingIdentityCount === 1) {
318-
// An existing identity was found in the User Agent's tenant.
319-
identity = identities[0];
320-
321-
} else {
322-
throw new Error(`connect() failed due to unexpected state: Expected 1 but found ${existingIdentityCount} stored identities.`);
243+
let identity: BearerIdentity;
244+
245+
// Query the Agent's DWN tenant for identity records.
246+
const identities = await userAgent.identity.list();
247+
248+
// If an existing identity is not found found, create a new one.
249+
const existingIdentityCount = identities.length;
250+
251+
// on init/registration
252+
if (existingIdentityCount === 0) {
253+
if (walletConnectOptions) {
254+
// WIP: ingest this
255+
const { delegateDid } = await WalletConnect.initClient(walletConnectOptions);
256+
// WIP
257+
// identity = await userAgent.identity.import({
258+
// portableIdentity: {
259+
// portableDid : did,
260+
// metadata : { name: 'Connection', uri: did.uri, tenant: did.uri }
261+
// }
262+
// });
263+
264+
// WIP. just going to early return for now.
265+
return { web5: null, did: null, delegateDid };
323266
}
324267

325-
// Set the stored identity as the connected DID.
326-
connectedDid = identity.did.uri;
268+
// Use the specified DWN endpoints or get default tech preview hosted nodes.
269+
const serviceEndpointNodes = techPreview?.dwnEndpoints ?? didCreateOptions?.dwnEndpoints ?? ['https://dwn.tbddev.org/beta'];
270+
271+
// Generate a new Identity for the end-user.
272+
identity = await userAgent.identity.create({
273+
didMethod : 'dht',
274+
metadata : { name: 'Default' },
275+
didOptions : {
276+
services: [
277+
{
278+
id : 'dwn',
279+
type : 'DecentralizedWebNode',
280+
serviceEndpoint : serviceEndpointNodes,
281+
enc : '#enc',
282+
sig : '#sig',
283+
}
284+
],
285+
verificationMethods: [
286+
{
287+
algorithm : 'Ed25519',
288+
id : 'sig',
289+
purposes : ['assertionMethod', 'authentication']
290+
},
291+
{
292+
algorithm : 'secp256k1',
293+
id : 'enc',
294+
purposes : ['keyAgreement']
295+
}
296+
]
297+
}
298+
});
299+
300+
// Persists the Identity to be available in future sessions
301+
await userAgent.identity.manage({ portableIdentity: await identity.export() });
302+
} else if (existingIdentityCount === 1) {
303+
// An existing identity was found in the User Agent's tenant.
304+
identity = identities[0];
305+
} else {
306+
throw new Error(`connect() failed due to unexpected state: Expected 1 but found ${existingIdentityCount} stored identities.`);
327307
}
328308

309+
connectedDid = identity.did.uri;
310+
329311
if (registration !== undefined) {
330312
// If a registration object is passed, we attempt to register the AgentDID and the ConnectedDID with the DWN endpoints provided
331313
const serviceEndpointNodes = techPreview?.dwnEndpoints ?? didCreateOptions?.dwnEndpoints;
@@ -369,7 +351,6 @@ export class Web5 {
369351
}
370352

371353
const web5 = new Web5({ agent, connectedDid });
372-
373354
return { web5, did: connectedDid, recoveryPhrase };
374355
}
375-
}
356+
}

‎packages/api/tests/web5.spec.ts

+276-96
Large diffs are not rendered by default.

‎packages/credentials/package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@
7777
"dependencies": {
7878
"@sphereon/pex": "3.3.3",
7979
"@web5/common": "1.0.1",
80-
"@web5/crypto": "1.0.1",
80+
"@web5/crypto": "workspace:*",
8181
"@web5/dids": "1.1.1",
8282
"jsonschema": "1.4.1",
8383
"pako": "^2.1.0"
@@ -89,7 +89,7 @@
8989
"@types/chai": "4.3.16",
9090
"@types/eslint": "8.56.10",
9191
"@types/mocha": "10.0.6",
92-
"@types/node": "20.14.11",
92+
"@types/node": "20.14.8",
9393
"@types/pako": "^2.0.3",
9494
"@types/sinon": "17.0.3",
9595
"@typescript-eslint/eslint-plugin": "7.10.0",

‎packages/crypto-aws-kms/package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@
7171
},
7272
"dependencies": {
7373
"@aws-sdk/client-kms": "3.616.0",
74-
"@web5/crypto": "1.0.0"
74+
"@web5/crypto": "workspace:*"
7575
},
7676
"devDependencies": {
7777
"@playwright/test": "1.45.3",
@@ -99,4 +99,4 @@
9999
"source-map-loader": "5.0.0",
100100
"typescript": "5.4.5"
101101
}
102-
}
102+
}

‎packages/crypto/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@
8686
"@types/chai-as-promised": "7.1.8",
8787
"@types/eslint": "8.56.10",
8888
"@types/mocha": "10.0.6",
89-
"@types/node": "20.12.11",
89+
"@types/node": "20.14.8",
9090
"@types/sinon": "17.0.3",
9191
"@typescript-eslint/eslint-plugin": "7.14.1",
9292
"@typescript-eslint/parser": "7.14.1",

‎packages/crypto/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export * from './local-key-manager.js';
22
export * as utils from './utils.js';
3+
export * from './utils.js';
34

45
export * from './algorithms/aes-ctr.js';
56
export * from './algorithms/aes-gcm.js';

‎packages/crypto/src/utils.ts

+85-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
import type { Jwk } from './jose/jwk.js';
22

33
import { crypto } from '@noble/hashes/crypto';
44
import { randomBytes as nobleRandomBytes } from '@noble/hashes/utils';
@@ -72,6 +72,7 @@
7272
* If the `alg` property is present, its value takes precedence and is returned. Otherwise, the
7373
* `crv` property is used to determine the algorithm.
7474
*
75+
* @memberof CryptoUtils
7576
* @see {@link https://www.iana.org/assignments/jose/jose.xhtml#web-signature-encryption-algorithms | JOSE Algorithms}
7677
* @see {@link https://datatracker.ietf.org/doc/draft-ietf-jose-fully-specified-algorithms/ | Fully-Specified Algorithms for JOSE and COSE}
7778
*
@@ -85,7 +86,6 @@
8586
* const algorithm = getJoseSignatureAlgorithmFromPublicKey(publicKey);
8687
* console.log(algorithm); // Output: "EdDSA"
8788
* ```
88-
*
8989
* @param publicKey - A JWK containing the `alg` and/or `crv` properties.
9090
* @returns The name of the algorithm associated with the key.
9191
* @throws Error if the algorithm cannot be determined from the provided input.
@@ -156,6 +156,7 @@
156156
* Generates secure pseudorandom values of the specified length using
157157
* `crypto.getRandomValues`, which defers to the operating system.
158158
*
159+
* @memberof CryptoUtils
159160
* @remarks
160161
* This function is a wrapper around `randomBytes` from the '@noble/hashes'
161162
* package. It's designed to be cryptographically strong, suitable for
@@ -192,7 +193,7 @@
192193
* Note that while UUIDs are not guaranteed to be unique, they are
193194
* practically unique" given the large number of possible UUIDs and
194195
* the randomness of generation.
195-
*
196+
* @memberof CryptoUtils
196197
* @example
197198
* ```ts
198199
* const uuid = randomUuid();
@@ -205,4 +206,85 @@
205206
const uuid = crypto.randomUUID();
206207

207208
return uuid;
208-
}
209+
}
210+
211+
212+
/**
213+
* Generates a secure random PIN (Personal Identification Number) of a
214+
* specified length.
215+
*
216+
* This function ensures that the generated PIN is cryptographically secure and
217+
* uniformly distributed by using rejection sampling. It repeatedly generates
218+
* random numbers until it gets one in the desired range [0, max]. This avoids
219+
* bias introduced by simply taking the modulus or truncating the number.
220+
*
221+
* Note: The function can generate PINs of 3 to 10 digits in length.
222+
* Any request for a PIN outside this range will result in an error.
223+
*
224+
* Example usage:
225+
*
226+
* ```ts
227+
* const pin = randomPin({ length: 4 });
228+
* console.log(pin); // Outputs a 4-digit PIN, e.g., "0231"
229+
* ```
230+
* @memberof CryptoUtils
231+
* @param options - The options object containing the desired length of the generated PIN.
232+
* @param options.length - The desired length of the generated PIN. The value should be
233+
* an integer between 3 and 8 inclusive.
234+
*
235+
* @returns A string representing the generated PIN. The PIN will be zero-padded
236+
* to match the specified length, if necessary.
237+
*
238+
* @throws Will throw an error if the requested PIN length is less than 3 or greater than 8.
239+
*/
240+
export function randomPin({ length }: { length: number }): string {
241+
if (3 > length || length > 10) {
242+
throw new Error('randomPin() can securely generate a PIN between 3 to 10 digits.');
243+
}
244+
245+
const max = Math.pow(10, length) - 1;
246+
247+
let pin;
248+
249+
if (length <= 6) {
250+
const rejectionRange = Math.pow(10, length);
251+
do {
252+
// Adjust the byte generation based on length.
253+
const randomBuffer = randomBytes(Math.ceil(length / 2) ); // 2 digits per byte.
254+
const view = new DataView(randomBuffer.buffer);
255+
// Convert the buffer to integer and take modulus based on length.
256+
pin = view.getUint16(0, false) % rejectionRange;
257+
} while (pin > max);
258+
} else {
259+
const rejectionRange = Math.pow(10, 10); // For max 10 digit number.
260+
do {
261+
// Generates 4 random bytes.
262+
const randomBuffer = randomBytes(4);
263+
// Create a DataView to read from the randomBuffer.
264+
const view = new DataView(randomBuffer.buffer);
265+
// Transform bytes to number (big endian).
266+
pin = view.getUint32(0, false) % rejectionRange;
267+
} while (pin > max); // Reject if the number is outside the desired range.
268+
}
269+
270+
// Pad the PIN with leading zeros to the desired length.
271+
return pin.toString().padStart(length, '0');
272+
}
273+
274+
/** Utility functions for cryptographic operations. */
275+
export const CryptoUtils = {
276+
/** Generates a secure random PIN (Personal Identification Number) of a specified length. */
277+
randomPin,
278+
/** Generates a UUID following the version 4 format, as specified in RFC 4122. */
279+
randomUuid,
280+
/** Generates secure pseudorandom values of the specified length using `crypto.getRandomValues`, which defers to the operating system. */
281+
randomBytes,
282+
/** Checks if the Web Crypto API is supported in the current runtime environment. */
283+
isWebCryptoSupported,
284+
/** Determines the JOSE algorithm identifier of the digital signature algorithm based on the `alg` or `crv` property of a {@link Jwk | JWK}. */
285+
getJoseSignatureAlgorithmFromPublicKey,
286+
/** Checks whether the property specified is a member of the list of valid properties. */
287+
checkValidProperty,
288+
/** Checks whether the properties object provided contains the specified property. */
289+
checkRequiredProperty
290+
};

‎packages/crypto/tests/utils.spec.ts

+55
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
isWebCryptoSupported,
1111
checkRequiredProperty,
1212
getJoseSignatureAlgorithmFromPublicKey,
13+
randomPin
1314
} from '../src/utils.js';
1415

1516
// TODO: Remove this polyfill once Node.js v18 is no longer supported by @web5/crypto.
@@ -171,4 +172,58 @@ describe('Crypto Utils', () => {
171172
expect(set.size).to.equal(100);
172173
});
173174
});
175+
176+
describe('randomPin', () => {
177+
it('generates a 3-digit PIN', () => {
178+
const pin = randomPin({ length: 3 });
179+
expect(pin).to.match(/^\d{3}$/);
180+
});
181+
182+
it('generates a 4-digit PIN', () => {
183+
const pin = randomPin({ length: 4 });
184+
expect(pin).to.match(/^\d{4}$/);
185+
});
186+
187+
it('generates a 5-digit PIN', () => {
188+
const pin = randomPin({ length: 5 });
189+
expect(pin).to.match(/^\d{5}$/);
190+
});
191+
192+
it('generates a 6-digit PIN', () => {
193+
const pin = randomPin({ length: 6 });
194+
expect(pin).to.match(/^\d{6}$/);
195+
});
196+
197+
it('generates a 7-digit PIN', () => {
198+
const pin = randomPin({ length: 7 });
199+
expect(pin).to.match(/^\d{7}$/);
200+
});
201+
202+
it('generates an 8-digit PIN', () => {
203+
const pin = randomPin({ length: 8 });
204+
expect(pin).to.match(/^\d{8}$/);
205+
});
206+
207+
it('generates an 9-digit PIN', () => {
208+
const pin = randomPin({ length: 9 });
209+
expect(pin).to.match(/^\d{9}$/);
210+
});
211+
212+
it('generates an 10-digit PIN', () => {
213+
const pin = randomPin({ length: 10 });
214+
expect(pin).to.match(/^\d{10}$/);
215+
});
216+
217+
it('throws an error for a PIN length less than 3', () => {
218+
expect(
219+
() => randomPin({ length: 2 })
220+
).to.throw(Error, 'randomPin() can securely generate a PIN between 3 to 10 digits.');
221+
});
222+
223+
it('throws an error for a PIN length greater than 10', () => {
224+
expect(
225+
() => randomPin({ length: 11 })
226+
).to.throw(Error, 'randomPin() can securely generate a PIN between 3 to 10 digits.');
227+
});
228+
});
174229
});

‎packages/dids/package.json

+3-3
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@
7979
"@decentralized-identity/ion-sdk": "1.0.4",
8080
"@dnsquery/dns-packet": "6.1.1",
8181
"@web5/common": "1.0.0",
82-
"@web5/crypto": "1.0.1",
82+
"@web5/crypto": "workspace:*",
8383
"abstract-level": "1.0.4",
8484
"bencode": "4.0.0",
8585
"buffer": "6.0.3",
@@ -94,7 +94,7 @@
9494
"@types/eslint": "8.56.10",
9595
"@types/mocha": "10.0.7",
9696
"@types/ms": "0.7.34",
97-
"@types/node": "20.12.12",
97+
"@types/node": "20.14.8",
9898
"@types/sinon": "17.0.3",
9999
"@typescript-eslint/eslint-plugin": "7.9.0",
100100
"@typescript-eslint/parser": "7.14.1",
@@ -114,4 +114,4 @@
114114
"source-map-loader": "5.0.0",
115115
"typescript": "5.5.4"
116116
}
117-
}
117+
}

‎packages/identity-agent/package.json

+4-4
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@
7171
"dependencies": {
7272
"@web5/agent": "workspace:*",
7373
"@web5/common": "1.0.0",
74-
"@web5/crypto": "1.0.0",
74+
"@web5/crypto": "workspace:*",
7575
"@web5/dids": "1.1.0"
7676
},
7777
"devDependencies": {
@@ -80,8 +80,8 @@
8080
"@types/chai-as-promised": "7.1.5",
8181
"@types/eslint": "8.56.10",
8282
"@types/mocha": "10.0.1",
83-
"@types/node": "20.11.19",
84-
"@types/sinon": "17.0.2",
83+
"@types/node": "20.14.8",
84+
"@types/sinon": "17.0.3",
8585
"@typescript-eslint/eslint-plugin": "7.9.0",
8686
"@typescript-eslint/parser": "7.14.1",
8787
"@web/test-runner": "0.18.2",
@@ -97,7 +97,7 @@
9797
"node-stdlib-browser": "1.2.0",
9898
"playwright": "1.45.3",
9999
"rimraf": "4.4.0",
100-
"sinon": "16.1.3",
100+
"sinon": "18.0.0",
101101
"typescript": "5.1.6"
102102
}
103103
}

‎packages/proxy-agent/package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@
7171
"dependencies": {
7272
"@web5/agent": "workspace:*",
7373
"@web5/common": "1.0.0",
74-
"@web5/crypto": "1.0.0",
74+
"@web5/crypto": "workspace:*",
7575
"@web5/dids": "1.1.0"
7676
},
7777
"devDependencies": {
@@ -81,7 +81,7 @@
8181
"@types/dns-packet": "5.6.4",
8282
"@types/eslint": "8.56.10",
8383
"@types/mocha": "10.0.1",
84-
"@types/node": "20.11.19",
84+
"@types/node": "20.14.8",
8585
"@typescript-eslint/eslint-plugin": "7.9.0",
8686
"@typescript-eslint/parser": "7.14.1",
8787
"@web/test-runner": "0.18.2",

‎packages/user-agent/package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@
7171
"dependencies": {
7272
"@web5/agent": "workspace:*",
7373
"@web5/common": "1.0.0",
74-
"@web5/crypto": "1.0.0",
74+
"@web5/crypto": "workspace:*",
7575
"@web5/dids": "1.1.0"
7676
},
7777
"devDependencies": {
@@ -81,7 +81,7 @@
8181
"@types/dns-packet": "5.6.4",
8282
"@types/eslint": "8.56.10",
8383
"@types/mocha": "10.0.1",
84-
"@types/node": "20.11.19",
84+
"@types/node": "20.14.8",
8585
"@typescript-eslint/eslint-plugin": "7.9.0",
8686
"@typescript-eslint/parser": "7.14.1",
8787
"@web/test-runner": "0.18.2",

‎pnpm-lock.yaml

+1,331-1,761
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎tsconfig.json

+5-3
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"DOM",
55
"ES2022",
66
],
7-
"allowJs": true,
7+
"allowJs": false,
88
"strict": true,
99
"declaration": true,
1010
"declarationMap": true,
@@ -15,6 +15,8 @@
1515
// reference: https://devblogs.microsoft.com/typescript/announcing-typescript-4-7/#ecmascript-module-support-in-node-js
1616
"esModuleInterop": true,
1717
"resolveJsonModule": true,
18-
"moduleResolution": "NodeNext"
19-
}
18+
"moduleResolution": "NodeNext",
19+
"skipLibCheck": true
20+
},
21+
"exclude": ["eslint.config.cjs", "data", "bin", "web5-spec", "node_modules"]
2022
}

0 commit comments

Comments
 (0)
Please sign in to comment.