From 5a4123f6b46d46b5807026ebe4aaa73f14eebf22 Mon Sep 17 00:00:00 2001
From: Benjamin Goering <171782+gobengo@users.noreply.github.com>
Date: Tue, 4 Apr 2023 13:56:14 -0700
Subject: [PATCH] feat!: add did mailto package, replacing
`createDidMailtoFromEmail` (#722)
Motivation:
* #682
Breaking Changes
* remove `createDidMailtoFromEmail` export from
`@web3-storage/access/agent`
https://github.com/web3-storage/w3up/pull/722/commits/93e29c6afd0a0ec7a0eb3997ea1197533f7cff3a#diff-69a4efe733b2d7920dc103a0370eb1a285403e39c41e1b5e9a0718ea66b5a32fL33
* no dependencies under our org:
https://github.com/search?q=org%3Aweb3-storage%20createDidMailtoFromEmail&type=code
* Motivation: encouragement [from
@Gozala](https://github.com/web3-storage/w3up/pull/722#discussion_r1156572341)
and
[@travis](https://github.com/web3-storage/w3up/pull/722#discussion_r1157552497)
in review
---
.github/release-please-config.json | 1 +
.github/workflows/did-mailto.yml | 37 ++++++++
.github/workflows/release.yml | 1 +
packages/access-api/package.json | 1 +
.../access-api/src/routes/validate-email.js | 4 +-
.../src/service/access-authorize.js | 4 +-
packages/access-api/src/utils/did-mailto.js | 12 ---
.../access-api/test/access-authorize.test.js | 4 +-
packages/access-api/test/access-claim.test.js | 4 +-
.../test/access-client-agent.test.js | 10 +--
.../access-api/test/access-delegate.test.js | 8 +-
packages/access-api/test/provider-add.test.js | 2 +-
packages/access-api/test/provisions.test.js | 2 +-
packages/access-api/tsconfig.json | 6 +-
packages/access-client/package.json | 1 +
packages/access-client/src/agent-use-cases.js | 11 +--
packages/access-client/src/agent.js | 3 +-
.../access-client/src/utils/did-mailto.js | 15 ----
packages/access-client/test/agent.test.js | 9 +-
packages/access-client/tsconfig.json | 2 +-
packages/did-mailto/package.json | 67 ++++++++++++++
packages/did-mailto/src/index.js | 74 ++++++++++++++++
packages/did-mailto/src/types.ts | 12 +++
packages/did-mailto/test/did-mailto.spec.js | 88 +++++++++++++++++++
packages/did-mailto/test/test-types.ts | 5 ++
packages/did-mailto/tsconfig.json | 9 ++
pnpm-lock.yaml | 16 ++++
27 files changed, 346 insertions(+), 62 deletions(-)
create mode 100644 .github/workflows/did-mailto.yml
delete mode 100644 packages/access-api/src/utils/did-mailto.js
delete mode 100644 packages/access-client/src/utils/did-mailto.js
create mode 100644 packages/did-mailto/package.json
create mode 100644 packages/did-mailto/src/index.js
create mode 100644 packages/did-mailto/src/types.ts
create mode 100644 packages/did-mailto/test/did-mailto.spec.js
create mode 100644 packages/did-mailto/test/test-types.ts
create mode 100644 packages/did-mailto/tsconfig.json
diff --git a/.github/release-please-config.json b/.github/release-please-config.json
index a65c8870e..2ece5dad8 100644
--- a/.github/release-please-config.json
+++ b/.github/release-please-config.json
@@ -5,6 +5,7 @@
"packages/access-client": {},
"packages/access-api": {},
"packages/capabilities": {},
+ "packages/did-mailto": {},
"packages/upload-api": {},
"packages/upload-client": {},
"packages/w3up-client": {}
diff --git a/.github/workflows/did-mailto.yml b/.github/workflows/did-mailto.yml
new file mode 100644
index 000000000..b82464464
--- /dev/null
+++ b/.github/workflows/did-mailto.yml
@@ -0,0 +1,37 @@
+name: did-mailto
+on:
+ push:
+ branches:
+ - main
+ paths:
+ - 'packages/did-mailto/**'
+ - '.github/workflows/did-mailto.yml'
+ - 'pnpm-lock.yaml'
+ pull_request:
+ paths:
+ - 'packages/did-mailto/**'
+ - '.github/workflows/did-mailto.yml'
+ - 'pnpm-lock.yaml'
+jobs:
+ test:
+ name: Test
+ runs-on: ubuntu-latest
+ defaults:
+ run:
+ working-directory: ./packages/did-mailto
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v3
+ - name: Install
+ uses: pnpm/action-setup@v2.2.3
+ with:
+ version: 7
+ - name: Setup
+ uses: actions/setup-node@v3
+ with:
+ node-version: 18
+ registry-url: https://registry.npmjs.org/
+ cache: 'pnpm'
+ - run: pnpm --filter '@web3-storage/did-mailto...' install
+ - run: pnpm --filter '@web3-storage/did-mailto' lint
+ - run: pnpm --filter '@web3-storage/did-mailto' test
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 842009d71..f3d7ee303 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -34,6 +34,7 @@ jobs:
if: |
contains(fromJson(needs.release.outputs.paths_released), 'packages/access-client') ||
contains(fromJson(needs.release.outputs.paths_released), 'packages/capabilities') ||
+ contains(fromJson(needs.release.outputs.paths_released), 'packages/did-mailto') ||
contains(fromJson(needs.release.outputs.paths_released), 'packages/upload-client') ||
contains(fromJson(needs.release.outputs.paths_released), 'packages/upload-api') ||
contains(fromJson(needs.release.outputs.paths_released), 'packages/w3up-client')
diff --git a/packages/access-api/package.json b/packages/access-api/package.json
index 61dae28ce..eada063e3 100644
--- a/packages/access-api/package.json
+++ b/packages/access-api/package.json
@@ -27,6 +27,7 @@
"@ucanto/validator": "^6.1.0",
"@web3-storage/access": "workspace:^",
"@web3-storage/capabilities": "workspace:^",
+ "@web3-storage/did-mailto": "workspace:^",
"@web3-storage/worker-utils": "0.4.3-dev",
"dotenv": "^16.0.3",
"kysely": "^0.23.4",
diff --git a/packages/access-api/src/routes/validate-email.js b/packages/access-api/src/routes/validate-email.js
index f4bc8c234..beb666fde 100644
--- a/packages/access-api/src/routes/validate-email.js
+++ b/packages/access-api/src/routes/validate-email.js
@@ -4,7 +4,7 @@ import {
} from '@web3-storage/access/encoding'
import * as Access from '@web3-storage/capabilities/access'
import QRCode from 'qrcode'
-import { toEmail } from '../utils/did-mailto.js'
+import * as DidMailto from '@web3-storage/did-mailto'
import {
HtmlResponse,
ValidateEmail,
@@ -173,7 +173,7 @@ async function authorize(req, env) {
return new HtmlResponse(
(
diff --git a/packages/access-api/src/service/access-authorize.js b/packages/access-api/src/service/access-authorize.js
index 4732be9b8..270292b86 100644
--- a/packages/access-api/src/service/access-authorize.js
+++ b/packages/access-api/src/service/access-authorize.js
@@ -1,6 +1,6 @@
import * as Server from '@ucanto/server'
import * as Access from '@web3-storage/capabilities/access'
-import * as Mailto from '../utils/did-mailto.js'
+import * as DidMailto from '@web3-storage/did-mailto'
import { delegationToString } from '@web3-storage/access/encoding'
/**
@@ -52,7 +52,7 @@ export function accessAuthorizeProvider(ctx) {
const url = `${ctx.url.protocol}//${ctx.url.host}/validate-email?ucan=${encoded}&mode=authorize`
await ctx.email.sendValidation({
- to: Mailto.toEmail(capability.nb.iss),
+ to: DidMailto.toEmail(DidMailto.fromString(capability.nb.iss)),
url,
})
diff --git a/packages/access-api/src/utils/did-mailto.js b/packages/access-api/src/utils/did-mailto.js
deleted file mode 100644
index bf2e26525..000000000
--- a/packages/access-api/src/utils/did-mailto.js
+++ /dev/null
@@ -1,12 +0,0 @@
-/**
- *
- * @param {`did:${string}:${string}`} did
- * @returns
- */
-export function toEmail(did) {
- const parts = did.split(':')
- if (parts[1] !== 'mailto') {
- throw new Error(`DID ${did} is not a mailto did.`)
- }
- return `${decodeURIComponent(parts[3])}@${decodeURIComponent(parts[2])}`
-}
diff --git a/packages/access-api/test/access-authorize.test.js b/packages/access-api/test/access-authorize.test.js
index 111858ded..91867c8f5 100644
--- a/packages/access-api/test/access-authorize.test.js
+++ b/packages/access-api/test/access-authorize.test.js
@@ -10,7 +10,7 @@ import { Accounts } from '../src/models/accounts.js'
import { context } from './helpers/context.js'
// @ts-ignore
import isSubset from 'is-subset'
-import { toEmail } from '../src/utils/did-mailto.js'
+import * as DidMailto from '@web3-storage/did-mailto'
import {
warnOnErrorResult,
registerSpaces,
@@ -126,7 +126,7 @@ describe('access/authorize', function () {
const html = await rsp.text()
assert(html.includes('Email Validated'))
- assert(html.includes(toEmail(accountDID)))
+ assert(html.includes(DidMailto.toEmail(accountDID)))
assert(html.includes(issuer.did()))
})
diff --git a/packages/access-api/test/access-claim.test.js b/packages/access-api/test/access-claim.test.js
index 97bc84362..7ae227f6f 100644
--- a/packages/access-api/test/access-claim.test.js
+++ b/packages/access-api/test/access-claim.test.js
@@ -16,7 +16,9 @@ for (const handlerVariant of /** @type {const} */ ([
spaceWithStorageProvider,
...createTesterFromContext(() => context(), {
registerSpaces: [spaceWithStorageProvider],
- account: { did: () => /** @type {const} */ ('did:mailto:foo') },
+ account: {
+ did: () => /** @type {const} */ ('did:mailto:example.com:foo'),
+ },
}),
}
})(),
diff --git a/packages/access-api/test/access-client-agent.test.js b/packages/access-api/test/access-client-agent.test.js
index 459475360..3b07167a3 100644
--- a/packages/access-api/test/access-client-agent.test.js
+++ b/packages/access-api/test/access-client-agent.test.js
@@ -4,6 +4,7 @@ import {
assertNotError,
createTesterFromContext,
} from './helpers/ucanto-test-utils.js'
+import * as DidMailto from '@web3-storage/did-mailto'
import * as principal from '@ucanto/principal'
import {
addProvider,
@@ -11,7 +12,6 @@ import {
Agent as AccessAgent,
authorizeAndWait,
claimAccess,
- createDidMailtoFromEmail,
delegationsIncludeSessionProof,
pollAccessClaimUntil,
requestAccess,
@@ -80,7 +80,7 @@ for (const accessApiVariant of /** @type {const} */ ([
const abort = new AbortController()
after(() => abort.abort())
const account = {
- did: () => createDidMailtoFromEmail('example@dag.house'),
+ did: () => DidMailto.fromEmail(DidMailto.email('example@dag.house')),
}
await requestAccess(accessAgent, account, [{ can: '*' }])
assert.deepEqual(emails.length, emailCount + 1)
@@ -184,8 +184,8 @@ for (const accessApiVariant of /** @type {const} */ ([
it('can registerSpace', async () => {
const { connection, emails } = await accessApiVariant.create()
- const accountEmail = 'foo@dag.house'
- const account = { did: () => createDidMailtoFromEmail(accountEmail) }
+ const accountEmail = DidMailto.email('foo@dag.house')
+ const account = { did: () => DidMailto.fromEmail(accountEmail) }
const accessAgent = await AccessAgent.create(undefined, {
connection,
})
@@ -615,7 +615,7 @@ async function testSessionAuthorization(service, access, account, emails) {
* @returns {Ucanto.DID<'mailto'>}
*/
function thisEmailDidMailto() {
- return createDidMailtoFromEmail(this.email)
+ return DidMailto.fromEmail(DidMailto.email(this.email))
}
/**
diff --git a/packages/access-api/test/access-delegate.test.js b/packages/access-api/test/access-delegate.test.js
index a9bcb2048..88a944c26 100644
--- a/packages/access-api/test/access-delegate.test.js
+++ b/packages/access-api/test/access-delegate.test.js
@@ -26,7 +26,9 @@ for (const handlerVariant of /** @type {const} */ ([
name: 'handled by access-api in miniflare',
...(() => {
const spaceWithStorageProvider = principal.ed25519.generate()
- const account = { did: () => /** @type {const} */ ('did:mailto:foo') }
+ const account = {
+ did: () => /** @type {const} */ ('did:mailto:example.com:foo'),
+ }
return {
spaceWithStorageProvider,
...createTesterFromContext(() => context(), {
@@ -125,7 +127,9 @@ for (const variant of /** @type {const} */ ([
name: 'handled by access-api in miniflare',
...(() => {
const spaceWithStorageProvider = principal.ed25519.generate()
- const account = { did: () => /** @type {const} */ ('did:mailto:foo') }
+ const account = {
+ did: () => /** @type {const} */ ('did:mailto:example.com:foo'),
+ }
return {
spaceWithStorageProvider,
...createTesterFromContext(() => context(), {
diff --git a/packages/access-api/test/provider-add.test.js b/packages/access-api/test/provider-add.test.js
index 7be6fa401..46e4659b2 100644
--- a/packages/access-api/test/provider-add.test.js
+++ b/packages/access-api/test/provider-add.test.js
@@ -270,7 +270,7 @@ const setup = async (options = {}) => {
const context = await createContextWithMailbox(options)
const space = await principal.ed25519.generate()
const agent = await principal.ed25519.generate()
- const account = principal.Absentee.from({ id: 'did:mailto:foo' })
+ const account = principal.Absentee.from({ id: 'did:mailto:example.com:foo' })
return { ...context, space, agent, account }
}
diff --git a/packages/access-api/test/provisions.test.js b/packages/access-api/test/provisions.test.js
index 8fc8d6102..bb66553ab 100644
--- a/packages/access-api/test/provisions.test.js
+++ b/packages/access-api/test/provisions.test.js
@@ -65,7 +65,7 @@ describe('DbProvisions', () => {
const modifiedFirstProvision = {
...firstProvision,
space: /** @type {const} */ ('did:key:foo'),
- account: /** @type {const} */ ('did:mailto:foo'),
+ account: /** @type {const} */ ('did:mailto:example.com:foo'),
// note this type assertion is wrong, but useful to set up the test
provider: /** @type {import('@ucanto/interface').DID<'web'>} */ (
'did:provider:foo'
diff --git a/packages/access-api/tsconfig.json b/packages/access-api/tsconfig.json
index da475de58..1facffca5 100644
--- a/packages/access-api/tsconfig.json
+++ b/packages/access-api/tsconfig.json
@@ -8,5 +8,9 @@
},
"include": ["src", "scripts", "test", "package.json", "sql"],
"exclude": ["**/node_modules/**"],
- "references": [{ "path": "../access-client" }, { "path": "../capabilities" }]
+ "references": [
+ { "path": "../access-client" },
+ { "path": "../capabilities" },
+ { "path": "../did-mailto" }
+ ]
}
diff --git a/packages/access-client/package.json b/packages/access-client/package.json
index 387b65ce3..d073fc343 100644
--- a/packages/access-client/package.json
+++ b/packages/access-client/package.json
@@ -67,6 +67,7 @@
"@ucanto/transport": "^5.1.1",
"@ucanto/validator": "^6.1.0",
"@web3-storage/capabilities": "workspace:^",
+ "@web3-storage/did-mailto": "workspace:^",
"bigint-mod-arith": "^3.1.2",
"conf": "10.2.0",
"inquirer": "^9.1.4",
diff --git a/packages/access-client/src/agent-use-cases.js b/packages/access-client/src/agent-use-cases.js
index 659180fcd..09306d150 100644
--- a/packages/access-client/src/agent-use-cases.js
+++ b/packages/access-client/src/agent-use-cases.js
@@ -1,8 +1,4 @@
-import {
- addSpacesFromDelegations,
- Agent as AccessAgent,
- createDidMailtoFromEmail,
-} from './agent.js'
+import { addSpacesFromDelegations, Agent as AccessAgent } from './agent.js'
import * as Ucanto from '@ucanto/interface'
import * as Access from '@web3-storage/capabilities/access'
import { bytesToDelegations, stringToDelegation } from './encoding.js'
@@ -12,6 +8,7 @@ import { Websocket, AbortError } from './utils/ws.js'
import { AgentData, isSessionProof } from './agent-data.js'
import * as ucanto from '@ucanto/core'
import { DID as DIDValidator } from '@ucanto/validator'
+import * as DidMailto from '@web3-storage/did-mailto'
/**
* Request access by a session allowing this agent to issue UCANs
@@ -242,7 +239,7 @@ export async function waitForAuthorizationByPolling(access, opts = {}) {
export async function authorizeAndWait(access, email, opts = {}) {
const expectAuthorization =
opts.expectAuthorization || waitForAuthorizationByPolling
- const account = { did: () => createDidMailtoFromEmail(email) }
+ const account = { did: () => DidMailto.fromEmail(email) }
await requestAccess(
access,
account,
@@ -337,7 +334,7 @@ export async function addProviderAndDelegateToAccount(
if (spaceMeta && spaceMeta.isRegistered) {
throw new Error('Space already registered with web3.storage.')
}
- const account = { did: () => createDidMailtoFromEmail(email) }
+ const account = { did: () => DidMailto.fromEmail(DidMailto.email(email)) }
await addProvider({ access, space, account, provider })
const delegateSpaceAccessResult = await delegateSpaceAccessToAccount(
access,
diff --git a/packages/access-client/src/agent.js b/packages/access-client/src/agent.js
index 1532a4461..516653236 100644
--- a/packages/access-client/src/agent.js
+++ b/packages/access-client/src/agent.js
@@ -24,13 +24,12 @@ import {
canDelegateCapability,
} from './delegations.js'
import { AgentData, getSessionProofs } from './agent-data.js'
-import { createDidMailtoFromEmail } from './utils/did-mailto.js'
import {
addProviderAndDelegateToAccount,
waitForDelegationOnSocket,
} from './agent-use-cases.js'
-export { AgentData, createDidMailtoFromEmail }
+export { AgentData }
export * from './agent-use-cases.js'
const HOST = 'https://access.web3.storage'
diff --git a/packages/access-client/src/utils/did-mailto.js b/packages/access-client/src/utils/did-mailto.js
deleted file mode 100644
index 45480755e..000000000
--- a/packages/access-client/src/utils/did-mailto.js
+++ /dev/null
@@ -1,15 +0,0 @@
-/**
- * @param {string} email
- * @returns {`did:mailto:${string}:${string}`}
- */
-export function createDidMailtoFromEmail(email) {
- const emailParts = email.split('@')
- if (emailParts.length !== 2) {
- throw new Error(`unexpected email ${email}`)
- }
- const [local, domain] = emailParts
- const did = /** @type {const} */ (
- `did:mailto:${encodeURIComponent(domain)}:${encodeURIComponent(local)}`
- )
- return did
-}
diff --git a/packages/access-client/test/agent.test.js b/packages/access-client/test/agent.test.js
index 319e954a0..8c8f401fa 100644
--- a/packages/access-client/test/agent.test.js
+++ b/packages/access-client/test/agent.test.js
@@ -1,6 +1,6 @@
import assert from 'assert'
import { URI } from '@ucanto/validator'
-import { Agent, connection, createDidMailtoFromEmail } from '../src/agent.js'
+import { Agent, connection } from '../src/agent.js'
import * as Space from '@web3-storage/capabilities/space'
import { createServer } from './helpers/utils.js'
import * as fixtures from './helpers/fixtures.js'
@@ -253,11 +253,4 @@ describe('Agent', function () {
/cannot delegate capability store\/remove/
)
})
-
- it('exports createDidMailtoFromEmail', async () => {
- assert.deepEqual(
- createDidMailtoFromEmail('foo@dag.house'),
- 'did:mailto:dag.house:foo'
- )
- })
})
diff --git a/packages/access-client/tsconfig.json b/packages/access-client/tsconfig.json
index 8f9db5b03..e3b39cb5c 100644
--- a/packages/access-client/tsconfig.json
+++ b/packages/access-client/tsconfig.json
@@ -7,5 +7,5 @@
},
"include": ["src", "scripts", "test", "package.json"],
"exclude": ["**/node_modules/**"],
- "references": [{ "path": "../capabilities" }]
+ "references": [{ "path": "../capabilities" }, { "path": "../did-mailto" }]
}
diff --git a/packages/did-mailto/package.json b/packages/did-mailto/package.json
new file mode 100644
index 000000000..946222408
--- /dev/null
+++ b/packages/did-mailto/package.json
@@ -0,0 +1,67 @@
+{
+ "name": "@web3-storage/did-mailto",
+ "version": "1.0.0",
+ "description": "did:mailto",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/web3-storage/w3up.git",
+ "directory": "packages/did-mailto"
+ },
+ "license": "(Apache-2.0 OR MIT)",
+ "type": "module",
+ "types": "dist/src/index.d.ts",
+ "main": "src/index.js",
+ "files": [
+ "src",
+ "test",
+ "dist/**/*.d.ts",
+ "dist/**/*.d.ts.map"
+ ],
+ "exports": {
+ ".": {
+ "types": "./dist/src/index.d.ts",
+ "import": "./src/index.js"
+ }
+ },
+ "scripts": {
+ "build": "tsc --build",
+ "check": "tsc --build",
+ "lint": "tsc --build",
+ "test": "mocha --bail --timeout 10s -n no-warnings -n experimental-vm-modules -n experimental-fetch test/**/*.spec.js",
+ "test-watch": "pnpm build && mocha --bail --timeout 10s --watch --parallel -n no-warnings -n experimental-vm-modules -n experimental-fetch --watch-files src,test"
+ },
+ "devDependencies": {
+ "@types/mocha": "^10.0.1",
+ "@types/node": "^18.11.18",
+ "hd-scripts": "^4.1.0",
+ "mocha": "^10.2.0"
+ },
+ "eslintConfig": {
+ "extends": [
+ "./node_modules/hd-scripts/eslint/index.js"
+ ],
+ "parserOptions": {
+ "project": "./tsconfig.json"
+ },
+ "rules": {
+ "jsdoc/no-undefined-types": [
+ "error",
+ {
+ "definedTypes": [
+ "Iterable"
+ ]
+ }
+ ]
+ },
+ "env": {
+ "mocha": true
+ },
+ "ignorePatterns": [
+ "dist",
+ "coverage"
+ ]
+ },
+ "engines": {
+ "node": ">=16.15"
+ }
+}
diff --git a/packages/did-mailto/src/index.js b/packages/did-mailto/src/index.js
new file mode 100644
index 000000000..c41ccd835
--- /dev/null
+++ b/packages/did-mailto/src/index.js
@@ -0,0 +1,74 @@
+export const foo = 1
+
+/**
+ * create a did:mailto from an email address
+ *
+ * @param {import("./types").EmailAddress} email
+ * @returns {import("./types").DidMailto}
+ */
+export function fromEmail(email) {
+ const { domain, local } = parseEmail(email)
+ const did = /** @type {const} */ (
+ `did:mailto:${encodeURIComponent(domain)}:${encodeURIComponent(local)}`
+ )
+ return did
+}
+
+/**
+ * @param {import("./types").DidMailto} did
+ * @returns {import("./types").EmailAddress}
+ */
+export function toEmail(did) {
+ const parts = did.split(':')
+ if (parts[1] !== 'mailto') {
+ throw new Error(`DID ${did} is not a mailto did.`)
+ }
+ return `${decodeURIComponent(parts[3])}@${decodeURIComponent(parts[2])}`
+}
+
+/**
+ * given a string, if it is an EmailAddress, return it, otherwise throw an error.
+ * Use this to parse string input to `EmailAddress` type to pass to `fromEmail` (when needed).
+ * This is not meant to be a general RFC5322 (et al) email address validator, which would be more expensive.
+ *
+ * @param {string} input
+ * @returns {import("./types").EmailAddress}
+ */
+export function email(input) {
+ const { domain, local } = parseEmail(input)
+ /** @type {import("./types").EmailAddress} */
+ const emailAddress = `${local}@${domain}`
+ return emailAddress
+}
+
+/**
+ * parse a did mailto from a string
+ *
+ * @param {string} input
+ * @returns {import("./types").DidMailto}
+ */
+export function fromString(input) {
+ const colonParts = input.split(':')
+ if (colonParts.length !== 4) {
+ throw new TypeError(
+ `expected did:mailto to have 4 colon-delimited segments, but got ${colonParts.length}`
+ )
+ }
+ const [domain, local] = [colonParts[2], colonParts[3]]
+ return `did:mailto:${domain}:${local}`
+}
+
+/**
+ * @param {string} email
+ */
+function parseEmail(email) {
+ const atParts = email.split('@')
+ if (atParts.length < 2) {
+ throw new TypeError(
+ `expected at least 2 @-delimtied segments, but got ${atParts.length}`
+ )
+ }
+ const domain = atParts.at(-1) ?? ''
+ const local = atParts.slice(0, -1).join('@')
+ return { domain, local }
+}
diff --git a/packages/did-mailto/src/types.ts b/packages/did-mailto/src/types.ts
new file mode 100644
index 000000000..51c2a45b5
--- /dev/null
+++ b/packages/did-mailto/src/types.ts
@@ -0,0 +1,12 @@
+// https://datatracker.ietf.org/doc/html/rfc5322#section-3.4.1
+export type LocalPart = string
+export type Domain = string
+export type EmailAddress = `${LocalPart}@${Domain}`
+
+// https://www.rfc-editor.org/rfc/rfc3986#section-2.1
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
+export type PercentEncoded = string
+
+// did:mailto
+export type DidMailto =
+ `did:mailto:${PercentEncoded}:${PercentEncoded}`
diff --git a/packages/did-mailto/test/did-mailto.spec.js b/packages/did-mailto/test/did-mailto.spec.js
new file mode 100644
index 000000000..216054266
--- /dev/null
+++ b/packages/did-mailto/test/did-mailto.spec.js
@@ -0,0 +1,88 @@
+import * as assert from 'assert'
+import * as didMailto from '../src/index.js'
+
+describe('did-mailto', () => {
+ testDidMailto(didMailto, async (name, test) => it(name, test))
+})
+
+/**
+ * @param {typeof didMailto} didMailto
+ * @param {import("./test-types").TestAdder} test
+ */
+function testDidMailto(didMailto, test) {
+ test('module is an object', async () => {
+ assert.equal(typeof didMailto, 'object')
+ })
+ for (const { email, did } of examples()) {
+ test(`fromEmail("${email}")`, async () => {
+ assert.deepStrictEqual(
+ didMailto.fromEmail(email),
+ did
+ )
+ })
+ test(`toEmail("${did}")`, async () => {
+ assert.deepStrictEqual(
+ didMailto.toEmail(did),
+ email
+ )
+ })
+ test(`toEmail(fromEmail("${email}"))`, async () => {
+ assert.deepStrictEqual(
+ didMailto.toEmail(didMailto.fromEmail(email)),
+ email
+ )
+ })
+ test(`fromEmail(toEmail("${did}"))`, async () => {
+ assert.deepStrictEqual(
+ didMailto.fromEmail(didMailto.toEmail(did)),
+ did
+ )
+ })
+ }
+ for (const email of validEmailAddresses()) {
+ test(`email("${email}")`, async () => {
+ assert.doesNotThrow(
+ () => didMailto.email(email),
+ 'can parse to email'
+ )
+ })
+ }
+}
+
+function* examples() {
+ yield {
+ email: didMailto.email('example+123@example.com'),
+ did: didMailto.fromString('did:mailto:example.com:example%2B123'),
+ }
+ yield {
+ email: didMailto.email('"email@1"@example.com'),
+ did: didMailto.fromString(`did:mailto:example.com:%22email%401%22`),
+ }
+}
+
+function* validEmailAddresses() {
+ // https://gist.github.com/cjaoude/fd9910626629b53c4d25#file-gistfile1-txt-L5
+ yield* [
+ 'email@example.com',
+ 'firstname.lastname@example.com',
+ 'email@subdomain.example.com',
+ 'firstname+lastname@example.com',
+ 'email@123.123.123.123',
+ 'email@[123.123.123.123]',
+ '"email"@example.com',
+ '"email@1"@example.com',
+ '1234567890@example.com',
+ 'email@example-one.com',
+ '_______@example.com',
+ 'email@example.name',
+ 'email@example.museum',
+ 'email@example.co.jp',
+ 'firstname-lastname@example.com',
+ ]
+ // https://gist.github.com/cjaoude/fd9910626629b53c4d25#file-gistfile1-txt-L24
+ yield* [
+ 'much.”more\\ unusual”@example.com',
+ 'very.unusual.”@”.unusual.com@example.com',
+ 'very.”(),:;<>[]”.VERY.”very@\\ "very”.unusual@strange.example.com',
+ ]
+}
diff --git a/packages/did-mailto/test/test-types.ts b/packages/did-mailto/test/test-types.ts
new file mode 100644
index 000000000..9f53b588d
--- /dev/null
+++ b/packages/did-mailto/test/test-types.ts
@@ -0,0 +1,5 @@
+// similar to mocha `it`
+export type TestAdder = (
+ name: string,
+ runTest: () => Promise
+) => Promise
diff --git a/packages/did-mailto/tsconfig.json b/packages/did-mailto/tsconfig.json
new file mode 100644
index 000000000..ab5d5d625
--- /dev/null
+++ b/packages/did-mailto/tsconfig.json
@@ -0,0 +1,9 @@
+{
+ "extends": "../../tsconfig.json",
+ "compilerOptions": {
+ "outDir": "dist"
+ },
+ "include": ["src", "test"],
+ "exclude": ["**/node_modules/**", "dist"],
+ "references": []
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 7f81731ab..022f6be64 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -48,6 +48,7 @@ importers:
'@ucanto/validator': ^6.1.0
'@web3-storage/access': workspace:^
'@web3-storage/capabilities': workspace:^
+ '@web3-storage/did-mailto': workspace:^
'@web3-storage/worker-utils': 0.4.3-dev
better-sqlite3: 8.0.1
buffer: ^6.0.3
@@ -83,6 +84,7 @@ importers:
'@ucanto/validator': 6.1.0
'@web3-storage/access': link:../access-client
'@web3-storage/capabilities': link:../capabilities
+ '@web3-storage/did-mailto': link:../did-mailto
'@web3-storage/worker-utils': 0.4.3-dev
dotenv: 16.0.3
kysely: 0.23.4
@@ -141,6 +143,7 @@ importers:
'@ucanto/transport': ^5.1.1
'@ucanto/validator': ^6.1.0
'@web3-storage/capabilities': workspace:^
+ '@web3-storage/did-mailto': workspace:^
assert: ^2.0.0
bigint-mod-arith: ^3.1.2
conf: 10.2.0
@@ -173,6 +176,7 @@ importers:
'@ucanto/transport': 5.1.1
'@ucanto/validator': 6.1.0
'@web3-storage/capabilities': link:../capabilities
+ '@web3-storage/did-mailto': link:../did-mailto
bigint-mod-arith: 3.1.2
conf: 10.2.0
inquirer: 9.1.4
@@ -240,6 +244,18 @@ importers:
typescript: 4.9.5
watch: 1.0.2
+ packages/did-mailto:
+ specifiers:
+ '@types/mocha': ^10.0.1
+ '@types/node': ^18.11.18
+ hd-scripts: ^4.1.0
+ mocha: ^10.2.0
+ devDependencies:
+ '@types/mocha': 10.0.1
+ '@types/node': 18.15.5
+ hd-scripts: 4.1.0
+ mocha: 10.2.0
+
packages/upload-api:
specifiers:
'@ipld/car': ^5.1.1